170 lines
5.2 KiB
Go
170 lines
5.2 KiB
Go
package datetime
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
|
|
)
|
|
|
|
// YearViewOptions configure a YearView.
|
|
type YearViewOptions struct {
|
|
// Location is the zone days are cut in; required.
|
|
Location *time.Location
|
|
// Version must change when events change in place.
|
|
Version int
|
|
WeekStart WeekStart
|
|
// Today is marked; zero uses the system clock.
|
|
Today core.Date
|
|
Disabled bool
|
|
Label string
|
|
}
|
|
|
|
// YearView shows twelve months with busy-day dots.
|
|
func YearView(c *ui.Context, year *int, events []CalendarEvent, opts YearViewOptions) CalendarViewResult {
|
|
loc := eventLocation("YearView", opts.Location)
|
|
if *year < 1 {
|
|
panic(fmt.Sprintf("mujica: YearView year %d is not valid", *year))
|
|
}
|
|
eventsCheck("YearView", events)
|
|
d := core.Density(c)
|
|
today := core.DatetimeToday(c, opts.Today)
|
|
first := core.Date{Year: *year, Month: time.January, Day: 1}
|
|
label := opts.Label
|
|
if label == "" {
|
|
label = strconv.Itoa(*year)
|
|
}
|
|
r := CalendarViewResult{}
|
|
root := ui.Column(c).Gap(d.Unit() * 3).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled)
|
|
r.Element = root
|
|
counts := eventsDerived(root, "year-view-counts", events, opts.Version, eventDayStart(first, loc), eventDayStart(first.AddMonths(12), loc),
|
|
func(hits []int) map[core.Date]int {
|
|
m := map[core.Date]int{}
|
|
for _, h := range hits {
|
|
ev := events[h]
|
|
for day := core.DateOf(ev.Start.In(loc)); eventDayStart(day, loc).Before(ev.End); day = day.AddDays(1) {
|
|
m[day]++
|
|
}
|
|
}
|
|
return m
|
|
})
|
|
cursor := ui.Local(root, "year-view-cursor", func() core.Date { return core.Date{} })
|
|
if cursor.Year != *year {
|
|
*cursor = core.Date{Year: *year, Month: today.Month, Day: min(today.Day, 28)}
|
|
}
|
|
root.Children(func() {
|
|
calendarViewHeader(c, strconv.Itoa(*year), core.Msg(c, "calendar.prev_year"), core.Msg(c, "calendar.next_year"), func(dir int) {
|
|
*year += dir
|
|
})
|
|
grid := ui.Row(c).Wrap().AlignItems(ui.Start).Gap(d.Unit() * 4).FocusGroup(ui.Horizontal | ui.Vertical)
|
|
moved := false
|
|
if !grid.IsDisabled() {
|
|
for key, days := range map[ui.Key]int{ui.KeyLeft: -1, ui.KeyRight: 1, ui.KeyUp: -7, ui.KeyDown: 7} {
|
|
if grid.Shortcut(0, key) {
|
|
*cursor, moved = cursor.AddDays(days), true
|
|
}
|
|
}
|
|
for key, months := range map[ui.Key]int{ui.KeyPageUp: -1, ui.KeyPageDown: 1} {
|
|
if grid.Shortcut(0, key) {
|
|
*cursor, moved = cursor.AddMonths(months), true
|
|
}
|
|
}
|
|
if moved && cursor.Year != *year {
|
|
*year = cursor.Year
|
|
}
|
|
}
|
|
grid.Children(func() {
|
|
for m := time.January; m <= time.December; m++ {
|
|
if day, ok := yearMonth(c, *year, m, counts, today, *cursor, moved, opts.WeekStart); ok && !grid.IsDisabled() {
|
|
r.day, *cursor = day, day
|
|
}
|
|
}
|
|
})
|
|
})
|
|
return r
|
|
}
|
|
|
|
// yearMonth draws one small month and reports a day clicked.
|
|
func yearMonth(c *ui.Context, year int, m time.Month, counts map[core.Date]int, today, cursor core.Date, focus bool, ws WeekStart) (core.Date, bool) {
|
|
k := core.Tokens(c)
|
|
cell := theme.MinHitSize
|
|
first := CalendarFirstWeekday(c, ws)
|
|
start := core.Date{Year: year, Month: m, Day: 1}
|
|
lead := (int(start.Weekday()) - int(first) + 7) % 7
|
|
var picked core.Date
|
|
ui.Column(c).Width(cell * 7).Gap(2).Children(func() {
|
|
ui.Text(c, core.DatetimeMonthName(c, m)).Font(theme.SerifFont).TextColor(k.Text).Bold()
|
|
ui.Row(c).Children(func() {
|
|
for i := range 7 {
|
|
wd := (int(first) + i) % 7
|
|
ui.Text(c, core.Msg(c, "calendar.weekday_"+strconv.Itoa(wd))).Width(cell).TextAlign(ui.Center).
|
|
FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted)
|
|
}
|
|
})
|
|
last := start.AddMonths(1).AddDays(-1).Day
|
|
for w := 0; w*7 < lead+last; w++ {
|
|
ui.Row(c).Children(func() {
|
|
for i := range 7 {
|
|
n := w*7 + i - lead + 1
|
|
if n < 1 || n > last {
|
|
ui.Box(c).Size(cell, cell)
|
|
continue
|
|
}
|
|
day := core.Date{Year: year, Month: m, Day: n}
|
|
busy := counts[day]
|
|
text := core.DatetimeDayLabel(c, day)
|
|
if busy > 0 {
|
|
text += ", " + core.CalendarCount(c, "year_view.events", busy)
|
|
}
|
|
b := ui.ButtonBase(c).Size(cell, cell).Radius(theme.ControlRadius).Label(text).Column()
|
|
internal.Ring(b, k.Focus, theme.ControlRadius)
|
|
if day == cursor && focus {
|
|
b.Focus()
|
|
}
|
|
switch {
|
|
case day == today:
|
|
b.Background(k.Selection)
|
|
case b.Hovered():
|
|
b.Background(k.SurfaceHover)
|
|
}
|
|
dots := yearDots(busy)
|
|
b.Draw(func(p *ui.Painter, r ui.Rect) {
|
|
x0 := r.X + r.W/2 - float32(dots*5-2)/2
|
|
for j := range dots {
|
|
p.Fill(ui.Rect{X: x0 + float32(j)*5, Y: r.Y + r.H - 6, W: 3, H: 3}, k.AccentText, 1.5)
|
|
}
|
|
})
|
|
b.Children(func() {
|
|
t := ui.Text(c, strconv.Itoa(n)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Text).FontFeatures("tnum")
|
|
if day == today {
|
|
t.Bold().TextColor(k.AccentText)
|
|
}
|
|
})
|
|
if b.Clicked() {
|
|
picked = day
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
return picked, !picked.IsZero()
|
|
}
|
|
|
|
// yearDots maps an event count to one to three dots.
|
|
func yearDots(n int) int {
|
|
switch {
|
|
case n == 0:
|
|
return 0
|
|
case n <= 2:
|
|
return 1
|
|
case n <= 5:
|
|
return 2
|
|
}
|
|
return 3
|
|
}
|