diff --git a/calendar.go b/calendar.go new file mode 100644 index 0000000..818881b --- /dev/null +++ b/calendar.go @@ -0,0 +1,167 @@ +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" + +// Calendar is an object that can display a date and allow the user to change +// it. It can display one month at a time. +type Calendar struct { + tomo.ContainerBox + + grid tomo.ContainerBox + time time.Time + monthLabel *Label + + on struct { + edit event.FuncBroadcaster + } +} + +// NewCalendar creates a new calendar with the specified date. +func NewCalendar (tm time.Time) *Calendar { + calendar := &Calendar { + ContainerBox: tomo.NewContainerBox(), + time: tm, + } + calendar.SetRole(tomo.R("objects", "Calendar", "")) + calendar.SetLayout(layouts.ContractVertical) + + prevButton := NewButton("") + prevButton.SetIcon(tomo.IconGoPrevious) + prevButton.OnClick(func () { + calendar.prevMonth() + calendar.on.edit.Broadcast() + }) + nextButton := NewButton("") + nextButton.SetIcon(tomo.IconGoNext) + nextButton.OnClick(func () { + calendar.nextMonth() + calendar.on.edit.Broadcast() + }) + calendar.monthLabel = NewLabel("") + calendar.monthLabel.SetAlign(tomo.AlignMiddle, tomo.AlignMiddle) + + calendar.grid = tomo.NewContainerBox() + calendar.grid.SetRole(tomo.R("objects", "CalendarGrid", "")) + calendar.grid.SetLayout(layouts.NewGrid ( + []bool { true, true, true, true, true, true, true }, + []bool { })) + calendar.Add(NewInnerContainer ( + layouts.Row { false, true, false }, + prevButton, calendar.monthLabel, nextButton)) + calendar.Add(calendar.grid) + + calendar.OnScroll(calendar.handleScroll) + calendar.CaptureScroll(true) + + calendar.refresh() + return calendar +} + +// SetTime sets the date the calendar will display. +func (this *Calendar) SetTime (tm time.Time) { + if this.time == tm { return } + this.time = tm + this.refresh() +} + +// OnEdit sets a function to be called when the user changes the date on the +// calendar. +func (this *Calendar) OnEdit (callback func ()) { + this.on.edit.Connect(callback) +} + +func (this *Calendar) prevMonth () { + this.time = firstOfMonth(this.time.Add(24 * time.Hour * 40)) + this.refresh() +} + +func (this *Calendar) nextMonth () { + this.time = firstOfMonth(this.time.Add(24 * time.Hour * -20)) + 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.SetAlign(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", "weekend")) + } else { + day.SetRole(tomo.R("objects", "CalendarDay", "weekday")) + } + this.grid.Add(day) + } else { + this.grid.Add(tomo.NewBox()) + } + } +} + +func (this *Calendar) handleScroll (x, y float64) { + if y < 0 { + this.nextMonth() + } else { + this.prevMonth() + } +} + +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 +}