package project import ( "fmt" "log/slog" "math" "github.com/egoist/mygo/ui" "git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core" "git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/input" "git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal" "git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme" ) // StatusSelectOptions configure a StatusSelect. type StatusSelectOptions struct { Disabled bool Label string } var statusGroupKeys = [...]string{"status_select.backlog", "status_select.unstarted", "status_select.started", "status_select.completed", "status_select.canceled"} // StatusSelect chooses a status grouped by workflow stage. func StatusSelect(c *ui.Context, value *string, statuses []WorkflowStatus, opts StatusSelectOptions) core.ChoiceResult { if len(statuses) == 0 { panic("mujica: StatusSelect needs statuses") } for i, s := range statuses { if s.Group < StatusBacklog || s.Group > StatusCanceled || s.ID == "" { panic(fmt.Sprintf("mujica: StatusSelect status %+v is not valid", s)) } for _, o := range statuses[:i] { if o.ID == s.ID { panic(fmt.Sprintf("mujica: StatusSelect status %q repeats", s.ID)) } } } cur := projectStatus(statuses, *value) k := core.Tokens(c) old := *value pick := old var s ui.SelectParts[string] root := ui.Row(c).Shrink(0).Disabled(opts.Disabled) epoch := ui.Local(root, "status-epoch", func() struct { disabled bool generation int } { return struct { disabled bool generation int }{} }) if disabled := root.IsDisabled(); disabled != epoch.disabled { epoch.disabled = disabled epoch.generation++ } root.Children(func() { ui.Row(c.Key(epoch.generation)).Children(func() { s = ui.SelectBase(c, &pick) }) }) tr := s.Trigger label := opts.Label if label == "" { label = core.Msg(c, "status_select.label") } tr.Label(label).Description(cur.Name) input.SelectField(c, tr, s.Open(), func() { statusGlyphBox(c, cur.Group) ui.Text(c, cur.Name).SingleLine().Grow(1).Shrink(1).TextColor(k.Text) }) if !tr.IsDisabled() { s.Popup(func(panel ui.Element) { input.SelectPanel(c, panel) _, wh := c.Size() ui.Scroll(c).MaxHeight(min(320, wh/2)).Children(func() { for g := StatusBacklog; g <= StatusCanceled; g++ { first := true for _, st := range statuses { if st.Group != g { continue } if first { ui.Text(c, core.Msg(c, statusGroupKeys[g])).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted). Padding(core.Density(c).Unit(), core.Density(c).Unit()*2).Role(ui.RoleColumnHeader) first = false } it := s.Item(st.ID) input.SelectRow(c, it, it.Highlighted(), st.ID == *value, "") it.Label(st.Name).Children(func() { statusGlyphBox(c, st.Group) ui.Text(c, st.Name).SingleLine() }) } } }) }) } if pick != *value && !tr.IsDisabled() { *value = pick } changed := *value != old if changed { slog.Debug("mujica: status_select change", "from", old, "to", *value) } return core.NewChoiceResult(tr, changed) } // statusGlyphBox reserves 14 DIPs for a status glyph. func statusGlyphBox(c *ui.Context, g StatusGroup) { k := core.Tokens(c) ui.Box(c).Size(14, 14).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { statusGlyph(p, r, g, k) }) } // statusGlyph draws a distinct shape for each workflow stage. func statusGlyph(p *ui.Painter, r ui.Rect, g StatusGroup, k theme.Tokens) { cx, cy, rad := r.X+r.W/2, r.Y+r.H/2, r.W/2-1 ring := func(col ui.Color) { var path ui.Path internal.Arc(&path, cx, cy, rad, 0, 2*math.Pi, true) p.StrokePath(&path, 1.5, col) } switch g { case StatusBacklog: for i := 0; i < 8; i += 2 { var path ui.Path internal.Arc(&path, cx, cy, rad, float64(i)*math.Pi/4, float64(i+1)*math.Pi/4, true) p.StrokePath(&path, 1.5, k.TextMuted) } case StatusUnstarted: ring(k.TextMuted) case StatusStarted: ring(k.Warning) p.FillPath(internal.Sector(cx, cy, 0, rad-2, 0, math.Pi), k.Warning) case StatusCompleted: p.FillPath(internal.Sector(cx, cy, 0, rad+1, 0, 2*math.Pi), k.Success) internal.Mark(p, ui.Rect{X: r.X + 3, Y: r.Y + 3, W: r.W - 6, H: r.H - 6}, "check", k.Surface) case StatusCanceled: p.FillPath(internal.Sector(cx, cy, 0, rad+1, 0, 2*math.Pi), k.TextMuted) internal.Mark(p, ui.Rect{X: r.X + 3, Y: r.Y + 3, W: r.W - 6, H: r.H - 6}, "x", k.Surface) } }