Files
MujicaUI/datetime/year_view.go
T

170 lines
5.1 KiB
Go

package datetime
import (
"fmt"
"strconv"
"time"
"github.com/egoist/mygo/ui"
"github.com/ZacharyZhang-NY/MujicaUI/core"
"github.com/ZacharyZhang-NY/MujicaUI/internal"
"github.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
}