WIP components (as built)
This commit is contained in:
102 files changed
+6538
-1
No files matched your search
+158
@@ -0,0 +1,158 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/icons"
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// SubtaskListOptions configure a SubtaskList.
|
||||
type SubtaskListOptions struct {
|
||||
Disabled bool
|
||||
Label string
|
||||
// Version changes when subtasks change in place.
|
||||
Version int
|
||||
}
|
||||
|
||||
// subtaskDrag is the ID of a subtask being dragged.
|
||||
type subtaskDrag string
|
||||
|
||||
// SubtaskList edits and reorders subtasks with completion progress.
|
||||
func SubtaskList(c *ui.Context, subs *[]Subtask, opts SubtaskListOptions) ChoiceResult {
|
||||
k := Tokens(c)
|
||||
d := Density(c)
|
||||
label := opts.Label
|
||||
if label == "" {
|
||||
label = msg(c, "subtask_list.label")
|
||||
}
|
||||
changed := false
|
||||
col := ui.Column(c).Gap(d.Unit() * 2).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled)
|
||||
draft := ui.Local(col, "subtask-draft", func() string { return "" })
|
||||
cache := ui.Local(col, "subtask-summary", func() subtaskCache { return subtaskCache{} })
|
||||
var first *Subtask
|
||||
if len(*subs) > 0 {
|
||||
first = &(*subs)[0]
|
||||
}
|
||||
if !cache.ready || cache.first != first || cache.n != len(*subs) || cache.version != opts.Version {
|
||||
*cache = subtaskCache{first: first, n: len(*subs), version: opts.Version, ready: true}
|
||||
seen := map[string]bool{}
|
||||
for _, s := range *subs {
|
||||
if s.ID == "" || seen[s.ID] {
|
||||
panic(fmt.Sprintf("mujica: SubtaskList subtask %q is empty or repeated", s.ID))
|
||||
}
|
||||
seen[s.ID] = true
|
||||
if s.Done {
|
||||
cache.done++
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
state := ui.Local(col, "subtask-rows", func() ui.ListState { return ui.ListState{} })
|
||||
state.Key = func(i int) any { return (*subs)[i].ID }
|
||||
var pending func()
|
||||
|
||||
move := func(from, to int, how string) {
|
||||
if col.IsDisabled() || to < 0 || to >= len(*subs) || from == to {
|
||||
return
|
||||
}
|
||||
|
||||
if pending == nil {
|
||||
pending = func() {
|
||||
step := (*subs)[from]
|
||||
*subs = slices.Insert(slices.Delete(*subs, from, from+1), to, step)
|
||||
changed = true
|
||||
state.ScrollIntoView(to)
|
||||
slog.Debug("mujica: subtask_list move", "id", step.ID, "to", to, "by", how)
|
||||
}
|
||||
}
|
||||
}
|
||||
col.Children(func() {
|
||||
done := cache.done
|
||||
ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() {
|
||||
frac := 0.0
|
||||
if len(*subs) > 0 {
|
||||
frac = float64(done) / float64(len(*subs))
|
||||
}
|
||||
ui.Box(c).Grow(1).Children(func() { projectBar(c, frac, label) })
|
||||
ui.Text(c, fmt.Sprintf(msg(c, "subtask_list.progress"), done, len(*subs))).FontSize(FontSize(c, theme.CaptionSize)).
|
||||
TextColor(k.TextMuted).FontFeatures("tnum")
|
||||
})
|
||||
removed := -1
|
||||
ui.List(c, state, len(*subs), func(i int) {
|
||||
s := &(*subs)[i]
|
||||
row := ui.Row(c).Key(s.ID).MinHeight(d.ControlHeight()).Gap(d.Unit() * 2).AlignItems(ui.Center).Role(ui.RoleListItem)
|
||||
if id, ok := ui.Drop[subtaskDrag](row); ok {
|
||||
if from := slices.IndexFunc(*subs, func(o Subtask) bool { return o.ID == string(id) }); from >= 0 {
|
||||
move(from, i, "drag")
|
||||
}
|
||||
}
|
||||
if _, over := ui.DragOver[subtaskDrag](row); over {
|
||||
row.Background(k.Selection)
|
||||
}
|
||||
row.Children(func() {
|
||||
grip := ui.Box(c).Size(theme.MinHitSize, theme.MinHitSize).Center().Cursor(ui.CursorGrab).Label(fmt.Sprintf(msg(c, "subtask_list.drag"), s.Title))
|
||||
if !grip.IsDisabled() {
|
||||
grip.Drag(subtaskDrag(s.ID))
|
||||
}
|
||||
grip.Children(func() { ui.Icon(c, icons.Must("grip-vertical")).Size(14, 14).TextColor(k.TextMuted) })
|
||||
v := s.Done
|
||||
cb := Checkbox(c, &v, s.Title, CheckboxOptions{Disabled: opts.Disabled})
|
||||
if cb.Changed() && !cb.IsDisabled() {
|
||||
s.Done, changed = v, true
|
||||
}
|
||||
if !cb.IsDisabled() {
|
||||
if cb.Shortcut(ui.Alt, ui.KeyUp) {
|
||||
move(i, i-1, "keys")
|
||||
}
|
||||
if cb.Shortcut(ui.Alt, ui.KeyDown) {
|
||||
move(i, i+1, "keys")
|
||||
}
|
||||
}
|
||||
ui.Spacer(c)
|
||||
if b := iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "subtask_list.remove"), s.Title)); b.Clicked() && !b.IsDisabled() {
|
||||
removed = i
|
||||
}
|
||||
})
|
||||
}).Height(min(float32(len(*subs)), 8) * d.ControlHeight())
|
||||
if removed >= 0 && !col.IsDisabled() {
|
||||
slog.Debug("mujica: subtask_list remove", "id", (*subs)[removed].ID)
|
||||
*subs, changed = slices.Delete(*subs, removed, removed+1), true
|
||||
}
|
||||
in := TextInput(c, draft, TextInputOptions{Placeholder: msg(c, "subtask_list.add"), Label: msg(c, "subtask_list.add"), Disabled: opts.Disabled})
|
||||
if in.Submitted() && strings.TrimSpace(*draft) != "" && !col.IsDisabled() {
|
||||
*subs = append(*subs, Subtask{ID: subtaskNewID(*subs), Title: strings.TrimSpace(*draft)})
|
||||
*draft, changed = "", true
|
||||
slog.Debug("mujica: subtask_list add")
|
||||
}
|
||||
})
|
||||
if pending != nil {
|
||||
pending()
|
||||
}
|
||||
if changed {
|
||||
cache.ready = false
|
||||
c.Invalidate()
|
||||
}
|
||||
return ChoiceResult{Element: col, changed: changed}
|
||||
}
|
||||
|
||||
// subtaskNewID returns an ID no subtask uses.
|
||||
func subtaskNewID(subs []Subtask) string {
|
||||
for n := len(subs) + 1; ; n++ {
|
||||
id := fmt.Sprintf("sub-%d", n)
|
||||
if !slices.ContainsFunc(subs, func(s Subtask) bool { return s.ID == id }) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type subtaskCache struct {
|
||||
first *Subtask
|
||||
n, version, done int
|
||||
ready bool
|
||||
}
|
||||
Reference in new issue
Block a user