WIP components (as built)
This commit is contained in:
102 files changed
+6538
-1
No files matched your search
@@ -0,0 +1,208 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/icons"
|
||||
"mujicaui/internal"
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// LabelManagerOptions configure a LabelManager.
|
||||
type LabelManagerOptions struct {
|
||||
Disabled bool
|
||||
Label string
|
||||
// Version changes when labels or selection change in place.
|
||||
Version int
|
||||
}
|
||||
|
||||
// labelManagerState is the label being edited and the new-label draft.
|
||||
type labelManagerState struct {
|
||||
editing string
|
||||
name string
|
||||
draft string
|
||||
problem string
|
||||
}
|
||||
|
||||
// LabelManager edits labels and stores chosen IDs in selected.
|
||||
func LabelManager(c *ui.Context, labels *[]TaskLabel, selected *[]string, opts LabelManagerOptions) ChoiceResult {
|
||||
k := Tokens(c)
|
||||
d := Density(c)
|
||||
label := opts.Label
|
||||
if label == "" {
|
||||
label = msg(c, "label_manager.label")
|
||||
}
|
||||
changed := false
|
||||
col := ui.Column(c).Gap(d.Unit() * 2).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled)
|
||||
st := ui.Local(col, "label-manager", func() *labelManagerState { return &labelManagerState{} })
|
||||
s := *st
|
||||
cache := ui.Local(col, "label-data", func() labelDataCache { return labelDataCache{} })
|
||||
var first *TaskLabel
|
||||
var selectedFirst *string
|
||||
if len(*labels) > 0 {
|
||||
first = &(*labels)[0]
|
||||
}
|
||||
if len(*selected) > 0 {
|
||||
selectedFirst = &(*selected)[0]
|
||||
}
|
||||
if !cache.ready || cache.first != first || cache.n != len(*labels) || cache.selectedFirst != selectedFirst || cache.selectedN != len(*selected) || cache.version != opts.Version {
|
||||
*cache = labelDataCache{first: first, n: len(*labels), selectedFirst: selectedFirst, selectedN: len(*selected), version: opts.Version, ready: true, selected: map[string]bool{}}
|
||||
known := map[string]bool{}
|
||||
for _, l := range *labels {
|
||||
eventColor(k, l.Color)
|
||||
if l.ID == "" || known[l.ID] {
|
||||
panic("mujica: LabelManager needs unique non-empty IDs")
|
||||
}
|
||||
known[l.ID] = true
|
||||
}
|
||||
for _, id := range *selected {
|
||||
if !known[id] {
|
||||
panic("mujica: LabelManager selection contains an unknown ID")
|
||||
}
|
||||
cache.selected[id] = true
|
||||
}
|
||||
}
|
||||
state := ui.Local(col, "label-rows", func() ui.ListState { return ui.ListState{} })
|
||||
state.Key = func(i int) any { return (*labels)[i].ID }
|
||||
|
||||
live := func() bool { return !col.IsDisabled() }
|
||||
taken := func(name, except string) bool {
|
||||
return slices.ContainsFunc(*labels, func(l TaskLabel) bool { return l.ID != except && strings.EqualFold(l.Name, name) })
|
||||
}
|
||||
col.Children(func() {
|
||||
removed := -1
|
||||
ui.List(c, state, len(*labels), func(i int) {
|
||||
l := &(*labels)[i]
|
||||
ui.Row(c).Key(l.ID).MinHeight(d.ControlHeight()).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() {
|
||||
on := cache.selected[l.ID]
|
||||
box := ui.CheckboxBase(c, &on).Size(theme.MinHitSize, theme.MinHitSize).Center().Label(l.Name)
|
||||
internal.Ring(box, k.Focus, theme.ControlRadius)
|
||||
if box.Draw(func(p *ui.Painter, r ui.Rect) { labelSwatch(p, r, eventColor(k, l.Color), on, k) }).Changed() && live() {
|
||||
if on {
|
||||
*selected = append(*selected, l.ID)
|
||||
} else {
|
||||
*selected = slices.DeleteFunc(*selected, func(id string) bool { return id == l.ID })
|
||||
}
|
||||
changed = true
|
||||
slog.Debug("mujica: label_manager select", "id", l.ID, "selected", on)
|
||||
}
|
||||
if s.editing == l.ID && live() {
|
||||
labelEditRow(c, s, l, taken, &changed)
|
||||
return
|
||||
}
|
||||
ui.Text(c, l.Name).Grow(1).TextColor(k.Text).SingleLine().Ellipsis("…")
|
||||
if b := iconAction(c, icons.Must("palette"), fmt.Sprintf(msg(c, "label_manager.edit"), l.Name)); b.Clicked() && live() {
|
||||
s.editing, s.name, s.problem = l.ID, l.Name, ""
|
||||
}
|
||||
if b := iconAction(c, icons.Must("trash-2"), fmt.Sprintf(msg(c, "label_manager.delete"), l.Name)); b.Clicked() && live() {
|
||||
removed = i
|
||||
}
|
||||
})
|
||||
}).Height(min(float32(len(*labels)), 8) * d.ControlHeight())
|
||||
if removed >= 0 {
|
||||
id := (*labels)[removed].ID
|
||||
slog.Debug("mujica: label_manager delete", "id", id)
|
||||
*labels = slices.Delete(*labels, removed, removed+1)
|
||||
*selected = slices.DeleteFunc(*selected, func(x string) bool { return x == id })
|
||||
changed = true
|
||||
}
|
||||
if len(*labels) == 0 {
|
||||
ui.Text(c, msg(c, "label_manager.empty")).TextColor(k.TextMuted)
|
||||
}
|
||||
in := TextInput(c, &s.draft, TextInputOptions{Placeholder: msg(c, "label_manager.new"), Label: msg(c, "label_manager.new"),
|
||||
Disabled: opts.Disabled, Error: s.problem})
|
||||
if in.Submitted() && live() {
|
||||
name := strings.TrimSpace(s.draft)
|
||||
switch {
|
||||
case name == "":
|
||||
case taken(name, ""):
|
||||
s.problem = fmt.Sprintf(msg(c, "label_manager.taken"), name)
|
||||
default:
|
||||
l := TaskLabel{ID: labelNewID(*labels), Name: name, Color: EventColor(len(*labels) % 6)}
|
||||
*labels = append(*labels, l)
|
||||
s.draft, s.problem, changed = "", "", true
|
||||
slog.Debug("mujica: label_manager create", "id", l.ID)
|
||||
}
|
||||
}
|
||||
})
|
||||
if changed {
|
||||
cache.ready = false
|
||||
c.Invalidate()
|
||||
}
|
||||
return ChoiceResult{Element: col, changed: changed}
|
||||
}
|
||||
|
||||
// labelEditRow renames a label and picks its color.
|
||||
func labelEditRow(c *ui.Context, s *labelManagerState, l *TaskLabel, taken func(string, string) bool, changed *bool) {
|
||||
k := Tokens(c)
|
||||
d := Density(c)
|
||||
ui.Column(c).Grow(1).Gap(d.Unit()).Children(func() {
|
||||
in := TextInput(c, &s.name, TextInputOptions{Label: msg(c, "label_manager.name"), Error: s.problem})
|
||||
in.Input.AutoFocus()
|
||||
ui.Row(c).Gap(d.Unit()).Role(ui.RoleRadioGroup).Label(msg(c, "label_manager.color")).Children(func() {
|
||||
for i := range 6 {
|
||||
col := EventColor(i)
|
||||
sel := l.Color
|
||||
b := ui.RadioBase(c, &sel, col).Size(theme.MinHitSize, theme.MinHitSize).Label(msg(c, labelColorKeys[i]))
|
||||
internal.Ring(b, k.Focus, theme.ControlRadius)
|
||||
on := l.Color == col
|
||||
b.Draw(func(p *ui.Painter, r ui.Rect) { labelSwatch(p, r, eventColor(k, col), on, k) })
|
||||
if b.Changed() && !b.IsDisabled() {
|
||||
l.Color, *changed = col, true
|
||||
slog.Debug("mujica: label_manager color", "id", l.ID, "color", col)
|
||||
}
|
||||
}
|
||||
})
|
||||
done := in.Submitted() || in.Input.Shortcut(0, ui.KeyEscape)
|
||||
if in.Submitted() {
|
||||
name := strings.TrimSpace(s.name)
|
||||
switch {
|
||||
case name == "":
|
||||
done = false
|
||||
case taken(name, l.ID):
|
||||
s.problem, done = fmt.Sprintf(msg(c, "label_manager.taken"), name), false
|
||||
case name != l.Name:
|
||||
l.Name, *changed = name, true
|
||||
slog.Debug("mujica: label_manager rename", "id", l.ID)
|
||||
}
|
||||
}
|
||||
if done {
|
||||
s.editing, s.problem = "", ""
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var labelColorKeys = [...]string{"label_manager.accent", "label_manager.info", "label_manager.success", "label_manager.warning",
|
||||
"label_manager.danger", "label_manager.neutral"}
|
||||
|
||||
// labelSwatch draws a color dot with a selection check.
|
||||
func labelSwatch(p *ui.Painter, r ui.Rect, col ui.Color, on bool, k theme.Tokens) {
|
||||
box := ui.Rect{X: r.X + r.W/2 - 8, Y: r.Y + r.H/2 - 8, W: 16, H: 16}
|
||||
p.Fill(box, col, 8)
|
||||
if on {
|
||||
internal.Mark(p, ui.Rect{X: box.X + 2, Y: box.Y + 2, W: 12, H: 12}, "check", k.Surface)
|
||||
}
|
||||
}
|
||||
|
||||
// labelNewID returns an ID no label uses.
|
||||
func labelNewID(labels []TaskLabel) string {
|
||||
for n := len(labels) + 1; ; n++ {
|
||||
id := fmt.Sprintf("label-%d", n)
|
||||
if !slices.ContainsFunc(labels, func(l TaskLabel) bool { return l.ID == id }) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type labelDataCache struct {
|
||||
first *TaskLabel
|
||||
selectedFirst *string
|
||||
n, selectedN, version int
|
||||
ready bool
|
||||
selected map[string]bool
|
||||
}
|
||||
Reference in new issue
Block a user