objects/calendar.go

191 lines
4.7 KiB
Go

package objects
import "fmt"
import "time"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/objects/layouts"
var _ tomo.Object = new(Calendar)
// Calendar is an object that can display a date and allow the user to change
// it. It can display one month at a time.
//
// Sub-components:
// - CalendarGrid organizes the days into a grid.
// - CalendarWeekdayHeader appears at the top of each grid column, and shows
// the day of the week that column represents.
// - CalendarDay appears within the grid for each day of the current month.
//
// CalendarDay tags:
// - [weekend] The day is a weekend.
// - [weekday] The day is a weekday.
type Calendar struct {
box tomo.ContainerBox
grid tomo.ContainerBox
time time.Time
monthLabel *Label
on struct {
valueChange event.FuncBroadcaster
}
}
// NewCalendar creates a new calendar with the specified date.
func NewCalendar (tm time.Time) *Calendar {
calendar := &Calendar {
box: tomo.NewContainerBox(),
time: tm,
}
calendar.box.SetRole(tomo.R("objects", "Calendar"))
calendar.box.SetAttr(tomo.ALayout(layouts.ContractVertical))
prevButton := NewButton("")
prevButton.SetIcon(tomo.IconGoPrevious)
prevButton.OnClick(func () {
calendar.prevMonth()
calendar.on.valueChange.Broadcast()
})
nextButton := NewButton("")
nextButton.SetIcon(tomo.IconGoNext)
nextButton.OnClick(func () {
calendar.nextMonth()
calendar.on.valueChange.Broadcast()
})
calendar.monthLabel = NewLabel("")
calendar.monthLabel.SetAlign(tomo.AlignMiddle, tomo.AlignMiddle)
calendar.grid = tomo.NewContainerBox()
calendar.grid.SetRole(tomo.R("objects", "CalendarGrid"))
calendar.grid.SetAttr(tomo.ALayout(layouts.NewGrid (
true, true, true, true, true, true, true)()))
calendar.box.Add(NewInnerContainer (
layouts.Row { false, true, false },
prevButton, calendar.monthLabel, nextButton))
calendar.box.Add(calendar.grid)
calendar.box.OnScroll(calendar.handleScroll)
calendar.refresh()
return calendar
}
// GetBox returns the underlying box.
func (this *Calendar) GetBox () tomo.Box {
return this.box
}
// Value returns the time this calendar is displaying.
func (this *Calendar) Value () time.Time {
return this.time
}
// SetValue sets the date the calendar will display.
func (this *Calendar) SetValue (tm time.Time) {
if this.time == tm { return }
this.time = tm
this.refresh()
}
// OnValueChange sets a function to be called when the user changes the date on
// the calendar.
func (this *Calendar) OnValueChange (callback func ()) {
this.on.valueChange.Connect(callback)
}
func (this *Calendar) prevMonth () {
this.time = firstOfMonth(this.time.Add(24 * time.Hour * -20))
this.refresh()
}
func (this *Calendar) nextMonth () {
this.time = firstOfMonth(this.time.Add(24 * time.Hour * 40))
this.refresh()
}
var weekdayAbbreviations = []string {
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
}
func (this *Calendar) refresh () {
this.monthLabel.SetText(this.time.Format("2006 January"))
this.grid.Clear()
for _, day := range weekdayAbbreviations {
dayLabel := tomo.NewTextBox()
dayLabel.SetRole(tomo.R("objects", "CalendarWeekdayHeader"))
dayLabel.SetText(day)
dayLabel.SetAttr(tomo.AAlign(tomo.AlignMiddle, tomo.AlignMiddle))
this.grid.Add(dayLabel)
}
dayIter := 0 - int(firstOfMonth(this.time).Weekday())
if dayIter <= -6 {
dayIter = 1
}
weekday := 0
totalDays := daysInMonth(this.time)
for ; dayIter <= totalDays; dayIter ++ {
weekday = (weekday + 1) % 7
if dayIter > 0 {
day := tomo.NewTextBox()
day.SetText(fmt.Sprint(dayIter))
if weekday == 1 || weekday == 0 {
day.SetRole(tomo.R("objects", "CalendarDay"))
day.SetTag("weekend", true)
} else {
day.SetRole(tomo.R("objects", "CalendarDay"))
day.SetTag("weekday", true)
}
this.grid.Add(day)
} else {
this.grid.Add(tomo.NewBox())
}
}
}
func (this *Calendar) handleScroll (x, y float64) bool {
if y < 0 {
this.prevMonth()
} else {
this.nextMonth()
}
return true
}
func firstOfMonth (tm time.Time) time.Time {
return time.Date(tm.Year(), tm.Month(), 0, 0, 0, 0, 0, time.Local)
}
func daysInMonth (tm time.Time) (days int) {
year := tm.Year()
month := tm.Month()
switch month {
case 1: days = 31
case 2:
// betcha didn't know this about leap years
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
days = 29
} else {
days = 28
}
case 3: days = 31
case 4: days = 30
case 5: days = 31
case 6: days = 30
case 7: days = 31
case 8: days = 31
case 9: days = 30
case 10: days = 31
case 11: days = 30
case 12: days = 31
}
return
}
func canonMonth (tm time.Time) int {
return int(tm.Month() - 1) + tm.Year() * 12
}