Calendars, date and time inputs, pickers, clocks, timers, a sun dial, a season wheel, an agenda and a day schedule. Kit.At rebuilds at a moment (relative times, midnight). Stepper and NumberInput arrows stop under Inert. DateInput parks a popup focus as its own.
87 lines
2.6 KiB
Go
87 lines
2.6 KiB
Go
package datetime
|
||
|
||
import (
|
||
"fmt"
|
||
"slices"
|
||
"time"
|
||
|
||
"github.com/egoist/mygo/ui"
|
||
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||
)
|
||
|
||
type DayScheduleOptions struct {
|
||
// From and To bound the hours; zeros mean 8–18.
|
||
From, To int
|
||
}
|
||
|
||
// hourH is an hour row's height; gutter holds labels.
|
||
const hourH, gutter = pixel.LineHeight + 4, 24
|
||
|
||
// DaySchedule places events on hour rows by start and length.
|
||
func DaySchedule(c *ui.Context, events []Event, o DayScheduleOptions) *ui.Element {
|
||
k := pixel.K(c)
|
||
from, to := o.From, o.To
|
||
if from == 0 && to == 0 {
|
||
from, to = 8, 18
|
||
}
|
||
if from < 0 || to > 24 || from >= to {
|
||
panic(fmt.Sprintf("datetime: DaySchedule hours %d to %d", from, to))
|
||
}
|
||
sorted := slices.Clone(events)
|
||
slices.SortStableFunc(sorted, func(a, b Event) int { return a.Start.Compare(b.Start) })
|
||
// Chains of overlapping events share the width in lanes.
|
||
lanes, shared := make([]int, len(sorted)), make([]int, len(sorted))
|
||
var ends []time.Time
|
||
var last time.Time
|
||
head := 0
|
||
for i, e := range sorted {
|
||
if i > 0 && e.Start.Compare(last) >= 0 {
|
||
for j := head; j < i; j++ {
|
||
shared[j] = len(ends)
|
||
}
|
||
head, ends = i, nil
|
||
}
|
||
lane := slices.IndexFunc(ends, func(end time.Time) bool { return end.Compare(e.Start) <= 0 })
|
||
if lane < 0 {
|
||
lane, ends = len(ends), append(ends, time.Time{})
|
||
}
|
||
lanes[i], ends[lane] = lane, e.Start.Add(e.Length)
|
||
if end := ends[lane]; end.Compare(last) > 0 || i == head {
|
||
last = end
|
||
}
|
||
}
|
||
for j := head; j < len(sorted); j++ {
|
||
shared[j] = len(ends)
|
||
}
|
||
box := ui.Box(c).FillWidth().Height(k.D((to - from) * hourH)).Role(ui.RoleList)
|
||
w, check := pixel.WrapWidth(c, box)
|
||
return box.Draw(func(p *ui.Painter, r ui.Rect) {
|
||
check(r)
|
||
g := k.Canvas(p, r)
|
||
for h := from; h < to; h++ {
|
||
y := (h - from) * hourH
|
||
g.Micro(0, y+1, fmt.Sprintf("%02d:00", h), pixel.Faint, 1)
|
||
g.Checker(gutter, y, g.W-gutter, 1, pixel.Edge)
|
||
}
|
||
}).Children(func() {
|
||
for i, e := range sorted {
|
||
// Minutes from the top row, cut to the hours shown.
|
||
top := e.Start.Hour()*60 + e.Start.Minute() - from*60
|
||
end := min(top+int(e.Length/time.Minute), (to-from)*60)
|
||
if top = max(0, top); end <= top {
|
||
continue
|
||
}
|
||
h := max(3, (end-top)*hourH/60)
|
||
y := min(top*hourH/60, (to-from)*hourH-h)
|
||
laneW := max(4, (w-gutter)/shared[i])
|
||
ui.Box(c).Absolute().Left(k.D(gutter+lanes[i]*laneW)).Top(k.D(y)).Size(k.D(laneW-1), k.D(h)).Role(ui.RoleListItem).
|
||
Label(e.Start.Format("15:04") + " " + e.Title).Draw(func(p *ui.Painter, r ui.Rect) {
|
||
g := k.Canvas(p, r)
|
||
g.Frame(0, 0, g.W, g.H, pixel.Raised, pixel.AccentLit)
|
||
g.Clip(1, 1, g.W-2, g.H-2, func() { g.Text(3, 0, e.Title, pixel.Ink, 1) })
|
||
})
|
||
}
|
||
})
|
||
}
|