datetimeIconButton moves to icon_button.go as iconAction(c, icon, label) and replaces the identical copies in Dialog close, PasswordInput toggle and AppShell collapse. Hover and press now set Text on every caller, including the date and time pickers, which kept TextMuted on hover. Image retry (bordered, danger ink), TagsInput remove (14 DIP icon, no press state) and Tag close (12 DIP icon, fade) are different buttons and stay as they are.
270 lines
7.8 KiB
Go
270 lines
7.8 KiB
Go
package mujica
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/icons"
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// WeekStart is a month's first column; zero follows the locale.
|
|
type WeekStart int
|
|
|
|
const (
|
|
WeekStartLocale WeekStart = iota
|
|
WeekStartSunday
|
|
WeekStartMonday
|
|
WeekStartSaturday
|
|
)
|
|
|
|
// CalendarOptions configure a Calendar.
|
|
type CalendarOptions struct {
|
|
// Min and Max bound choosable days; zero is open.
|
|
Min, Max Date
|
|
// DisableDate marks single days that cannot be chosen.
|
|
DisableDate func(Date) bool
|
|
WeekStart WeekStart
|
|
// Today is marked; zero uses the system clock.
|
|
Today Date
|
|
Disabled bool
|
|
// Label names the calendar for assistive technology.
|
|
Label string
|
|
}
|
|
|
|
// Calendar shows one month and chooses a day into *value.
|
|
func Calendar(c *ui.Context, value *Date, opts CalendarOptions) DateTimeResult {
|
|
datetimeCheckDate("Calendar value", *value)
|
|
datetimeCheckBounds("Calendar", opts.Min, opts.Max)
|
|
box := ui.Column(c)
|
|
cursor := ui.Local(box, "calendar-cursor", func() Date { return Date{} })
|
|
seen := ui.Local(box, "calendar-seen", func() Date { return Date{} })
|
|
today := datetimeToday(c, opts.Today)
|
|
if *seen != *value || cursor.IsZero() {
|
|
*seen = *value
|
|
*cursor = *value
|
|
if cursor.IsZero() {
|
|
*cursor = today
|
|
}
|
|
}
|
|
changed := false
|
|
box.Disabled(opts.Disabled).Children(func() {
|
|
picked, _ := calendarMonth(c, cursor, calendarConfig{
|
|
chosen: func(d Date) bool { return d == *value },
|
|
enabled: calendarEnabled(opts.Min, opts.Max, opts.DisableDate),
|
|
today: today,
|
|
weekStart: opts.WeekStart,
|
|
label: opts.Label,
|
|
})
|
|
if !picked.IsZero() && picked != *value {
|
|
slog.Debug("mujica: calendar change", "from", *value, "to", picked)
|
|
*value, *seen = picked, picked
|
|
changed = true
|
|
}
|
|
})
|
|
return DateTimeResult{Element: box, changed: changed}
|
|
}
|
|
|
|
// calendarConfig is how calendarMonth marks and limits its days.
|
|
type calendarConfig struct {
|
|
chosen func(Date) bool
|
|
inRange func(Date) bool
|
|
enabled func(Date) bool
|
|
today Date
|
|
weekStart WeekStart
|
|
autoFocus bool
|
|
label string
|
|
}
|
|
|
|
// calendarEnabled combines bounds and a day filter.
|
|
func calendarEnabled(lo, hi Date, disable func(Date) bool) func(Date) bool {
|
|
return func(d Date) bool { return d.within(lo, hi) && (disable == nil || !disable(d)) }
|
|
}
|
|
|
|
// calendarFirstWeekday resolves the first column for the window's locale.
|
|
func calendarFirstWeekday(c *ui.Context, w WeekStart) time.Weekday {
|
|
switch w {
|
|
case WeekStartSunday:
|
|
return time.Sunday
|
|
case WeekStartMonday:
|
|
return time.Monday
|
|
case WeekStartSaturday:
|
|
return time.Saturday
|
|
case WeekStartLocale:
|
|
if CurrentLocale(c) == ZhCN {
|
|
return time.Monday
|
|
}
|
|
return time.Sunday
|
|
}
|
|
panic(fmt.Sprintf("mujica: unknown WeekStart %d", w))
|
|
}
|
|
|
|
// calendarSeek returns the first enabled day from d stepping by dir, or d.
|
|
func calendarSeek(d Date, dir int, enabled func(Date) bool) Date {
|
|
for i, x := 0, d; i < 31; i, x = i+1, x.AddDays(dir) {
|
|
if enabled(x) {
|
|
return x
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
|
|
// calendarMonth draws *cursor's month; returns picked and hovered days.
|
|
func calendarMonth(c *ui.Context, cursor *Date, cfg calendarConfig) (picked, hovered Date) {
|
|
k := Tokens(c)
|
|
d := Density(c)
|
|
cur := *cursor
|
|
title := datetimeMonthTitle(c, cur.Year, cur.Month)
|
|
label := cfg.label
|
|
if label == "" {
|
|
label = msg(c, "calendar.label")
|
|
}
|
|
cell := d.ControlHeight()
|
|
root := ui.Column(c).Gap(d.Unit() * 2).Shrink(0).Width(cell * 7).Role(ui.RoleGroup).Label(label)
|
|
keyed := ui.Local(root, "calendar-keys", func() bool { return false })
|
|
move := func(to Date, dir int, keys bool) {
|
|
*cursor = calendarSeek(to, dir, cfg.enabled)
|
|
*keyed = keys
|
|
}
|
|
root.Children(func() {
|
|
ui.Row(c).AlignItems(ui.Center).Gap(d.Unit()).Children(func() {
|
|
if iconAction(c, icons.Must("chevrons-left"), msg(c, "calendar.prev_year")).Clicked() {
|
|
move(cur.AddMonths(-12), -1, false)
|
|
}
|
|
if iconAction(c, icons.Must("chevron-left"), msg(c, "calendar.prev_month")).Clicked() {
|
|
move(cur.AddMonths(-1), -1, false)
|
|
}
|
|
ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.BodySize+2)).Bold().
|
|
TextColor(k.Text).Grow(1).TextAlign(ui.Center).SingleLine()
|
|
if iconAction(c, icons.Must("chevron-right"), msg(c, "calendar.next_month")).Clicked() {
|
|
move(cur.AddMonths(1), 1, false)
|
|
}
|
|
if iconAction(c, icons.Must("chevrons-right"), msg(c, "calendar.next_year")).Clicked() {
|
|
move(cur.AddMonths(12), 1, false)
|
|
}
|
|
})
|
|
first := calendarFirstWeekday(c, cfg.weekStart)
|
|
ui.Row(c).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).PaddingY(d.Unit()).Children(func() {
|
|
for i := range 7 {
|
|
wd := (int(first) + i) % 7
|
|
ui.Text(c, msg(c, "calendar.weekday_"+strconv.Itoa(wd))).Width(cell).TextAlign(ui.Center).
|
|
FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleColumnHeader).
|
|
Label(msg(c, "calendar.weekday_long_"+strconv.Itoa(wd)))
|
|
}
|
|
})
|
|
days := ui.Column(c).Role(ui.RoleTable).Label(title).FocusGroup(ui.Horizontal | ui.Vertical)
|
|
keysMoved := true
|
|
switch {
|
|
case days.Shortcut(0, ui.KeyLeft):
|
|
move(cur.AddDays(-1), -1, true)
|
|
case days.Shortcut(0, ui.KeyRight):
|
|
move(cur.AddDays(1), 1, true)
|
|
case days.Shortcut(0, ui.KeyUp):
|
|
move(cur.AddDays(-7), -1, true)
|
|
case days.Shortcut(0, ui.KeyDown):
|
|
move(cur.AddDays(7), 1, true)
|
|
case days.Shortcut(0, ui.KeyPageUp):
|
|
move(cur.AddMonths(-1), -1, true)
|
|
case days.Shortcut(0, ui.KeyPageDown):
|
|
move(cur.AddMonths(1), 1, true)
|
|
case days.Shortcut(ui.Shift, ui.KeyPageUp):
|
|
move(cur.AddMonths(-12), -1, true)
|
|
case days.Shortcut(ui.Shift, ui.KeyPageDown):
|
|
move(cur.AddMonths(12), 1, true)
|
|
case days.Shortcut(0, ui.KeyHome):
|
|
move(Date{cur.Year, cur.Month, 1}, 1, true)
|
|
case days.Shortcut(0, ui.KeyEnd):
|
|
move(Date{cur.Year, cur.Month, 1}.AddMonths(1).AddDays(-1), -1, true)
|
|
default:
|
|
keysMoved = false
|
|
}
|
|
cur = *cursor
|
|
start := Date{cur.Year, cur.Month, 1}
|
|
start = start.AddDays(-((int(start.Weekday()) - int(first) + 7) % 7))
|
|
days.Children(func() {
|
|
for w := range 6 {
|
|
ui.Row(c).Role(ui.RoleRow).Children(func() {
|
|
for i := range 7 {
|
|
day := start.AddDays(w*7 + i)
|
|
clicked, hover := calendarDay(c, day, cur, cfg, cell, keysMoved, *keyed)
|
|
if clicked {
|
|
picked = day
|
|
move(day, 1, false)
|
|
}
|
|
if hover {
|
|
hovered = day
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
})
|
|
return picked, hovered
|
|
}
|
|
|
|
// calendarDay draws one day cell and reports whether it was picked or hovered.
|
|
func calendarDay(c *ui.Context, day, cur Date, cfg calendarConfig, size float32, focus, keyed bool) (picked, hovered bool) {
|
|
k := Tokens(c)
|
|
enabled := cfg.enabled(day)
|
|
chosen := cfg.chosen(day)
|
|
sel := Date{}
|
|
if chosen {
|
|
sel = day
|
|
}
|
|
cell := ui.RadioBase(c, &sel, day).Size(size, size-4).Center().Radius(theme.ControlRadius).
|
|
Label(datetimeDayLabel(c, day)).Disabled(!enabled).FocusRing(false)
|
|
if day == cur && enabled {
|
|
if focus {
|
|
cell.Focus()
|
|
} else if cfg.autoFocus {
|
|
cell.AutoFocus()
|
|
}
|
|
}
|
|
band := cfg.inRange != nil && cfg.inRange(day)
|
|
fg := k.Text
|
|
if day.Month != cur.Month {
|
|
fg = k.TextMuted
|
|
}
|
|
switch {
|
|
case chosen:
|
|
cell.Background(k.Accent)
|
|
fg = k.OnAccent
|
|
if cell.Hovered() {
|
|
cell.Background(k.AccentHover)
|
|
}
|
|
case band:
|
|
cell.Background(k.Selection).Radius(0)
|
|
case cell.Pressed():
|
|
cell.Background(k.Selection)
|
|
case cell.Hovered():
|
|
cell.Background(k.SurfaceHover)
|
|
}
|
|
ring := cell.FocusVisible() || cell.Focused() && keyed
|
|
cell.DrawOver(func(p *ui.Painter, r ui.Rect) {
|
|
if chosen {
|
|
datetimeSelectedMark(p, r, k.OnAccent)
|
|
}
|
|
if day == cfg.today {
|
|
p.FillPath(internal.Diamond(r.X+r.W/2, r.Y+r.H-5, 5), fg)
|
|
}
|
|
if ring {
|
|
internal.FocusRing(p, r, theme.ControlRadius, k.Focus)
|
|
}
|
|
})
|
|
cell.Children(func() {
|
|
t := ui.Text(c, strconv.Itoa(day.Day)).TextColor(fg).FontFeatures("tnum")
|
|
if day == cfg.today {
|
|
t.Bold()
|
|
}
|
|
if !enabled {
|
|
t.Strikethrough()
|
|
}
|
|
})
|
|
return cell.Clicked() && enabled, cell.Hovered()
|
|
}
|