From 16533b8f022e3ea2ff8ded457e60b980b1165467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 12:27:55 -0400 Subject: [PATCH] Data: components 069-080 --- accordion.go | 132 +++++++ accordion_test.go | 128 +++++++ card.go | 70 ++++ card_test.go | 96 +++++ carousel.go | 175 +++++++++ carousel_test.go | 132 +++++++ collapsible.go | 99 +++++ collapsible_test.go | 86 +++++ data_table.go | 332 +++++++++++++++++ data_table_columns.go | 76 ++++ data_table_test.go | 337 ++++++++++++++++++ description_list.go | 113 ++++++ description_list_test.go | 81 +++++ docs/components/069-card.md | 46 +++ docs/components/070-list.md | 62 ++++ docs/components/071-data-table.md | 73 ++++ docs/components/072-tree.md | 57 +++ docs/components/073-tree-table.md | 61 ++++ docs/components/074-accordion.md | 47 +++ docs/components/075-collapsible.md | 45 +++ docs/components/076-timeline.md | 44 +++ docs/components/077-description-list.md | 45 +++ docs/components/078-statistic.md | 50 +++ docs/components/079-image-viewer.md | 48 +++ docs/components/080-carousel.md | 49 +++ example_data_test.go | 182 ++++++++++ examples/gallery/pages_data.go | 80 ++++- examples/gallery/pages_data_code.go | 144 ++++++++ examples/gallery/pages_data_collections.go | 303 ++++++++++++++++ examples/gallery/pages_data_content.go | 239 +++++++++++++ image_viewer.go | 193 ++++++++++ image_viewer_test.go | 157 ++++++++ list.go | 235 ++++++++++++ list_test.go | 215 +++++++++++ messages_data.go | 36 ++ statistic.go | 113 ++++++ statistic_test.go | 81 +++++ testdata/snapshots/darwin/accordion-dark.png | Bin 0 -> 11341 bytes testdata/snapshots/darwin/accordion-light.png | Bin 0 -> 10804 bytes .../darwin/card-comfortable-dark.png | Bin 0 -> 9358 bytes .../darwin/card-comfortable-light.png | Bin 0 -> 9052 bytes .../snapshots/darwin/card-compact-dark.png | Bin 0 -> 9355 bytes .../snapshots/darwin/card-compact-light.png | Bin 0 -> 9029 bytes testdata/snapshots/darwin/card-dark.png | Bin 0 -> 18950 bytes testdata/snapshots/darwin/card-light.png | Bin 0 -> 18417 bytes testdata/snapshots/darwin/carousel-dark.png | Bin 0 -> 5713 bytes testdata/snapshots/darwin/carousel-light.png | Bin 0 -> 5656 bytes .../snapshots/darwin/collapsible-dark.png | Bin 0 -> 9453 bytes .../snapshots/darwin/collapsible-light.png | Bin 0 -> 9172 bytes testdata/snapshots/darwin/data_table-dark.png | Bin 0 -> 12573 bytes .../snapshots/darwin/data_table-light.png | Bin 0 -> 12230 bytes .../darwin/description_list-dark.png | Bin 0 -> 11815 bytes .../darwin/description_list-light.png | Bin 0 -> 11296 bytes .../snapshots/darwin/image_viewer-dark.png | Bin 0 -> 15039 bytes .../snapshots/darwin/image_viewer-light.png | Bin 0 -> 14838 bytes testdata/snapshots/darwin/list-dark.png | Bin 0 -> 18588 bytes testdata/snapshots/darwin/list-light.png | Bin 0 -> 18172 bytes testdata/snapshots/darwin/statistic-dark.png | Bin 0 -> 13556 bytes testdata/snapshots/darwin/statistic-light.png | Bin 0 -> 13125 bytes testdata/snapshots/darwin/timeline-dark.png | Bin 0 -> 28085 bytes testdata/snapshots/darwin/timeline-light.png | Bin 0 -> 26700 bytes testdata/snapshots/darwin/tree-dark.png | Bin 0 -> 9132 bytes testdata/snapshots/darwin/tree-light.png | Bin 0 -> 8920 bytes testdata/snapshots/darwin/tree_table-dark.png | Bin 0 -> 9912 bytes .../snapshots/darwin/tree_table-light.png | Bin 0 -> 9545 bytes timeline.go | 115 ++++++ timeline_test.go | 50 +++ tree.go | 183 ++++++++++ tree_table.go | 177 +++++++++ tree_table_test.go | 192 ++++++++++ tree_test.go | 222 ++++++++++++ 71 files changed, 5400 insertions(+), 1 deletion(-) create mode 100644 accordion.go create mode 100644 accordion_test.go create mode 100644 card.go create mode 100644 card_test.go create mode 100644 carousel.go create mode 100644 carousel_test.go create mode 100644 collapsible.go create mode 100644 collapsible_test.go create mode 100644 data_table.go create mode 100644 data_table_columns.go create mode 100644 data_table_test.go create mode 100644 description_list.go create mode 100644 description_list_test.go create mode 100644 docs/components/069-card.md create mode 100644 docs/components/070-list.md create mode 100644 docs/components/071-data-table.md create mode 100644 docs/components/072-tree.md create mode 100644 docs/components/073-tree-table.md create mode 100644 docs/components/074-accordion.md create mode 100644 docs/components/075-collapsible.md create mode 100644 docs/components/076-timeline.md create mode 100644 docs/components/077-description-list.md create mode 100644 docs/components/078-statistic.md create mode 100644 docs/components/079-image-viewer.md create mode 100644 docs/components/080-carousel.md create mode 100644 example_data_test.go create mode 100644 examples/gallery/pages_data_code.go create mode 100644 examples/gallery/pages_data_collections.go create mode 100644 examples/gallery/pages_data_content.go create mode 100644 image_viewer.go create mode 100644 image_viewer_test.go create mode 100644 list.go create mode 100644 list_test.go create mode 100644 messages_data.go create mode 100644 statistic.go create mode 100644 statistic_test.go create mode 100644 testdata/snapshots/darwin/accordion-dark.png create mode 100644 testdata/snapshots/darwin/accordion-light.png create mode 100644 testdata/snapshots/darwin/card-comfortable-dark.png create mode 100644 testdata/snapshots/darwin/card-comfortable-light.png create mode 100644 testdata/snapshots/darwin/card-compact-dark.png create mode 100644 testdata/snapshots/darwin/card-compact-light.png create mode 100644 testdata/snapshots/darwin/card-dark.png create mode 100644 testdata/snapshots/darwin/card-light.png create mode 100644 testdata/snapshots/darwin/carousel-dark.png create mode 100644 testdata/snapshots/darwin/carousel-light.png create mode 100644 testdata/snapshots/darwin/collapsible-dark.png create mode 100644 testdata/snapshots/darwin/collapsible-light.png create mode 100644 testdata/snapshots/darwin/data_table-dark.png create mode 100644 testdata/snapshots/darwin/data_table-light.png create mode 100644 testdata/snapshots/darwin/description_list-dark.png create mode 100644 testdata/snapshots/darwin/description_list-light.png create mode 100644 testdata/snapshots/darwin/image_viewer-dark.png create mode 100644 testdata/snapshots/darwin/image_viewer-light.png create mode 100644 testdata/snapshots/darwin/list-dark.png create mode 100644 testdata/snapshots/darwin/list-light.png create mode 100644 testdata/snapshots/darwin/statistic-dark.png create mode 100644 testdata/snapshots/darwin/statistic-light.png create mode 100644 testdata/snapshots/darwin/timeline-dark.png create mode 100644 testdata/snapshots/darwin/timeline-light.png create mode 100644 testdata/snapshots/darwin/tree-dark.png create mode 100644 testdata/snapshots/darwin/tree-light.png create mode 100644 testdata/snapshots/darwin/tree_table-dark.png create mode 100644 testdata/snapshots/darwin/tree_table-light.png create mode 100644 timeline.go create mode 100644 timeline_test.go create mode 100644 tree.go create mode 100644 tree_table.go create mode 100644 tree_table_test.go create mode 100644 tree_test.go diff --git a/accordion.go b/accordion.go new file mode 100644 index 0000000..a13e865 --- /dev/null +++ b/accordion.go @@ -0,0 +1,132 @@ +package mujica + +import ( + "log/slog" + "slices" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// AccordionItem is one section of an Accordion. +type AccordionItem struct { + // ID identifies the section in the open list; required, unique. + ID string + Title string + Content func() + Disabled bool +} + +// AccordionOptions configure an Accordion. +type AccordionOptions struct { + // Multiple lets several sections stay open; otherwise opening one closes the rest. + Multiple bool + // KeepOne stops the last open section from closing. + KeepOne bool +} + +// AccordionView is a built Accordion and what happened to it in this frame. +type AccordionView struct { + *ui.Element + changed bool +} + +// Changed reports that a section opened or closed. +func (v AccordionView) Changed() bool { return v.changed } + +// Accordion shows sections whose open IDs live in *open, which the caller controls. +func Accordion(c *ui.Context, open *[]string, items []AccordionItem, opts AccordionOptions) AccordionView { + seen := map[string]bool{} + for _, it := range items { + if it.ID == "" || seen[it.ID] || it.Content == nil { + panic("mujica: Accordion item needs a unique ID and Content: " + it.ID) + } + seen[it.ID] = true + } + k := Tokens(c) + d := Density(c) + u := d.Unit() + var v AccordionView + v.Element = ui.Column(c).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius) + focusTo := ui.Local(v.Element, "focus", func() int { return -1 }) + v.Children(func() { + for i, it := range items { + ui.Column(c).Key(it.ID).Children(func() { + if i > 0 { + ui.Box(c).Height(1).FillWidth().Background(k.Border) + } + isOpen := slices.Contains(*open, it.ID) + was := isOpen + p := ui.CollapsibleBase(c, &isOpen) + tr := p.Trigger.Disabled(it.Disabled).AlignItems(ui.Center).Gap(u*2).MinHeight(d.ControlHeight()+u*2). + Padding(0, u*4).Label(it.Title) + internal.Ring(tr, k.Focus, theme.ControlRadius) + if tr.Hovered() && !it.Disabled { + tr.Background(k.SurfaceHover) + } + if *focusTo == i { + tr.Focus() + *focusTo = -1 + } + if j := accordionKey(tr, items, i); j >= 0 { + *focusTo = j + } + if isOpen != was && !it.Disabled && accordionSet(open, it.ID, isOpen, opts) { + v.changed = true + } + isOpen = slices.Contains(*open, it.ID) + prog := tr.Animate("mujica.open", collapsibleTarget(isOpen), Motion(c, theme.StateDuration)) + tr.Children(func() { + ui.Text(c, it.Title).FontWeight(600).Grow(1).MinWidth(0) + ui.Icon(c, icons.Must("chevron-down")).Size(16, 16).Rotate(180 * prog).TextColor(k.TextMuted).Shrink(0) + }) + if panel := collapsiblePanel(c, isOpen, prog, it.Content); panel != nil { + panel.Padding(0, u*4, u*4).Gap(u * 2) + } + }) + } + }) + return v +} + +// accordionKey returns the header the arrows, Home or End move to from header i, or -1. +func accordionKey(tr *ui.Element, items []AccordionItem, i int) int { + step, from := 0, i + switch { + case tr.Shortcut(0, ui.KeyDown): + step = 1 + case tr.Shortcut(0, ui.KeyUp): + step = -1 + case tr.Shortcut(0, ui.KeyHome): + step, from = 1, -1 + case tr.Shortcut(0, ui.KeyEnd): + step, from = -1, len(items) + default: + return -1 + } + for j := from + step; j >= 0 && j < len(items); j += step { + if !items[j].Disabled { + return j + } + } + return -1 +} + +// accordionSet applies a header toggle under the options and reports a change. +func accordionSet(open *[]string, id string, on bool, opts AccordionOptions) bool { + switch { + case on && opts.Multiple: + *open = append(slices.Clone(*open), id) + case on: + *open = []string{id} + case opts.KeepOne && len(*open) == 1: + return false + default: + *open = slices.DeleteFunc(slices.Clone(*open), func(s string) bool { return s == id }) + } + slog.Debug("mujica: accordion toggle", "id", id, "open", on) + return true +} diff --git a/accordion_test.go b/accordion_test.go new file mode 100644 index 0000000..a0b0778 --- /dev/null +++ b/accordion_test.go @@ -0,0 +1,128 @@ +package mujica + +import ( + "fmt" + "testing" + + "github.com/egoist/mygo/ui" +) + +func accordionHarness(open *[]string, opts *AccordionOptions, changes *int) func(c *ui.Context) { + return func(c *ui.Context) { + Use(c, Settings{}) + items := []AccordionItem{ + {ID: "a", Title: "Choir", Content: func() { ui.Text(c, "Choir body") }}, + {ID: "b", Title: "Nave", Content: func() { ui.Text(c, "Nave body") }}, + {ID: "x", Title: "Locked", Disabled: true, Content: func() { ui.Text(c, "Locked body") }}, + {ID: "c", Title: "Crypt", Content: func() { ui.Text(c, "Crypt body") }}, + } + ui.Column(c).Padding(10).Children(func() { + if Accordion(c, open, items, *opts).Changed() { + *changes++ + } + }) + } +} + +func TestAccordionSingle(t *testing.T) { + var open []string + var opts AccordionOptions + changes := 0 + tt := ui.NewTester(accordionHarness(&open, &opts, &changes), 300, 400) + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) + _ = tt.Click("Choir") + _ = tt.Click("Nave") + if fmt.Sprint(open) != "[b]" || changes != 2 || tt.HasText("Choir body") || !tt.HasText("Nave body") { + t.Fatalf("open %v changes %d", open, changes) + } + _ = tt.Click("Nave") + if len(open) != 0 { + t.Fatalf("could not close all: %v", open) + } + _ = tt.Click("Locked") + if len(open) != 0 || changes != 3 { + t.Fatalf("disabled opened: %v", open) + } +} + +func TestAccordionMultipleKeepOne(t *testing.T) { + open := []string{"a"} + opts := AccordionOptions{Multiple: true, KeepOne: true} + changes := 0 + tt := ui.NewTester(accordionHarness(&open, &opts, &changes), 300, 400) + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) + _ = tt.Click("Crypt") + if fmt.Sprint(open) != "[a c]" { + t.Fatalf("multiple: %v", open) + } + _ = tt.Click("Choir") + _ = tt.Click("Crypt") + if fmt.Sprint(open) != "[c]" || changes != 2 { + t.Fatalf("keep one: %v changes %d", open, changes) + } +} + +func TestAccordionKeyboard(t *testing.T) { + var open []string + var opts AccordionOptions + changes := 0 + tt := ui.NewTester(accordionHarness(&open, &opts, &changes), 300, 400) + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) + tt.Key(0, ui.KeyTab) + if !tt.Focused("Choir") { + t.Fatal("Tab missed the first header") + } + tt.Key(0, ui.KeyDown) + if !tt.Focused("Nave") { + t.Fatal("Down missed Nave") + } + tt.Key(0, ui.KeyDown) // skips the disabled header + if !tt.Focused("Crypt") { + t.Fatal("Down did not skip the disabled header") + } + tt.Key(0, ui.KeyHome) + if !tt.Focused("Choir") { + t.Fatal("Home missed") + } + tt.Key(0, ui.KeyEnd) + tt.Key(0, ui.KeyEnter) + if fmt.Sprint(open) != "[c]" { + t.Fatalf("Enter on End opened %v", open) + } + tt.Key(0, ui.KeyUp) + if !tt.Focused("Nave") { + t.Fatal("Up missed Nave") + } +} + +func TestAccordionBadItems(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic on duplicate IDs") + } + }() + var open []string + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Accordion(c, &open, []AccordionItem{{ID: "a", Content: func() {}}, {ID: "a", Content: func() {}}}, AccordionOptions{}) + }, 100, 100) +} + +func TestAccordionSnapshot(t *testing.T) { + var opts AccordionOptions + changes := 0 + open := []string{"b"} + snapshot(t, "accordion", 400, 300, accordionHarnessNoUse(&open, &opts, &changes)) +} + +func accordionHarnessNoUse(open *[]string, opts *AccordionOptions, changes *int) func(c *ui.Context) { + return func(c *ui.Context) { + items := []AccordionItem{ + {ID: "a", Title: "Choir", Content: func() { ui.Text(c, "Twelve voices, two at the altar.") }}, + {ID: "b", Title: "Nave", Content: func() { ui.Text(c, "Pews of black oak, polished for the feast.") }}, + {ID: "x", Title: "Locked", Disabled: true, Content: func() {}}, + {ID: "c", Title: "Crypt", Content: func() {}}, + } + ui.Column(c).Padding(16).Children(func() { Accordion(c, open, items, *opts) }) + } +} diff --git a/card.go b/card.go new file mode 100644 index 0000000..6f06830 --- /dev/null +++ b/card.go @@ -0,0 +1,70 @@ +package mujica + +import ( + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// CardOptions configure a Card. +type CardOptions struct { + // Title heads the card in the serif face; empty shows no header. + Title string + // Footer builds the bottom slot, below a divider; nil shows none. + Footer func() + // Decorated draws a double border and corner ornaments in Ornament. + Decorated bool +} + +// cardInset is how far the inner line of a decorated card sits inside. +const cardInset float32 = 4 + +// Card groups content on a surface with an outline. It adds no click behavior. +func Card(c *ui.Context, opts CardOptions, body func()) *ui.Element { + k := Tokens(c) + u := Density(c).Unit() + pad := u * 4 + if opts.Decorated { + pad += cardInset + 2 + } + card := ui.Column(c).Background(k.Surface).Radius(theme.CardRadius).Gap(u * 3).Padding(pad).Role(ui.RoleGroup) + if opts.Title != "" { + card.Label(opts.Title) + } + if opts.Decorated { + card.Border(theme.BorderWidth, k.Ornament).DrawOver(func(p *ui.Painter, r ui.Rect) { + cardOrnament(p, r, k.Ornament) + }) + } else { + card.Border(theme.BorderWidth, k.Border) + } + card.Children(func() { + if opts.Title != "" { + ui.Text(c, opts.Title).Font(theme.SerifFont).FontSize(theme.H3Size).FontWeight(600).LineHeight(1.3) + if opts.Decorated { + ui.Box(c).Height(7).FillWidth().Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { + cy := r.Y + r.H/2 + p.Line(r.X, cy, r.X+r.W/2-6, cy, 1, k.Ornament) + p.Line(r.X+r.W/2+6, cy, r.X+r.W, cy, 1, k.Ornament) + p.FillPath(internal.Diamond(internal.Snap(p, r.X+r.W/2), internal.Snap(p, cy), 7), k.Ornament) + }) + } + } + ui.Column(c).Gap(u * 2).Children(body) + if opts.Footer != nil { + ui.Box(c).Height(1).FillWidth().Shrink(0).Background(k.Border) + ui.Row(c).Gap(u * 2).Justify(ui.End).AlignItems(ui.Center).Wrap().Children(opts.Footer) + } + }) + return card +} + +// cardOrnament draws the inner line and the four corner diamonds. +func cardOrnament(p *ui.Painter, r ui.Rect, col ui.Color) { + in := ui.Rect{X: r.X + cardInset, Y: r.Y + cardInset, W: r.W - 2*cardInset, H: r.H - 2*cardInset} + p.Stroke(in, col, 1, 1) + for _, pt := range [4][2]float32{{in.X, in.Y}, {in.X + in.W, in.Y}, {in.X, in.Y + in.H}, {in.X + in.W, in.Y + in.H}} { + p.FillPath(internal.Diamond(internal.Snap(p, pt[0]), internal.Snap(p, pt[1]), 9), col) + } +} diff --git a/card_test.go b/card_test.go new file mode 100644 index 0000000..3d44603 --- /dev/null +++ b/card_test.go @@ -0,0 +1,96 @@ +package mujica + +import ( + "image/color" + "strings" + "testing" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +func cardView(decorated bool) func(c *ui.Context) { + return func(c *ui.Context) { + ui.Column(c).Padding(20).AlignItems(ui.Start).Children(func() { + Card(c, CardOptions{Title: "Archive", Decorated: decorated, Footer: func() { + Button(c, "Open", ButtonOptions{}) + }}, func() { + ui.Text(c, "Letters of the court") + }).Width(260) + }) + } +} + +func TestCardSlots(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Mode: Light}) + cardView(false)(c) + }, 400, 300) + for _, s := range []string{"Archive", "Letters of the court", "Open"} { + if !tt.HasText(s) { + t.Fatalf("missing %q in %q", s, tt.Texts()) + } + } + title, _ := tt.Find("Archive") + body, _ := tt.Find("Letters of the court") + foot, _ := tt.Find("Open") + if !(title.Y < body.Y && body.Y < foot.Y) { + t.Fatalf("slot order: title %v body %v footer %v", title.Y, body.Y, foot.Y) + } +} + +func TestCardNoTitleNoFooter(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Card(c, CardOptions{}, func() { ui.Text(c, "Only body") }) + }, 300, 200) + if got := tt.Texts(); len(got) != 1 || got[0] != "Only body" { + t.Fatalf("texts %q", got) + } +} + +func TestCardDecorationColor(t *testing.T) { + for _, dec := range []bool{false, true} { + var bounds ui.Rect + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Mode: Light}) + ui.Column(c).Padding(20).AlignItems(ui.Start).Children(func() { + bounds = Card(c, CardOptions{Title: "T", Decorated: dec}, func() {}).Width(200).Bounds() + }) + }, 300, 200) + tt.Frame() + img := tt.Image() + // The inner corner diamond sits at the inset corner. + px := img.At(int(bounds.X+cardInset), int(bounds.Y+cardInset)) + want := toRGBA(theme.Light().Ornament) + if got := color.RGBAModel.Convert(px).(color.RGBA); (got == want) != dec { + t.Fatalf("decorated=%v: corner pixel %v, ornament %v", dec, got, want) + } + } +} + +func toRGBA(c ui.Color) color.RGBA { return color.RGBA{c.R, c.G, c.B, c.A} } + +func TestCardSnapshot(t *testing.T) { + snapshot(t, "card", 640, 240, func(c *ui.Context) { + ui.Row(c).Padding(20).Gap(20).AlignItems(ui.Start).Children(func() { + Card(c, CardOptions{Title: "Archive", Footer: func() { + Button(c, "Open", ButtonOptions{Variant: Secondary}) + }}, func() { ui.Text(c, "Letters of the court, sealed in wine-red wax.") }).Width(280) + Card(c, CardOptions{Title: "Chapel", Decorated: true, Footer: func() { + Button(c, "Enter", ButtonOptions{}) + }}, func() { ui.Text(c, "Vespers at seven; candles by the north door.") }).Width(280) + }) + }) + for _, d := range []theme.Density{theme.Compact, theme.Comfortable} { + snapshot(t, "card-"+strings.ToLower(d.String()), 640, 260, func(c *ui.Context) { + mode := Light + if Tokens(c).Dark { + mode = Dark + } + Use(c, Settings{Mode: mode, Density: d}) + cardView(true)(c) + }) + } +} diff --git a/carousel.go b/carousel.go new file mode 100644 index 0000000..81078e3 --- /dev/null +++ b/carousel.go @@ -0,0 +1,175 @@ +package mujica + +import ( + "fmt" + "log/slog" + "time" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// CarouselOptions configure a Carousel. +type CarouselOptions struct { + // Label names the carousel for assistive technology. + Label string + // Autoplay advances slides every Interval; it pauses on hover, focus or the pause button. + Autoplay bool + // Interval defaults to 5 seconds. + Interval time.Duration + // Loop wraps from the last slide to the first and back; Autoplay always wraps. + Loop bool +} + +// CarouselView is a built Carousel and what happened to it in this frame. +type CarouselView struct { + *ui.Element + changed bool +} + +// Changed reports a new slide. +func (v CarouselView) Changed() bool { return v.changed } + +type carouselState struct { + paused bool + next time.Time +} + +// Carousel shows slide *index of n with previous, next and position controls. +func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func(i int)) CarouselView { + if n < 0 || opts.Interval < 0 { + panic("mujica: Carousel with negative count or Interval") + } + interval := opts.Interval + if interval == 0 { + interval = 5 * time.Second + } + k := Tokens(c) + u := Density(c).Unit() + var v CarouselView + v.Element = ui.Column(c).Gap(u * 2).Label(opts.Label) + st := ui.Local(v.Element, "state", func() carouselState { return carouselState{} }) + if n > 0 && (*index < 0 || *index >= n) { + slog.Debug("mujica: carousel index clamped", "index", *index, "n", n) + *index = max(0, min(*index, n-1)) + } + wrap := opts.Loop || opts.Autoplay + goTo := func(i int) { + if wrap { + i = (i%n + n) % n + } + if n == 0 || i < 0 || i >= n || i == *index { + return + } + *index = i + v.changed = true + c.Announce(fmt.Sprintf(msg(c, "carousel.position"), i+1, n)) + slog.Debug("mujica: carousel slide", "index", i) + } + now := c.Now() + playing := opts.Autoplay && n > 1 && !st.paused && !v.Hovered() && !v.FocusWithin() + if !playing { + st.next = time.Time{} + } else if st.next.IsZero() { + st.next = now.Add(interval) + } else if !now.Before(st.next) { + goTo(*index + 1) + st.next = now.Add(interval) + } + if playing { + c.After(st.next.Sub(now)) + } + switch { + case v.Shortcut(0, ui.KeyLeft): + goTo(*index - 1) + case v.Shortcut(0, ui.KeyRight): + goTo(*index + 1) + case v.Shortcut(0, ui.KeyHome): + goTo(0) + case v.Shortcut(0, ui.KeyEnd): + goTo(n - 1) + } + v.Children(func() { + ui.Row(c).Grow(1).MinHeight(120).AlignItems(ui.Center).Gap(u * 2).Children(func() { + prev := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icons.Must("chevron-left"), Label: msg(c, "carousel.prev"), Disabled: n < 2 || !wrap && *index == 0}) + ui.Column(c).Grow(1).MinWidth(0).FillHeight().Clip().Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius). + Role(ui.RoleGroup).Label(fmt.Sprintf(msg(c, "carousel.position"), *index+1, n)).Children(func() { + if n > 0 { + ui.Column(c).Key(*index).Grow(1).Children(func() { slide(*index) }) + } + }) + next := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icons.Must("chevron-right"), Label: msg(c, "carousel.next"), Disabled: n < 2 || !wrap && *index == n-1}) + if prev.Clicked() { + goTo(*index - 1) + } + if next.Clicked() { + goTo(*index + 1) + } + }) + ui.Row(c).Justify(ui.Center).AlignItems(ui.Center).Gap(u * 2).Children(func() { + ui.Row(c).Gap(u).FocusGroup(ui.Horizontal).Children(func() { + for i := range n { + if carouselDot(c, i, n, i == *index).Clicked() { + goTo(i) + } + } + }) + if opts.Autoplay && carouselPause(c, st.paused).Clicked() { + st.paused = !st.paused + slog.Debug("mujica: carousel autoplay", "paused", st.paused) + } + }) + }) + return v +} + +// carouselDot is a position button: a filled diamond for the current slide. +func carouselDot(c *ui.Context, i, n int, on bool) *ui.Element { + k := Tokens(c) + b := ui.ButtonBase(c).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.ControlRadius). + Label(fmt.Sprintf(msg(c, "carousel.position"), i+1, n)).Role(ui.RoleTab).Selected(on) + internal.Ring(b, k.Focus, theme.ControlRadius) + hover := b.Hovered() + b.Background(ui.Transparent).Draw(func(p *ui.Painter, r ui.Rect) { + cx, cy := internal.Snap(p, r.X+r.W/2), internal.Snap(p, r.Y+r.H/2) + switch { + case on: + p.FillPath(internal.Diamond(cx, cy, 12), k.AccentText) + case hover: + p.FillPath(internal.Diamond(cx, cy, 8), k.TextMuted) + default: + p.StrokePath(internal.Diamond(cx, cy, 8), 1.25, k.TextMuted) + } + }) + return b +} + +// carouselPause is the pause button: two bars, or a play triangle while paused. +func carouselPause(c *ui.Context, paused bool) *ui.Element { + k := Tokens(c) + label := msg(c, "carousel.pause") + if paused { + label = msg(c, "carousel.play") + } + b := ui.ButtonBase(c).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.ControlRadius).Label(label) + internal.Ring(b, k.Focus, theme.ControlRadius) + if b.Pressed() { + b.Background(k.Selection) + } else if b.Hovered() { + b.Background(k.SurfaceHover) + } + return b.Draw(func(p *ui.Painter, r ui.Rect) { + cx, cy := internal.Snap(p, r.X+r.W/2), internal.Snap(p, r.Y+r.H/2) + if !paused { + p.Fill(ui.Rect{X: cx - 4, Y: cy - 5, W: 3, H: 10}, k.AccentText, 0) + p.Fill(ui.Rect{X: cx + 1, Y: cy - 5, W: 3, H: 10}, k.AccentText, 0) + return + } + var tri ui.Path + tri.MoveTo(cx-3, cy-5).LineTo(cx+5, cy).LineTo(cx-3, cy+5).Close() + p.FillPath(&tri, k.AccentText) + }) +} diff --git a/carousel_test.go b/carousel_test.go new file mode 100644 index 0000000..b30e2e4 --- /dev/null +++ b/carousel_test.go @@ -0,0 +1,132 @@ +package mujica + +import ( + "fmt" + "testing" + "time" + + "github.com/egoist/mygo/ui" +) + +type carouselHarness struct { + index int + opts CarouselOptions + changes int +} + +func (h *carouselHarness) view(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Padding(10).Size(400, 260).Children(func() { + v := Carousel(c, &h.index, 4, h.opts, func(i int) { + ui.Text(c, fmt.Sprintf("Window %d", i+1)).Padding(20) + }) + v.Grow(1) + if v.Changed() { + h.changes++ + } + }) +} + +func TestCarouselButtonsAndDots(t *testing.T) { + h := &carouselHarness{} + tt := ui.NewTester(h.view, 420, 280) + _ = tt.Click("Previous slide") + if h.index != 0 || h.changes != 0 { + t.Fatal("previous moved past the first slide") + } + _ = tt.Click("Next slide") + if h.index != 1 || !tt.HasText("Window 2") || h.changes != 1 { + t.Fatalf("next: %d changes %d %q", h.index, h.changes, tt.Texts()) + } + _ = tt.Click("Slide 4 of 4") + if h.index != 3 { + t.Fatalf("dot: %d", h.index) + } + _ = tt.Click("Next slide") + if h.index != 3 { + t.Fatal("next moved past the last slide without Loop") + } + h.opts.Loop = true + tt.Frame() + _ = tt.Click("Next slide") + if h.index != 0 { + t.Fatalf("loop: %d", h.index) + } +} + +func TestCarouselKeyboard(t *testing.T) { + h := &carouselHarness{} + tt := ui.NewTester(h.view, 420, 280) + tt.Key(0, ui.KeyTab) // previous is disabled: focus lands on next + tt.Key(0, ui.KeyRight) + tt.Key(0, ui.KeyRight) + if h.index != 2 { + t.Fatalf("Right×2: %d", h.index) + } + tt.Key(0, ui.KeyEnd) + if h.index != 3 { + t.Fatalf("End: %d", h.index) + } + tt.Key(0, ui.KeyHome) + tt.Key(0, ui.KeyLeft) + if h.index != 0 { + t.Fatalf("Home, Left: %d", h.index) + } + if got := tt.Announcements(); len(got) == 0 || got[len(got)-1] != "Slide 1 of 4" { + t.Fatalf("announcements %q", got) + } +} + +func TestCarouselNoAutoplayByDefault(t *testing.T) { + h := &carouselHarness{opts: CarouselOptions{Interval: 10 * time.Millisecond}} + tt := ui.NewTester(h.view, 420, 280) + time.Sleep(30 * time.Millisecond) + tt.Frame() + if h.index != 0 || tt.HasText("Pause autoplay") { + t.Fatalf("advanced without Autoplay: %d", h.index) + } +} + +func TestCarouselAutoplayPause(t *testing.T) { + h := &carouselHarness{opts: CarouselOptions{Autoplay: true, Interval: 20 * time.Millisecond}} + tt := ui.NewTester(h.view, 420, 280) + time.Sleep(30 * time.Millisecond) + tt.Frame() + if h.index != 1 { + t.Fatalf("autoplay did not advance: %d", h.index) + } + _ = tt.Click("Pause autoplay") + tt.Move(1, 1) // away from the carousel + tt.SetFocused(false) + tt.SetFocused(true) + at := h.index + time.Sleep(50 * time.Millisecond) + tt.Frame() + if h.index != at || !tt.HasText("") { + t.Fatalf("paused carousel advanced: %d -> %d", at, h.index) + } + if _, ok := tt.Find("Resume autoplay"); !ok { + t.Fatal("no resume button") + } + r, _ := tt.Find("Window " + fmt.Sprint(at+1)) + _ = tt.Click("Resume autoplay") + tt.Move(r.X+2, r.Y+2) // hover pauses too + time.Sleep(50 * time.Millisecond) + tt.Frame() + if h.index != at { + t.Fatalf("hovered carousel advanced: %d -> %d", at, h.index) + } +} + +func TestCarouselSnapshot(t *testing.T) { + snapshot(t, "carousel", 440, 240, func(c *ui.Context) { + i := ui.Local(c.Root(), "i", func() *int { v := 1; return &v }) + ui.Column(c).Padding(16).Size(440, 240).Children(func() { + Carousel(c, *i, 4, CarouselOptions{Autoplay: true, Label: "Windows"}, func(n int) { + ui.Column(c).Grow(1).Center().Children(func() { + ui.Text(c, fmt.Sprintf("Rose window %d", n+1)).FontSize(18) + }) + }).Grow(1) + }) + }) +} diff --git a/collapsible.go b/collapsible.go new file mode 100644 index 0000000..7e90fa4 --- /dev/null +++ b/collapsible.go @@ -0,0 +1,99 @@ +package mujica + +import ( + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// CollapsibleOptions configure a Collapsible. +type CollapsibleOptions struct { + // Title is the trigger's text; required unless Trigger is set. + Title string + // Trigger builds custom trigger content after the arrow. + Trigger func() + Disabled bool +} + +// CollapsibleView is a built Collapsible and what happened to it in this frame. +type CollapsibleView struct { + *ui.Element + changed bool +} + +// Changed reports that the trigger opened or closed the content. +func (v CollapsibleView) Changed() bool { return v.changed } + +// Collapsible shows content below a trigger while *open; closed content is not built. +func Collapsible(c *ui.Context, open *bool, opts CollapsibleOptions, content func()) CollapsibleView { + if opts.Title == "" && opts.Trigger == nil { + panic("mujica: Collapsible needs Title or Trigger") + } + k := Tokens(c) + u := Density(c).Unit() + var v CollapsibleView + v.Element = ui.Column(c).Shrink(0) + v.Children(func() { + was := *open + p := ui.CollapsibleBase(c, open) + tr := p.Trigger + if opts.Disabled { + *open = was + } + v.changed = *open != was + tr.Disabled(opts.Disabled).AlignSelf(ui.Start).AlignItems(ui.Center).Gap(u*2). + MinHeight(Density(c).ControlHeight()).Padding(0, u*3, 0, u*2).Radius(theme.ControlRadius) + if opts.Title != "" { + tr.Label(opts.Title) + } + internal.Ring(tr, k.Focus, theme.ControlRadius) + if tr.Hovered() && !opts.Disabled { + tr.Background(k.SurfaceHover) + } + prog := tr.Animate("mujica.open", collapsibleTarget(*open), Motion(c, theme.StateDuration)) + tr.Children(func() { + ui.Icon(c, icons.Must("chevron-right")).Size(16, 16).Rotate(90 * prog).TextColor(k.TextMuted) + if opts.Trigger != nil { + opts.Trigger() + } else { + ui.Text(c, opts.Title).FontWeight(600).SingleLine() + } + }) + if panel := collapsiblePanel(c, *open, prog, content); panel != nil { + panel.Padding(u*2, u*2, u*2, u*8).Gap(u * 2) + } + }) + if v.changed { + slog.Debug("mujica: collapsible toggle", "open", *open) + } + return v +} + +func collapsibleTarget(open bool) float32 { + if open { + return 1 + } + return 0 +} + +// collapsiblePanel builds fn while open or animating, clipped to its progress. +func collapsiblePanel(c *ui.Context, open bool, prog float32, fn func()) *ui.Element { + if !open && prog == 0 { + return nil + } + clip := ui.Column(c).Shrink(0) + var inner *ui.Element + clip.Children(func() { + inner = ui.Column(c).Shrink(0) + inner.Children(fn) + }) + if prog < 1 { + // The content's height from the last frame, grown by progress. + clip.ClipY().Height(prog * inner.Bounds().H) + } + return inner +} diff --git a/collapsible_test.go b/collapsible_test.go new file mode 100644 index 0000000..90e5b28 --- /dev/null +++ b/collapsible_test.go @@ -0,0 +1,86 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestCollapsibleToggleAndFocus(t *testing.T) { + open, changes, clicks := false, 0, 0 + disabled := false + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Padding(10).Children(func() { + if Collapsible(c, &open, CollapsibleOptions{Title: "Details", Disabled: disabled}, func() { + if Button(c, "Inner", ButtonOptions{}).Clicked() { + clicks++ + } + }).Changed() { + changes++ + } + Button(c, "After", ButtonOptions{}) + }) + }, 300, 300) + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) + if tt.HasText("Inner") { + t.Fatal("closed content built") + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyTab) + if !tt.Focused("After") { + t.Fatal("Tab entered hidden content") + } + if err := tt.Click("Details"); err != nil { + t.Fatal(err) + } + if !open || changes != 1 || !tt.HasText("Inner") { + t.Fatalf("open %v changes %d", open, changes) + } + tt.Key(0, ui.KeyTab) + if !tt.Focused("Inner") { + t.Fatal("Tab skipped open content") + } + tt.Key(ui.Shift, ui.KeyTab) + tt.Key(0, ui.KeySpace) + if open || changes != 2 { + t.Fatalf("Space did not close: open %v changes %d", open, changes) + } + open = true // controlled from outside + tt.Frame() + if !tt.HasText("Inner") { + t.Fatal("external open ignored") + } + disabled = true + tt.Frame() + _ = tt.Click("Details") + if !open || changes != 2 { + t.Fatal("disabled trigger toggled") + } +} + +func TestCollapsibleNeedsTitle(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic") + } + }() + open := false + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Collapsible(c, &open, CollapsibleOptions{}, func() {}) + }, 100, 100) +} + +func TestCollapsibleSnapshot(t *testing.T) { + snapshot(t, "collapsible", 400, 200, func(c *ui.Context) { + a := ui.Local(c.Root(), "a", func() *bool { b := true; return &b }) + b := ui.Local(c.Root(), "b", func() *bool { b := false; return &b }) + ui.Column(c).Padding(16).Gap(8).Children(func() { + Collapsible(c, *a, CollapsibleOptions{Title: "Rites of the evening"}, func() { + ui.Text(c, "Candles are lit at the seventh bell.") + }) + Collapsible(c, *b, CollapsibleOptions{Title: "Sealed records", Disabled: true}, func() {}) + }) + }) +} diff --git a/data_table.go b/data_table.go new file mode 100644 index 0000000..eafc719 --- /dev/null +++ b/data_table.go @@ -0,0 +1,332 @@ +package mujica + +import ( + "log/slog" + "slices" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// DataColumn is a column of a DataTable showing rows of type R. +type DataColumn[R any] struct { + // ID identifies the column in sort, layout and visibility; required, unique. + ID string + Title string + // Width is in DIPs; 0 shares the room left. MinWidth bounds resizing. + Width, MinWidth float32 + Align ui.Align + // Sortable lets a header click sort by the column; local mode needs Compare. + Sortable bool + // Compare orders two rows, as cmp.Compare does. + Compare func(a, b R) int + // Text is the cell's plain text, for filtering and the default cell; required. + Text func(r R) string + // Cell builds a custom cell; nil shows Text. + Cell func(r R) +} + +// DataTableOptions configure a DataTable. +type DataTableOptions[R any, K comparable] struct { + Columns []DataColumn[R] + // Key returns a row's stable ID; required. + Key func(r R) K + Multiple bool + // Controlled shows rows as given: the caller sorts and filters on SortChanged. + Controlled bool + // Version must change when rows change in place; a new slice is noticed. + Version int + // Empty replaces the built-in "No items" text. + Empty string + Status DataStatus + Error string +} + +// DataTableState holds a DataTable's sort, filter, layout and selection. +type DataTableState[K comparable] struct { + Sort ui.SortOrder + // Filter keeps rows whose cells contain it, ignoring case, in local mode. + Filter string + // Hidden names the columns not shown, by ID. + Hidden map[string]bool + // Layout is the column order and widths the user set. + Layout ui.TableLayout + // Selection holds the chosen row IDs of a Multiple table. + Selection ui.Selection[K] + rows ui.ListState + cursor int + started bool + sel K + has bool + cache dataTableCache[K] +} + +// dataTableCache is the sorted, filtered view, rebuilt only when its inputs change. +type dataTableCache[K comparable] struct { + valid bool + first any + n int + version int + sort ui.SortOrder + filter string + view []int + at map[K]int +} + +// Selected returns the chosen row's ID, or the last chosen of a Multiple table. +func (s *DataTableState[K]) Selected() (K, bool) { return s.sel, s.has } + +// Rows returns how many rows the table shows after filtering. +func (s *DataTableState[K]) Rows() int { return len(s.cache.view) } + +// DataTableView is a built DataTable and what happened to it in this frame. +type DataTableView struct { + *ui.Element + changed, submitted, sorted, retry bool +} + +// Changed reports a new selection. +func (v DataTableView) Changed() bool { return v.changed } + +// Submitted reports Enter or a double click on a row. +func (v DataTableView) Submitted() bool { return v.submitted } + +// SortChanged reports a header click that changed State.Sort. +func (v DataTableView) SortChanged() bool { return v.sorted } + +// Retried reports a click on the retry action of a failed load. +func (v DataTableView) Retried() bool { return v.retry } + +const dataTableMarkID = "mujica.select" + +// DataTable shows rows in columns, building only those in view. +func DataTable[R any, K comparable](c *ui.Context, s *DataTableState[K], rows []R, opts DataTableOptions[R, K]) DataTableView { + dataTableCheck(opts) + if !s.started { + s.started, s.cursor = true, -1 + } + dataTableRefresh(s, rows, opts) + if s.has { + if i, ok := s.cache.at[s.sel]; ok { + s.cursor = i + } else { + s.cursor = -1 + } + } + k := Tokens(c) + d := Density(c) + var v DataTableView + v.Element = ui.Column(c).MinHeight(0) + v.Children(func() { + view := s.cache.view + if len(view) == 0 { + empty := opts.Empty + if len(rows) > 0 { + empty = msg(c, "data_table.noMatch") + } + v.retry = dataPlaceholder(c, opts.Status, opts.Error, empty, msg(c, "list.empty")) + return + } + vis, cols := dataTableColumns(c, s, opts) + s.rows.Key = func(i int) any { return opts.Key(rows[view[i]]) } + s.rows.Label = func(i int) string { return opts.Columns[vis[0]].Text(rows[view[i]]) } + s.rows.Selected, s.rows.Sort, s.rows.Columns = &s.cursor, &s.Sort, s.Layout + s.rows.Selection = nil + if opts.Multiple { + s.rows.Selection = &s.Selection + } + before := s.Sort + // Rows reach the data-row height of the density. + saved := c.Theme() + t := *saved + t.Spacing = d.ControlHeight() / 8 + c.SetTheme(&t) + e := ui.Table(c, &s.rows, cols, len(view), func(row, col int) { + r := rows[view[row]] + if col == 0 { + dataTableMark(c, s, opts, opts.Key(r), row) + return + } + dc := opts.Columns[vis[col-1]] + if dc.Cell != nil { + dc.Cell(r) + } else { + ui.Text(c, dc.Text(r)).SingleLine() + } + }) + c.SetTheme(saved) + e.Grow(1).MinHeight(0).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius) + internal.Ring(e, k.Focus, theme.ControlRadius) + s.Layout = s.rows.Columns + v.sorted = s.Sort != before + v.changed = e.Changed() && !v.sorted + v.submitted = e.Submitted() + v.retry = dataFooter(c, opts.Status, opts.Error) + if s.cursor >= 0 && s.cursor < len(view) { + s.sel, s.has = opts.Key(rows[view[s.cursor]]), true + } + }) + if v.sorted { + slog.Debug("mujica: data_table sort", "column", s.Sort.Column, "descending", s.Sort.Descending) + } + if v.changed { + slog.Debug("mujica: data_table select", "count", s.Selection.Len()) + } + return v +} + +func dataTableCheck[R any, K comparable](opts DataTableOptions[R, K]) { + if opts.Key == nil || len(opts.Columns) == 0 { + panic("mujica: DataTable needs Key and Columns") + } + seen := map[string]bool{} + for _, col := range opts.Columns { + if col.ID == "" || seen[col.ID] || col.ID == dataTableMarkID || col.Text == nil { + panic("mujica: DataTable column needs a unique ID and Text: " + col.ID) + } + if col.Sortable && !opts.Controlled && col.Compare == nil { + panic("mujica: sortable DataTable column needs Compare: " + col.ID) + } + seen[col.ID] = true + } +} + +// dataTableRefresh rebuilds the view only when rows, sort or filter changed. +func dataTableRefresh[R any, K comparable](s *DataTableState[K], rows []R, opts DataTableOptions[R, K]) { + cc := &s.cache + var first any + if len(rows) > 0 { + first = &rows[0] + } + sort, filter := s.Sort, s.Filter + if opts.Controlled { + sort, filter = ui.SortOrder{}, "" + } + data := !cc.valid || cc.first != first || cc.n != len(rows) || cc.version != opts.Version + if !data && cc.sort == sort && cc.filter == filter { + return + } + var view []int + if opts.Controlled { + view = make([]int, len(rows)) + for i := range view { + view[i] = i + } + } else { + view = DataTableRows(rows, opts.Columns, sort, filter) + } + at := make(map[K]int, len(view)) + for i, j := range view { + at[opts.Key(rows[j])] = i + } + if len(at) != len(view) { + panic("mujica: DataTable rows with duplicate keys") + } + *cc = dataTableCache[K]{valid: true, first: first, n: len(rows), version: opts.Version, sort: sort, filter: filter, view: view, at: at} + if data { + dataTablePrune(s, rows, opts) + } + slog.Debug("mujica: data_table view", "rows", len(view), "of", len(rows)) +} + +// dataTablePrune drops chosen IDs whose rows left the data. +func dataTablePrune[R any, K comparable](s *DataTableState[K], rows []R, opts DataTableOptions[R, K]) { + all := s.cache.at + if len(s.cache.view) != len(rows) { + all = make(map[K]int, len(rows)) + for i, r := range rows { + all[opts.Key(r)] = i + } + } + for key := range s.Selection.All() { + if _, ok := all[key]; !ok { + s.Selection.Remove(key) + } + } + if _, ok := all[s.sel]; s.has && !ok { + s.has = false + } +} + +func dataTableColumns[R any, K comparable](c *ui.Context, s *DataTableState[K], opts DataTableOptions[R, K]) ([]int, []ui.TableColumn) { + w := Density(c).ControlHeight() + cols := []ui.TableColumn{{ID: dataTableMarkID, Width: w, Fixed: true, Align: ui.Center}} + var vis []int + for i, col := range opts.Columns { + if s.Hidden[col.ID] { + continue + } + vis = append(vis, i) + cols = append(cols, ui.TableColumn{ID: col.ID, Title: col.Title, Width: col.Width, MinWidth: col.MinWidth, Align: col.Align, Sortable: col.Sortable}) + } + if len(vis) == 0 { + panic("mujica: DataTable with every column hidden") + } + return vis, cols +} + +// dataTableMark draws the selection column: a check box, or a diamond on the chosen row. +func dataTableMark[R any, K comparable](c *ui.Context, s *DataTableState[K], opts DataTableOptions[R, K], key K, row int) { + k := Tokens(c) + if !opts.Multiple { + if row == s.cursor { + ui.Box(c).Size(8, 8).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { + p.FillPath(internal.Diamond(internal.Snap(p, r.X+r.W/2), internal.Snap(p, r.Y+r.H/2), r.W), k.OnAccent) + }) + } + return + } + on := s.Selection.Has(key) + box := ui.Box(c).Size(16, 16).Shrink(0).Radius(theme.ControlRadius).Role(ui.RoleCheckBox).Label(msg(c, "data_table.selectRow")) + if on { + box.Border(theme.BorderWidth, k.OnAccent).TextColor(k.OnAccent).Children(func() { + ui.Icon(c, icons.Must("check")).Size(14, 14) + }).Center() + } else { + box.Border(theme.BorderWidth, k.ControlBorder) + } + if box.Clicked() { + if on { + s.Selection.Remove(key) + } else { + s.Selection.Add(key) + } + slog.Debug("mujica: data_table toggle", "on", !on) + } +} + +// DataTableRows sorts and filters rows as a local DataTable does; it returns indices. +func DataTableRows[R any](rows []R, cols []DataColumn[R], sort ui.SortOrder, filter string) []int { + view := make([]int, 0, len(rows)) + q := strings.ToLower(filter) + for i, r := range rows { + if q == "" || dataTableMatch(r, cols, q) { + view = append(view, i) + } + } + for _, col := range cols { + if col.ID == sort.Column && col.Compare != nil { + cmp := col.Compare + slices.SortStableFunc(view, func(a, b int) int { + if sort.Descending { + return cmp(rows[b], rows[a]) + } + return cmp(rows[a], rows[b]) + }) + } + } + return view +} + +func dataTableMatch[R any](r R, cols []DataColumn[R], q string) bool { + for _, col := range cols { + if strings.Contains(strings.ToLower(col.Text(r)), q) { + return true + } + } + return false +} diff --git a/data_table_columns.go b/data_table_columns.go new file mode 100644 index 0000000..430b8e1 --- /dev/null +++ b/data_table_columns.go @@ -0,0 +1,76 @@ +package mujica + +import ( + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// DataTableColumnPicker is a Columns button whose popover shows and hides columns. +// The last visible column cannot be hidden. +func DataTableColumnPicker[R any, K comparable](c *ui.Context, s *DataTableState[K], cols []DataColumn[R]) *ui.Element { + k := Tokens(c) + u := Density(c).Unit() + b := Button(c, msg(c, "data_table.columns"), ButtonOptions{Variant: Secondary, Icon: icons.Must("eye")}) + open := ui.Local(b, "open", func() bool { return false }) + if b.Clicked() { + *open = !*open + } + visible := 0 + for _, col := range cols { + if !s.Hidden[col.ID] { + visible++ + } + } + ui.PopoverBase(c, b, open, func(panel *ui.Element) { + panel.Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).Radius(theme.PopupRadius). + Padding(u * 2).Gap(u).MinWidth(200).MarginY(u) + for _, col := range cols { + ui.Column(c).Key(col.ID).Children(func() { dataTablePickerItem(c, s, col.ID, col.Title, visible) }) + } + }) + return b +} + +// dataTablePickerItem is one column's check box in the picker. +func dataTablePickerItem[K comparable](c *ui.Context, s *DataTableState[K], id, title string, visible int) { + k := Tokens(c) + d := Density(c) + u := d.Unit() + on := !s.Hidden[id] + last := on && visible == 1 + cb := ui.CheckboxBase(c, &on).Disabled(last).MinHeight(d.ControlHeight()). + Padding(0, u*2).Gap(u * 2).AlignItems(ui.Center).Radius(theme.ControlRadius) + internal.Ring(cb, k.Focus, theme.ControlRadius) + if cb.Hovered() && !last { + cb.Background(k.SurfaceHover) + } + if cb.Changed() && !last { + if s.Hidden == nil { + s.Hidden = map[string]bool{} + } + s.Hidden[id] = !on + slog.Debug("mujica: data_table column", "id", id, "visible", on) + } + cb.Children(func() { + dataTableCheckBox(c, on) + ui.Text(c, title).SingleLine() + }) +} + +// dataTableCheckBox draws a 16 DIP check box: OnAccent check on Accent when on. +func dataTableCheckBox(c *ui.Context, on bool) { + k := Tokens(c) + box := ui.Box(c).Size(16, 16).Shrink(0).Radius(theme.ControlRadius).Center() + if on { + box.Background(k.Accent).TextColor(k.OnAccent).Children(func() { + ui.Icon(c, icons.Must("check")).Size(14, 14) + }) + } else { + box.Border(theme.BorderWidth, k.ControlBorder).Background(k.Surface) + } +} diff --git a/data_table_test.go b/data_table_test.go new file mode 100644 index 0000000..b327fee --- /dev/null +++ b/data_table_test.go @@ -0,0 +1,337 @@ +package mujica + +import ( + "cmp" + "fmt" + "strconv" + "testing" + + "github.com/egoist/mygo/ui" +) + +type relic struct { + ID int + Name string + Age int +} + +type tableHarness struct { + rows []relic + st DataTableState[int] + opts DataTableOptions[relic, int] + compares int + changes int + sorts int + picker bool + c *ui.Context + seen map[string]int +} + +func newTableHarness(rows []relic) *tableHarness { + h := &tableHarness{rows: rows} + h.opts = DataTableOptions[relic, int]{ + Key: func(r relic) int { return r.ID }, + Columns: []DataColumn[relic]{ + {ID: "name", Title: "Name", Sortable: true, Text: func(r relic) string { return r.Name }, + Compare: func(a, b relic) int { h.compares++; return cmp.Compare(a.Name, b.Name) }}, + {ID: "age", Title: "Age", Width: 80, Align: ui.End, Sortable: true, Text: func(r relic) string { return strconv.Itoa(r.Age) }, + Compare: func(a, b relic) int { h.compares++; return cmp.Compare(a.Age, b.Age) }}, + }, + } + return h +} + +func (h *tableHarness) view(c *ui.Context) { + Use(c, Settings{Mode: Light}) + h.c, h.seen = c, map[string]int{} + ui.Column(c).Padding(10).Gap(8).Children(func() { + if h.picker { + DataTableColumnPicker(c, &h.st, h.opts.Columns) + } + cols := h.opts + for i := range cols.Columns { + text := cols.Columns[i].Text + cols.Columns[i].Text = func(r relic) string { h.seen[r.Name]++; return text(r) } + } + v := DataTable(c, &h.st, h.rows, cols) + v.Size(360, 300) + if v.Changed() { + h.changes++ + } + if v.SortChanged() { + h.sorts++ + } + }) +} + +func relics() []relic { + return []relic{{1, "Censer", 300}, {2, "Altar", 120}, {3, "Bell", 80}, {4, "Diptych", 450}} +} + +func (h *tableHarness) order(tt *ui.Tester) []string { + var names []string + for _, s := range tt.Texts() { + for _, r := range h.rows { + // Rows are labeled with their name too: skip the repeat. + if r.Name == s && (len(names) == 0 || names[len(names)-1] != s) { + names = append(names, s) + } + } + } + return names +} + +func TestDataTableSortsOnce(t *testing.T) { + h := newTableHarness(relics()) + tt := ui.NewTester(h.view, 400, 360) + if got := fmt.Sprint(h.order(tt)); got != "[Censer Altar Bell Diptych]" { + t.Fatalf("initial order %s", got) + } + if err := tt.Click("Name"); err != nil { + t.Fatal(err) + } + if got := fmt.Sprint(h.order(tt)); got != "[Altar Bell Censer Diptych]" || h.st.Sort != (ui.SortOrder{Column: "name"}) || h.sorts != 1 { + t.Fatalf("ascending %s sort %+v sorts %d", got, h.st.Sort, h.sorts) + } + n := h.compares + tt.Frame() + tt.Frame() + if h.compares != n { + t.Fatalf("sorted again without a change: %d compares", h.compares-n) + } + if err := tt.Click("Age"); err != nil { + t.Fatal(err) + } + if err := tt.Click("Age"); err != nil { + t.Fatal(err) + } + if got := fmt.Sprint(h.order(tt)); got != "[Diptych Censer Altar Bell]" || !h.st.Sort.Descending { + t.Fatalf("age descending %s", got) + } +} + +func TestDataTableSelectionFollowsRow(t *testing.T) { + h := newTableHarness(relics()) + tt := ui.NewTester(h.view, 400, 360) + if err := tt.Click("Bell"); err != nil { + t.Fatal(err) + } + if k, ok := h.st.Selected(); !ok || k != 3 || h.changes != 1 { + t.Fatalf("selected %d %v changes %d", k, ok, h.changes) + } + _ = tt.Click("Name") + _ = tt.Click("Name") + if k, _ := h.st.Selected(); k != 3 || h.rows[h.st.cache.view[h.st.cursor]].Name != "Bell" { + t.Fatalf("after sort selected %d at row %d", k, h.st.cursor) + } + tt.Key(0, ui.KeyDown) + if k, _ := h.st.Selected(); k != 2 { // descending: Diptych Censer Bell Altar + t.Fatalf("Down chose %d", k) + } + h.st.Filter = "ce" + tt.Frame() + if h.st.Rows() != 1 || h.st.cursor != -1 { + t.Fatalf("filter rows %d cursor %d", h.st.Rows(), h.st.cursor) + } + h.st.Filter = "" + tt.Frame() + if k, ok := h.st.Selected(); !ok || k != 2 || h.rows[h.st.cache.view[h.st.cursor]].Name != "Altar" { + t.Fatalf("selection lost through filter: %d %v", k, ok) + } + h.rows = []relic{{1, "Censer", 300}} + tt.Frame() + if _, ok := h.st.Selected(); ok { + t.Fatal("removed row stays selected") + } +} + +func TestDataTableMultipleMarks(t *testing.T) { + h := newTableHarness(relics()) + h.opts.Multiple = true + tt := ui.NewTester(h.view, 400, 360) + table, _ := tt.Find("Name") + for _, name := range []string{"Altar", "Diptych"} { + r, _ := tt.Find(name) + tt.ClickAt(table.X-28, r.Y+r.H/2) // the mark column sits before Name + } + if h.st.Selection.Len() != 2 || !h.st.Selection.Has(2) || !h.st.Selection.Has(4) { + t.Fatalf("marks chose %d", h.st.Selection.Len()) + } + _ = tt.Click("Age") + if h.st.Selection.Len() != 2 || !h.st.Selection.Has(4) { + t.Fatal("selection changed by sorting") + } + h.rows = h.rows[:2] // Censer, Altar + tt.Frame() + if h.st.Selection.Len() != 1 || !h.st.Selection.Has(2) { + t.Fatalf("prune kept %d", h.st.Selection.Len()) + } +} + +func TestDataTableColumns(t *testing.T) { + h := newTableHarness(relics()) + h.picker = true + tt := ui.NewTester(h.view, 400, 400) + h.st.Layout.Order = []string{"age", "name"} + tt.Frame() + age, _ := tt.Find("Age") + name, _ := tt.Find("Name") + if age.X >= name.X { + t.Fatalf("order not applied: age %v name %v", age.X, name.X) + } + if err := tt.Click("Columns"); err != nil { + t.Fatal(err) + } + ages := 0 + for _, s := range tt.Texts() { + if s == "Age" { + ages++ + } + } + if ages != 2 { // header and picker + t.Fatalf("picker missing: %q", tt.Texts()) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeySpace) + if !h.st.Hidden["name"] || tt.HasText("Censer") { + t.Fatalf("name not hidden: %v %q", h.st.Hidden, tt.Texts()) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeySpace) + if h.st.Hidden["age"] { + t.Fatal("hid the last visible column") + } +} + +func TestDataTableCustomCellAndControlled(t *testing.T) { + h := newTableHarness(relics()) + h.opts.Controlled = true + h.opts.Columns[0].Cell = func(r relic) { ui.Text(h.c, "◆ "+r.Name) } + tt := ui.NewTester(h.view, 400, 360) + if !tt.HasText("◆ Altar") || tt.HasText("Altar") && !tt.HasText("◆ Altar") { + t.Fatalf("custom cell missing: %q", tt.Texts()) + } + _ = tt.Click("Name") + first := "" + for _, s := range tt.Texts() { + if len(s) > 4 && s[:4] == "◆ " { + first = s + break + } + } + if h.sorts != 1 || h.compares != 0 || first != "◆ Censer" { + t.Fatalf("controlled sorted locally: sorts %d compares %d first %q", h.sorts, h.compares, first) + } +} + +func TestDataTableBadOptions(t *testing.T) { + for name, mutate := range map[string]func(o *DataTableOptions[relic, int]){ + "no key": func(o *DataTableOptions[relic, int]) { o.Key = nil }, + "no compare": func(o *DataTableOptions[relic, int]) { o.Columns[0].Compare = nil }, + "duplicate id": func(o *DataTableOptions[relic, int]) { o.Columns[1].ID = "name" }, + } { + h := newTableHarness(relics()) + mutate(&h.opts) + func() { + defer func() { + if recover() == nil { + t.Errorf("%s: no panic", name) + } + }() + ui.NewTester(h.view, 300, 300) + }() + } + h := newTableHarness([]relic{{1, "A", 1}, {1, "B", 2}}) + defer func() { + if recover() == nil { + t.Error("duplicate row keys: no panic") + } + }() + ui.NewTester(h.view, 300, 300) +} + +func TestDataTableStatuses(t *testing.T) { + h := newTableHarness(nil) + tt := ui.NewTester(h.view, 400, 360) + if !tt.HasText("No items") { + t.Fatalf("empty: %q", tt.Texts()) + } + h.rows = relics() + h.st.Filter = "zzz" + tt.Frame() + if !tt.HasText("No matching rows") { + t.Fatalf("no match: %q", tt.Texts()) + } + h.st.Filter = "" + h.opts.Status = DataFailed + tt.Frame() + if !tt.HasText("Could not load") || !tt.HasText("Retry") || !tt.HasText("Bell") { + t.Fatalf("failed append: %q", tt.Texts()) + } +} + +func TestDataTableLargeIsVirtualized(t *testing.T) { + rows := make([]relic, 100_000) + for i := range rows { + rows[i] = relic{ID: i, Name: fmt.Sprintf("Relic %06d", (i*7919)%100_000), Age: i % 997} + } + h := newTableHarness(rows) + for i := 3; i <= 10; i++ { + id := fmt.Sprintf("c%d", i) + h.opts.Columns = append(h.opts.Columns, DataColumn[relic]{ID: id, Title: id, Width: 60, Text: func(r relic) string { return id }}) + } + tt := ui.NewTester(h.view, 400, 360) + // Rows whose cells ran in the last frame: 300 DIPs show about 7 rows of 36 DIPs, plus overscan. + const bound = 16 + if len(h.seen) == 0 || len(h.seen) > bound { + t.Fatalf("built %d rows, bound %d", len(h.seen), bound) + } + if err := tt.Click("Name"); err != nil { + t.Fatal(err) + } + if len(h.seen) > bound || !tt.HasText("Relic 000000") || !tt.HasText("Relic 000001") { + t.Fatalf("after sort built %d rows, texts %q", len(h.seen), tt.Texts()) + } + if err := tt.Click("Relic 000001"); err != nil { + t.Fatal(err) + } + n := h.compares + _ = tt.Click("Name") // descending: the row moves 99,998 places + tt.Frame() + if k, _ := h.st.Selected(); k != rowWithName(rows, "Relic 000001") { + t.Fatalf("selection lost: %d", k) + } + m := h.compares + if m == n { + t.Fatal("descending did not sort") + } + tt.Frame() + tt.Frame() + if h.compares != m { + t.Fatal("re-sorted each frame") + } +} + +func rowWithName(rows []relic, name string) int { + for _, r := range rows { + if r.Name == name { + return r.ID + } + } + panic(name) +} + +func TestDataTableSnapshot(t *testing.T) { + snapshot(t, "data_table", 640, 300, func(c *ui.Context) { + h := ui.Local(c.Root(), "h", func() *tableHarness { + h := newTableHarness(relics()) + h.opts.Multiple = true + h.st.Sort = ui.SortOrder{Column: "name"} + h.st.Selection.Add(3) + return h + }) + ui.Column(c).Padding(16).Children(func() { + DataTable(c, &(*h).st, (*h).rows, (*h).opts).Size(600, 260) + }) + }) +} diff --git a/description_list.go b/description_list.go new file mode 100644 index 0000000..3be4d4b --- /dev/null +++ b/description_list.go @@ -0,0 +1,113 @@ +package mujica + +import ( + "log/slog" + "math" + "time" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/theme" +) + +// DescriptionItem is a name and its value. +type DescriptionItem struct { + Label string + Value string + // Span is how many columns the item takes; negative spans the whole row. + Span int + // Copyable adds a button copying Value to the clipboard. + Copyable bool +} + +// DescriptionListOptions configure a DescriptionList. +type DescriptionListOptions struct { + // Columns is the most columns shown; 0 means 2. + Columns int + // MinColumnWidth drops columns when narrower; 0 means 220 DIPs. + MinColumnWidth float32 +} + +// DescriptionListView is a built DescriptionList and what happened to it in this frame. +type DescriptionListView struct { + *ui.Element + copied int +} + +// Copied returns the index of the item whose value was copied in this frame. +func (v DescriptionListView) Copied() (int, bool) { return v.copied, v.copied >= 0 } + +// descriptionCopied shows the copied mark for this long. +const descriptionCopied = 1500 * time.Millisecond + +type descriptionState struct { + item int + until time.Time +} + +// DescriptionList shows names and values in columns, one column when narrow. +func DescriptionList(c *ui.Context, items []DescriptionItem, opts DescriptionListOptions) DescriptionListView { + if opts.Columns < 0 || opts.MinColumnWidth < 0 { + panic("mujica: DescriptionList with negative Columns or MinColumnWidth") + } + most, minW := opts.Columns, opts.MinColumnWidth + if most == 0 { + most = 2 + } + if minW == 0 { + minW = 220 + } + k := Tokens(c) + u := Density(c).Unit() + gap := u * 6 + v := DescriptionListView{Element: ui.Grid(c).GapX(gap).GapY(u * 4).FillWidth(), copied: -1} + st := ui.Local(v.Element, "state", func() descriptionState { return descriptionState{item: -1} }) + fit := func(w float32) int { + if w <= 0 { + return most + } + return max(1, min(most, int(math.Floor(float64((w+gap)/(minW+gap)))))) + } + cols := fit(v.Bounds().W) + v.Draw(func(p *ui.Painter, r ui.Rect) { + if fit(r.W) != cols { + // The new width needs other columns: build again. + c.Invalidate() + } + }) + now := c.Now() + v.Columns(cols).Children(func() { + for i, it := range items { + cell := ui.Column(c).Gap(u).MinWidth(0) + if it.Span < 0 || it.Span > cols { + cell.ColumnSpan(-1) + } else if it.Span > 1 { + cell.ColumnSpan(it.Span) + } + cell.Children(func() { + ui.Text(c, it.Label).FontSize(theme.CaptionSize).TextColor(k.TextMuted) + ui.Row(c).Gap(u).MinHeight(theme.MinHitSize).AlignItems(ui.Center).Children(func() { + ui.Text(c, it.Value).Shrink(1).MinWidth(0) + if !it.Copyable { + return + } + done := st.item == i && now.Before(st.until) + icon, label := icons.Must("copy"), msg(c, "description_list.copy")+" "+it.Label + if done { + icon, label = icons.Must("check"), msg(c, "description_list.copied") + c.After(st.until.Sub(now)) + } + if Button(c, "", ButtonOptions{Variant: Ghost, Icon: icon, Label: label}).MinHeight(theme.MinHitSize).Padding(0, u).Clicked() { + c.WriteClipboard(it.Value) + c.Announce(msg(c, "description_list.copied")) + *st = descriptionState{item: i, until: now.Add(descriptionCopied)} + v.copied = i + slog.Debug("mujica: description_list copy", "item", i) + } + }) + }) + } + }) + return v +} diff --git a/description_list_test.go b/description_list_test.go new file mode 100644 index 0000000..ba7a710 --- /dev/null +++ b/description_list_test.go @@ -0,0 +1,81 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +var descItems = []DescriptionItem{ + {Label: "Archivist", Value: "Sister Agnès", Copyable: true}, + {Label: "Shelf", Value: "North vault, row 7"}, + {Label: "Seal", Value: "Wine-red wax, cracked", Span: -1}, +} + +func TestDescriptionListColumnsAndCopy(t *testing.T) { + width := float32(560) + copied := -1 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Width(width).Children(func() { + if i, ok := DescriptionList(c, descItems, DescriptionListOptions{}).Copied(); ok { + copied = i + } + }) + }, 700, 400) + a, _ := tt.Find("Archivist") + b, _ := tt.Find("Shelf") + seal, _ := tt.Find("Seal") + if a.Y != b.Y || b.X <= a.X || seal.Y <= a.Y { + t.Fatalf("two columns expected: %v %v %v", a, b, seal) + } + if err := tt.Click("Copy Archivist"); err != nil { + t.Fatal(err) + } + if tt.Clipboard() != "Sister Agnès" || copied != 0 { + t.Fatalf("clipboard %q copied %d", tt.Clipboard(), copied) + } + if _, ok := tt.Find("Copied"); !ok { + t.Fatal("copied mark missing") + } + width = 300 + tt.Frame() + a, _ = tt.Find("Archivist") + b, _ = tt.Find("Shelf") + if b.Y <= a.Y || b.X != a.X { + t.Fatalf("narrow width kept columns: %v %v", a, b) + } +} + +func TestDescriptionListSpan(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Width(900).Children(func() { + DescriptionList(c, []DescriptionItem{{Label: "A", Value: "a", Span: 2}, {Label: "B", Value: "b"}, {Label: "C", Value: "c"}}, DescriptionListOptions{Columns: 3}) + }) + }, 950, 300) + a, _ := tt.Find("a") + b, _ := tt.Find("b") + cc, _ := tt.Find("c") + if b.Y != a.Y || cc.Y <= a.Y { + t.Fatalf("span 2 of 3: a %v b %v c %v", a, b, cc) + } +} + +func TestDescriptionListBadOptions(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + DescriptionList(c, nil, DescriptionListOptions{Columns: -1}) + }, 100, 100) +} + +func TestDescriptionListSnapshot(t *testing.T) { + snapshot(t, "description_list", 560, 200, func(c *ui.Context) { + ui.Column(c).Padding(20).Children(func() { DescriptionList(c, descItems, DescriptionListOptions{}) }) + }) +} diff --git a/docs/components/069-card.md b/docs/components/069-card.md new file mode 100644 index 0000000..e959146 --- /dev/null +++ b/docs/components/069-card.md @@ -0,0 +1,46 @@ +# 069 Card · 卡片 + +## 用途 + +把一组相关内容放在带描边的表面上:标题、内容与页脚三个插槽。默认不增加点击行为;可选 `Decorated` 绘制宫廷风格的双线边框与四角菱形角饰。Card 是视觉回归基准之一,快照覆盖浅深主题与三种密度。 + +## 最小示例 + +```go +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Card(c, mujica.CardOptions{Title: "Archive", Decorated: true, Footer: func() { + mujica.Button(c, "Open", mujica.ButtonOptions{}) + }}, func() { + ui.Text(c, "Letters of the court, sealed in wax.") + }).Width(280) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func Card(c *ui.Context, opts CardOptions, body func()) *ui.Element`,`body` 构建内容插槽。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Title` | `string` | 空 | 衬线标题;为空时不显示标题区。同时作为分组的无障碍名称。 | +| `Footer` | `func()` | nil | 页脚插槽,位于分隔线下方并右对齐,常放操作按钮。 | +| `Decorated` | `bool` | false | 外框与内框双线、四角菱形与标题下菱形分隔线,均使用 Ornament 色。 | + +## 状态 + +默认(Surface 表面 + Border 描边,4 DIP 圆角)、装饰(Ornament 双线与角饰)。卡片本身无悬停、按下、选中或禁用状态;内部控件保留各自状态。 + +## 事件 + +无。卡片不产生事件,交互来自内部控件。 + +## 键盘操作 + +卡片不获取焦点;Tab 按顺序进入内部控件。装饰只做绘制,不占焦点、不遮挡内容、不缩小点击区域。 + +## 限制 + +- 卡片不提供整卡点击;需要时由调用方在内容中放置按钮或链接。 +- 角饰为矢量绘制,位于 4 DIP 内框的四角;装饰模式会额外增加 6 DIP 内边距。 diff --git a/docs/components/070-list.md b/docs/components/070-list.md new file mode 100644 index 0000000..48dc71e --- /dev/null +++ b/docs/components/070-list.md @@ -0,0 +1,62 @@ +# 070 List · 列表 + +## 用途 + +展示任意数量的条目,只构建可见行(加预取两行),10 万条记录也能流畅滚动、选择。支持列表与网格两种模式、单选与多选、稳定条目键、按标签输入定位,以及首次加载、追加加载、失败与空状态。 + +## 最小示例 + +```go +names := []string{"Matins", "Lauds", "Prime", "Terce"} +var state mujica.ListState[string] +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + v := mujica.List(c, &state, len(names), mujica.ListOptions[string]{ + Key: func(i int) string { return names[i] }, + Label: func(i int) string { return names[i] }, + }, func(i int) { ui.Text(c, names[i]) }) + v.Size(240, 200) + if v.Changed() { + key, _ := state.Selected() + fmt.Println("chose", key) + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func List[K comparable](c *ui.Context, s *ListState[K], n int, opts ListOptions[K], item func(i int)) ListView`。`ListView` 嵌入 `*ui.Element`;`ListState` 由调用方持有,零值表示未选择。`state.Selected()` 返回所选键,多选时 `state.Selection` 保存全部键;`state.ScrollTo(i)` 滚动到第 i 项。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Key` | `func(i int) K` | 必填 | 条目的稳定键;选择与行状态跟随键,而不是下标。 | +| `Label` | `func(i int) string` | 必填 | 条目文字:无障碍名称与输入定位。 | +| `Multiple` | `bool` | false | 多选:Cmd/Ctrl 点击增减、Shift 连选、Cmd/Ctrl+A 全选。 | +| `Grid` | `bool` | false | 网格模式:按 `ItemWidth` 自动分列。 | +| `ItemWidth`, `ItemHeight` | `float32` | 160, 120 | 网格条目最小宽度与高度(DIP)。 | +| `Status` | `DataStatus` | `DataReady` | 加载状态:`DataLoading` 首次加载(无行时显示加载标记)、`DataLoadingMore` 追加加载(行下方显示“正在加载更多”)、`DataFailed` 失败(显示错误与“重试”)。 | +| `Error` | `string` | 内置“加载失败” | `DataFailed` 时显示的具体错误。 | +| `Empty` | `string` | 内置“暂无数据” | 无数据时的说明文字。 | + +## 状态 + +默认、悬停(SurfaceHover)、选中(Accent 填充 + OnAccent 文字 + 勾选标记)、聚焦(列表外 2 DIP 焦点环)、网格选中(AccentText 边框 + 勾选)、加载、追加加载、失败、空。 + +## 事件 + +`ListView.Changed()`:选择变化;`Submitted()`:Enter 或双击;`Retried()`:点击失败状态的“重试”。 + +## 键盘操作 + +- ↑/↓、Home/End,Linux 与 Windows 上 PageUp/PageDown:移动选择;网格模式下方向键二维移动。 +- 输入字母:按 `Label` 定位。 +- Enter:提交。 +- 多选:Shift+方向键连选;Cmd/Ctrl+A 全选;Linux/Windows 上 Ctrl+方向键只移动、Ctrl+Space 切换。 + +## 限制 + +- 虚拟化由 MyGo `ui.List`/`ui.GridView` 提供:每帧构建的元素数由可见区域和预取范围决定;程序跳转(`ScrollTo`)时目标两侧各构建一屏。测试以 10 万条记录统计“最后一帧调用条目构建函数的次数”验证上限(list_test.go)。 +- MyGo 单选跟随键时只在原位置附近 1000 行内查找(ui/list.go:30 `listSearch`);插入或删除大量上方行后,单选可能被清除。 +- Shift 连选的锚点由 MyGo 决定(ui/list.go `extend`),与平台习惯一致。 +- 网格模式没有 `ListState.Visible` 等滚动位置读取接口。 diff --git a/docs/components/071-data-table.md b/docs/components/071-data-table.md new file mode 100644 index 0000000..dce0755 --- /dev/null +++ b/docs/components/071-data-table.md @@ -0,0 +1,73 @@ +# 071 DataTable · 数据表格 + +## 用途 + +按列展示数据行,只构建可见行。提供排序、筛选、多选、列宽、列顺序、列显隐与自定义单元格。行与列都以稳定 ID 保存状态:排序、筛选或调整列之后,选择仍指向同一条数据。默认使用本地排序与筛选(仅在数据、排序或筛选条件变化时计算一次并缓存);`Controlled` 模式下按调用方顺序显示,调用方在 `SortChanged()` 时自行查询。 + +## 最小示例 + +```go +type relic struct { + ID int + Name string + Age int +} +rows := []relic{{1, "Censer", 300}, {2, "Altar", 120}, {3, "Bell", 80}} +var state mujica.DataTableState[int] +opts := mujica.DataTableOptions[relic, int]{ + Key: func(r relic) int { return r.ID }, + Columns: []mujica.DataColumn[relic]{ + {ID: "name", Title: "Name", Sortable: true, + Text: func(r relic) string { return r.Name }, + Compare: func(a, b relic) int { return cmp.Compare(a.Name, b.Name) }}, + {ID: "age", Title: "Age", Width: 80, Align: ui.End, Sortable: true, + Text: func(r relic) string { return strconv.Itoa(r.Age) }, + Compare: func(a, b relic) int { return cmp.Compare(a.Age, b.Age) }}, + }, +} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.DataTable(c, &state, rows, opts).Size(360, 240) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func DataTable[R any, K comparable](c *ui.Context, s *DataTableState[K], rows []R, opts DataTableOptions[R, K]) DataTableView`。另有 `DataTableColumnPicker(c, s, cols)`(“列”按钮与勾选弹层,控制显隐)和纯函数 `DataTableRows(rows, cols, sort, filter) []int`(本地排序筛选辅助,返回行下标)。 + +`DataTableState` 字段:`Sort ui.SortOrder`、`Filter string`(不区分大小写包含匹配全部列的 `Text`)、`Hidden map[string]bool`(按列 ID 隐藏)、`Layout ui.TableLayout`(用户拖动得到的列顺序与列宽,可保存恢复)、`Selection ui.Selection[K]`(多选)。方法:`Selected()`、`Rows()`(筛选后行数)。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Columns` | `[]DataColumn[R]` | 必填 | 列定义:`ID`(必填、唯一)、`Title`、`Width`(0 表示分配剩余宽度)、`MinWidth`、`Align`、`Sortable`、`Compare`(本地排序比较函数)、`Text`(必填,纯文本,用于筛选与默认单元格)、`Cell`(自定义单元格)。 | +| `Key` | `func(R) K` | 必填 | 行的稳定 ID;重复时 panic。 | +| `Multiple` | `bool` | false | 多选:首列显示勾选框,点击勾选框增减,Cmd/Ctrl、Shift 点击与 Cmd/Ctrl+A。 | +| `Controlled` | `bool` | false | 受控模式:不在本地排序筛选。 | +| `Version` | `int` | 0 | 原地修改 `rows` 后递增,通知缓存失效;换新切片无需递增。 | +| `Status` | `DataStatus` | `DataReady` | 加载状态:`DataLoading` 首次加载(无行时显示加载标记)、`DataLoadingMore` 追加加载(行下方显示“正在加载更多”)、`DataFailed` 失败(显示错误与“重试”)。 | +| `Error` | `string` | 内置“加载失败” | `DataFailed` 时显示的具体错误。 | +| `Empty` | `string` | 内置“暂无数据” | 无数据时的说明文字。 | + +## 状态 + +默认、表头悬停、排序箭头(升/降)、行悬停、选中行(Accent 填充 + OnAccent 文字;单选首列菱形,多选首列勾选框)、聚焦(表格外焦点环)、加载、追加加载、失败、空、无匹配。数据行高度不低于当前密度的数据行高(28/36/44 DIP)。 + +## 事件 + +`Changed()`:选择变化;`Submitted()`:Enter 或双击;`SortChanged()`:点击表头改变了 `State.Sort`;`Retried()`:点击“重试”。 + +## 键盘操作 + +- ↑/↓、Home/End:移动选择;Enter:提交;输入字母按首个可见列定位。 +- 多选:Shift/Cmd(Ctrl) 配合点击或方向键;Cmd/Ctrl+A 全选。 +- 表头:点击排序、再次点击反向;拖动表头边缘调整列宽,双击适配内容;拖动表头移动列。 +- 列选择器:Tab 进入勾选项,Space 切换;Escape 关闭。 + +## 限制 + +- MyGo `ui.Table` 只按调用方给出的顺序显示(docs/ui/table.md “Sorting”),因此本组件在本地完成排序,或在受控模式下交给调用方。 +- 表头内容只能是 `Title` 文字(ui/table.go `table` 构建表头),无法放置全选勾选框;全选使用 Cmd/Ctrl+A。 +- 单选的跟随由组件按键重新定位,跨越任意距离都有效;多选的键盘锚点在大规模重排后由 MyGo 重置(ui/list.go:30)。 +- 删除的数据行对应的选择会在数据变化时清除;被筛选隐藏的行保留选择。 +- 10 万行 × 10 列:测试统计“最后一帧构建过单元格的行数”不超过 16,并断言重复帧不再调用比较函数(data_table_test.go)。 diff --git a/docs/components/072-tree.md b/docs/components/072-tree.md new file mode 100644 index 0000000..5e37cb8 --- /dev/null +++ b/docs/components/072-tree.md @@ -0,0 +1,57 @@ +# 072 Tree · 树 + +## 用途 + +展示层级数据:节点展开、选择、键盘导航、外部加载子项与空状态。只构建可见行,10 万节点同样流畅;折叠的子树不创建任何界面元素。 + +## 最小示例 + +```go +kids := map[string][]string{"Nave": {"Pulpit", "Pews"}} +var state mujica.TreeState[string] +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Tree(c, &state, mujica.TreeOptions[string]{ + Roots: []string{"Nave", "Sacristy"}, + Children: func(n string) []string { return kids[n] }, + Label: func(n string) string { return n }, + }).Size(280, 240) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func Tree[K comparable](c *ui.Context, s *TreeState[K], opts TreeOptions[K]) TreeView`。节点值本身即稳定键。`TreeState` 方法:`Selected()`、`SetOpen(item, open)`、`IsOpen(item)`;字段 `Selection ui.Selection[K]` 保存多选。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Roots` | `[]K` | 空 | 顶层节点;为空时显示空状态。 | +| `Children` | `func(K) []K` | 必填 | 子节点:nil 表示叶子;未加载的节点返回空的非 nil 切片。 | +| `Label` | `func(K) string` | 必填 | 节点文字。 | +| `ItemStatus` | `func(K) DataStatus` | nil | 子项加载状态:`DataUnloaded`、`DataLoading`(行尾加载标记)、`DataFailed`(行尾错误与重试);nil 表示全部已加载。 | +| `Load` | `func(K)` | nil | 节点以 `DataUnloaded` 状态展开时调用一次,点击重试时再次调用。 | +| `Multiple` | `bool` | false | 多选。 | +| `Status` | `DataStatus` | `DataReady` | 加载状态:`DataLoading` 首次加载(无行时显示加载标记)、`DataLoadingMore` 追加加载(行下方显示“正在加载更多”)、`DataFailed` 失败(显示错误与“重试”)。 | +| `Error` | `string` | 内置“加载失败” | `DataFailed` 时显示的具体错误。 | +| `Empty` | `string` | 内置“暂无数据” | 无数据时的说明文字。 | + +## 状态 + +默认、悬停、选中(Accent 填充 + 勾选标记)、聚焦、展开/折叠(箭头旋转)、子项加载中、子项加载失败、空、首次加载与失败。 + +## 事件 + +`Changed()`:选择变化;`Submitted()`:Enter 或双击;`Retried()`:整树失败时点击重试。子项重试直接调用 `Load`。 + +## 键盘操作 + +- ↑/↓:移动;→:展开或进入第一个子节点;←:折叠或回到父节点;macOS 上 Option、其他平台 Shift 配合 ←/→ 展开或折叠全部子孙。 +- 输入字母按 `Label` 定位;Enter 提交。 + +## 限制 + +- 基于 MyGo `ui.Outline` 而不是 `ui.Tree`:`ui.Tree` 会构建全部已展开节点(docs/ui/tree.md “A tree builds every item it shows”),不满足大量节点虚拟化要求。 +- `Outline` 每帧遍历已展开的节点以计算行(ui/outline.go `flatten`),元素数受可见区域限制,但遍历成本随展开节点数增长。 +- 单选跟随节点的搜索范围为 1000 行(ui/list.go:30)。 +- 异步加载由调用方在后台完成并通过 `Window.Update` 回到界面线程。 diff --git a/docs/components/073-tree-table.md b/docs/components/073-tree-table.md new file mode 100644 index 0000000..60d8799 --- /dev/null +++ b/docs/components/073-tree-table.md @@ -0,0 +1,61 @@ +# 073 TreeTable · 树形表格 + +## 用途 + +把树形层级与表格列结合:首列显示展开箭头与缩进,其余列显示属性。支持展开、选择、列宽与列顺序、排序状态(同级节点在本地排序,结果按父节点缓存,只在子项或排序变化时重新计算),以及外部加载子项。只构建可见行。 + +## 最小示例 + +```go +kids := map[string][]string{"Vestry": {"Stole", "Alb"}} +size := map[string]int{"Vestry": 9, "Stole": 2, "Alb": 5, "Organ": 7} +var state mujica.TreeTableState[string] +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.TreeTable(c, &state, mujica.TreeTableOptions[string]{ + Roots: []string{"Vestry", "Organ"}, + Children: func(n string) []string { return kids[n] }, + Label: func(n string) string { return n }, + Columns: []mujica.TreeColumn[string]{ + {ID: "name", Title: "Name", Cell: func(n string) { ui.Text(c, n) }, + Compare: cmp.Compare[string]}, + {ID: "size", Title: "Size", Width: 80, Align: ui.End, + Cell: func(n string) { ui.Text(c, strconv.Itoa(size[n])) }}, + }, + }).Size(360, 240) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func TreeTable[K comparable](c *ui.Context, s *TreeTableState[K], opts TreeTableOptions[K]) DataTableView`。`TreeTableState` 字段:`Sort`、`Layout`、`Selection`;方法:`Selected()`、`SetOpen`、`IsOpen`。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Columns` | `[]TreeColumn[K]` | 必填 | 列:`ID`(必填、唯一)、`Title`、`Width`、`MinWidth`、`Align`、`Compare`(设置后可点击排序同级节点)、`Cell`(必填)。首列在箭头后显示 `Cell`。 | +| `Roots` | `[]K` | 空 | 顶层节点。 | +| `Children` | `func(K) []K` | 必填 | 子节点:nil 为叶子,空切片为未加载。子项变化时请返回新切片(排序缓存按切片识别)。 | +| `Label` | `func(K) string` | 必填 | 无障碍名称与输入定位。 | +| `ItemStatus`, `Load` | | nil | 与 Tree 相同的外部加载协议。 | +| `Multiple` | `bool` | false | 多选。 | +| `Status` | `DataStatus` | `DataReady` | 加载状态:`DataLoading` 首次加载(无行时显示加载标记)、`DataLoadingMore` 追加加载(行下方显示“正在加载更多”)、`DataFailed` 失败(显示错误与“重试”)。 | +| `Error` | `string` | 内置“加载失败” | `DataFailed` 时显示的具体错误。 | +| `Empty` | `string` | 内置“暂无数据” | 无数据时的说明文字。 | + +## 状态 + +默认、悬停、选中(Accent 填充 + 勾选标记)、聚焦、展开/折叠、排序箭头、子项加载中/失败、空。 + +## 事件 + +返回 `DataTableView`:`Changed()`、`Submitted()`、`SortChanged()`、`Retried()`。 + +## 键盘操作 + +- 与 Tree 相同的方向键与展开键;点击表头排序同级节点,再次点击反向;拖动表头边缘或表头调整列宽与顺序。 + +## 限制 + +- 基于 MyGo `ui.OutlineTable`;表头只能显示文字。 +- 排序只作用于同级节点,层级关系保持不变。 diff --git a/docs/components/074-accordion.md b/docs/components/074-accordion.md new file mode 100644 index 0000000..5d02cdc --- /dev/null +++ b/docs/components/074-accordion.md @@ -0,0 +1,47 @@ +# 074 Accordion · 手风琴 + +## 用途 + +多个可展开的区段放在一个带边框的盒子中。支持单项或多项展开、是否允许全部关闭,展开状态由调用方持有(受控)。 + +## 最小示例 + +```go +open := []string{"choir"} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Accordion(c, &open, []mujica.AccordionItem{ + {ID: "choir", Title: "Choir", Content: func() { ui.Text(c, "Twelve voices.") }}, + {ID: "nave", Title: "Nave", Content: func() { ui.Text(c, "Black oak pews.") }}, + }, mujica.AccordionOptions{}) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func Accordion(c *ui.Context, open *[]string, items []AccordionItem, opts AccordionOptions) AccordionView`。`*open` 是展开区段的 ID 列表;`AccordionItem` 字段:`ID`(必填、唯一)、`Title`、`Content`(必填)、`Disabled`。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Multiple` | `bool` | false | 允许同时展开多项;否则展开一项会关闭其他项。 | +| `KeepOne` | `bool` | false | 最后一个展开项不能被关闭。 | + +## 状态 + +默认、悬停、聚焦、展开(箭头翻转,内容 120 ms 展开;减少动画时直接切换)、禁用(不展开,退出 Tab 序列)。 + +## 事件 + +`AccordionView.Changed()`:某区段因用户操作而展开或关闭,`*open` 已更新。外部直接修改 `*open` 不触发事件。 + +## 键盘操作 + +- Tab:到达标题;标题打开时 Tab 进入其内容。 +- ↑/↓:上/下一个标题(跳过禁用项);Home/End:第一个/最后一个。 +- Enter/Space:展开或关闭。 + +## 限制 + +- 基于 MyGo `ui.CollapsibleBase`;MyGo 的 `ui.Accordion` 外观固定且用内部状态移动焦点(ui/collapsible.go `accordionKeys`),因此键盘移动在组件内重新实现:向上移动时焦点在下一帧到达。 +- 关闭动画进行的 120 ms 内内容仍在界面树中。 diff --git a/docs/components/075-collapsible.md b/docs/components/075-collapsible.md new file mode 100644 index 0000000..12b3cbe --- /dev/null +++ b/docs/components/075-collapsible.md @@ -0,0 +1,45 @@ +# 075 Collapsible · 折叠面板 + +## 用途 + +一个触发区控制下方内容的显示。展开状态由调用方的 `*bool` 控制,可在外部打开或关闭;关闭后的内容不构建,因此退出焦点序列。 + +## 最小示例 + +```go +open := false +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Collapsible(c, &open, mujica.CollapsibleOptions{Title: "Details"}, func() { + ui.Text(c, "Candles are lit at the seventh bell.") + }) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func Collapsible(c *ui.Context, open *bool, opts CollapsibleOptions, content func()) CollapsibleView`。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Title` | `string` | 空 | 触发区文字;与 `Trigger` 至少设置一个。 | +| `Trigger` | `func()` | nil | 自定义触发区内容(箭头之后)。 | +| `Disabled` | `bool` | false | 禁止展开或关闭,退出 Tab 序列。 | + +## 状态 + +默认、悬停、按下、聚焦、展开(箭头旋转 90°,120 ms)、禁用。 + +## 事件 + +`CollapsibleView.Changed()`:用户点击或按键切换了展开状态。 + +## 键盘操作 + +- Enter/Space:展开或关闭。 +- 关闭后内容不在 Tab 序列中。 + +## 限制 + +- MyGo `CollapsibleBase` 自带 200 ms 动画计时(ui/collapsible.go),组件使用自己的 120 ms 进度绘制,但 MyGo 的计时仍会在 200 ms 内请求帧。 diff --git a/docs/components/076-timeline.md b/docs/components/076-timeline.md new file mode 100644 index 0000000..5512437 --- /dev/null +++ b/docs/components/076-timeline.md @@ -0,0 +1,44 @@ +# 076 Timeline · 时间线 + +## 用途 + +纵向列出事件:时间、标题、内容与状态。标记之间以细线连接,长内容自动换行。状态同时用形状、图标和文字表示,不只依赖颜色。 + +## 最小示例 + +```go +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Timeline(c, []mujica.TimelineItem{ + {Time: "06:00", Title: "Matins sung", Status: mujica.TimelineDone}, + {Time: "09:00", Title: "Relics moved", Content: "From the crypt to the altar.", Status: mujica.TimelineActive}, + {Time: "18:00", Title: "Vespers"}, + }) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func Timeline(c *ui.Context, items []TimelineItem) *ui.Element`。`TimelineItem` 字段:`Time`、`Title`、`Content`、`Status`。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Status` | `TimelineStatus` | `TimelineDefault` | `TimelineDefault`(空心菱形)、`TimelineDone`(勾选图标 + “已完成”)、`TimelineActive`(实心菱形 + “进行中”)、`TimelineWarning`(警告图标 + “警告”)、`TimelineError`(错误图标 + “错误”)。 | + +## 状态 + +仅展示:默认、完成、进行中、警告、错误。 + +## 事件 + +无。 + +## 键盘操作 + +不获取焦点。 + +## 限制 + +- 仅纵向排列;内容为纯文本。 +- 不虚拟化,适合几十条以内的事件。 diff --git a/docs/components/077-description-list.md b/docs/components/077-description-list.md new file mode 100644 index 0000000..5fce41d --- /dev/null +++ b/docs/components/077-description-list.md @@ -0,0 +1,45 @@ +# 077 DescriptionList · 描述列表 + +## 用途 + +以名称与值的形式展示详情,支持多列、跨列与复制值。容器宽度不足时自动减少列数,最窄时为单列。 + +## 最小示例 + +```go +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.DescriptionList(c, []mujica.DescriptionItem{ + {Label: "Archivist", Value: "Sister Agnès", Copyable: true}, + {Label: "Shelf", Value: "North vault, row 7"}, + {Label: "Seal", Value: "Wine-red wax", Span: -1}, + }, mujica.DescriptionListOptions{}) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func DescriptionList(c *ui.Context, items []DescriptionItem, opts DescriptionListOptions) DescriptionListView`。`DescriptionItem` 字段:`Label`、`Value`、`Span`(跨列数,负数跨整行)、`Copyable`。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Columns` | `int` | 2 | 最多列数。 | +| `MinColumnWidth` | `float32` | 220 | 每列最小宽度(DIP);容器放不下时减少列数。 | + +## 状态 + +默认、复制按钮悬停与聚焦、已复制(勾选图标 1.5 秒,并向读屏软件播报“已复制”)。 + +## 事件 + +`DescriptionListView.Copied()` 返回本帧被复制项的下标。 + +## 键盘操作 + +- Tab 到达每个复制按钮(名称为“复制 + 标签”),Enter/Space 复制。 + +## 限制 + +- 列数根据上一帧的宽度计算,宽度变化后在下一帧重新排列(组件会立即请求该帧)。 +- 复制使用 MyGo 剪贴板接口,只写入纯文本。 diff --git a/docs/components/078-statistic.md b/docs/components/078-statistic.md new file mode 100644 index 0000000..d04bcc0 --- /dev/null +++ b/docs/components/078-statistic.md @@ -0,0 +1,50 @@ +# 078 Statistic · 统计数值 + +## 用途 + +突出显示一个数值,附带标题、单位、前后缀、精度与变化值说明。组件只格式化调用方给出的数值,不计算业务指标。 + +## 最小示例 + +```go +change := 12.5 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Statistic(c, 12840, mujica.StatisticOptions{ + Title: "Pilgrims", Unit: "souls", Precision: 0, + Change: &change, ChangeUnit: "%", ChangeNote: "vs last feast", + }) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func Statistic(c *ui.Context, value float64, opts StatisticOptions) *ui.Element`。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Title` | `string` | 空 | 数值上方的标题。 | +| `Precision` | `int` | 0 | 小数位数;负数 panic。 | +| `Prefix`, `Suffix` | `string` | 空 | 紧贴数字的前后缀,如 `£`、`k`。 | +| `Unit` | `string` | 空 | 数字后的较小单位文字。 | +| `Format` | `func(float64) string` | 千分位分组 | 自定义数字格式。 | +| `Change` | `*float64` | nil | 变化值:正数显示上箭头与 Success 色,负数下箭头与 Danger 色,零为持平;同时带正负号。 | +| `ChangeUnit` | `string` | 空 | 变化值单位,如 `%`。 | +| `ChangeNote` | `string` | 空 | 变化值说明,如“较上次”。 | + +## 状态 + +仅展示:上升、下降、持平(图标、符号与颜色共同表示)。 + +## 事件 + +无。 + +## 键盘操作 + +不获取焦点;分组名称把标题、数值、单位与变化一起读出。 + +## 限制 + +- 默认格式使用逗号分组与点号小数;其他地区格式通过 `Format` 提供。 diff --git a/docs/components/079-image-viewer.md b/docs/components/079-image-viewer.md new file mode 100644 index 0000000..c2a038c --- /dev/null +++ b/docs/components/079-image-viewer.md @@ -0,0 +1,48 @@ +# 079 ImageViewer · 图片查看器 + +## 用途 + +在窗口上方的模态层中查看图片:放大、缩小、平移、适应窗口、上一张/下一张,以及键盘操作与关闭。 + +## 最小示例 + +```go +picture := ui.MustParseSVG([]byte(``)) +open, index := true, 0 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.ImageViewer(c, &open, &index, []mujica.ViewerImage{{Source: picture, Alt: "Rose window"}}) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func ImageViewer(c *ui.Context, open *bool, index *int, images []ViewerImage) ImageViewerView`。`ViewerImage` 字段:`Source`(`*ui.Bitmap` 或 `*ui.SVG`,必填)、`Alt`(说明文字与无障碍名称)。关闭时返回的 `Element` 为 nil。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `open` | `*bool` | 必填 | 为 true 时显示;Escape、点击遮罩或关闭按钮设为 false。 | +| `index` | `*int` | 必填 | 当前图片下标;越界 panic。 | +| `images` | `[]ViewerImage` | 必填 | 至少一张。 | + +## 状态 + +缩放 25%–800%(相对适应窗口),按钮在边界处禁用;适应窗口时“适应”按钮禁用;首张/末张时上一张/下一张禁用。 + +## 事件 + +`ImageViewerView.Changed()`:切换了图片,`*index` 已更新。关闭通过 `*open` 变为 false 观察。 + +## 键盘操作 + +- + / -:放大、缩小;0:适应窗口。 +- ←/→:上一张、下一张;Shift+方向键:平移。 +- 滚轮:缩放;拖动图片:平移。 +- Escape:关闭;Tab 在工具按钮与图片之间移动。 + +## 限制 + +- 基于 MyGo `ui.DialogBase`,焦点限制与关闭后的焦点恢复由 MyGo 处理。 +- 图片解码由调用方完成(`ui.DecodeBitmap`),组件不读取文件或网络。 +- 不支持旋转与触控板捏合手势。 diff --git a/docs/components/080-carousel.md b/docs/components/080-carousel.md new file mode 100644 index 0000000..51cc923 --- /dev/null +++ b/docs/components/080-carousel.md @@ -0,0 +1,49 @@ +# 080 Carousel · 走马灯 + +## 用途 + +逐张展示内容,提供上一项、下一项与位置指示。默认不自动播放;开启自动播放后提供暂停按钮,并在指针悬停或焦点位于组件内时暂停。 + +## 最小示例 + +```go +index := 0 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Carousel(c, &index, 3, mujica.CarouselOptions{Label: "Windows", Autoplay: true}, func(i int) { + ui.Text(c, fmt.Sprintf("Rose window %d", i+1)).Padding(24) + }).Size(360, 220) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func(i int)) CarouselView`。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Label` | `string` | 空 | 无障碍名称。 | +| `Autoplay` | `bool` | false | 每隔 `Interval` 前进一项(循环)。 | +| `Interval` | `time.Duration` | 5s | 自动播放间隔;负数 panic。 | +| `Loop` | `bool` | false | 首尾相接;自动播放总是循环。 | + +## 状态 + +默认、按钮悬停/按下/聚焦、首尾禁用(未循环时)、当前位置(实心菱形)、自动播放中、已暂停(显示继续按钮)。 + +## 事件 + +`CarouselView.Changed()`:当前项变化,`*index` 已更新;同时向读屏软件播报“第 N 项,共 M 项”。 + +## 键盘操作 + +- 焦点位于组件内时:←/→ 上一项、下一项;Home/End 第一项、最后一项。 +- 位置指示为一个焦点组:方向键在指示点间移动,Enter/Space 跳转。 +- Tab 依次到达上一项、下一项、位置指示与暂停按钮。 + +## 限制 + +- 切换不做滑动动画,直接显示新的一项。 +- 不自动播放时没有计时器;自动播放只在可见帧中通过 `Context.After` 预约下一帧。 +- `*index` 超出范围时会被夹到有效范围(记录 debug 日志),例如条目数减少后。 diff --git a/example_data_test.go b/example_data_test.go new file mode 100644 index 0000000..a79859c --- /dev/null +++ b/example_data_test.go @@ -0,0 +1,182 @@ +package mujica_test + +import ( + "cmp" + "fmt" + "strconv" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +func ExampleCard() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Card(c, mujica.CardOptions{Title: "Archive", Decorated: true, Footer: func() { + mujica.Button(c, "Open", mujica.ButtonOptions{}) + }}, func() { + ui.Text(c, "Letters of the court, sealed in wax.") + }).Width(280) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleList() { + names := []string{"Matins", "Lauds", "Prime", "Terce"} + var state mujica.ListState[string] + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + v := mujica.List(c, &state, len(names), mujica.ListOptions[string]{ + Key: func(i int) string { return names[i] }, + Label: func(i int) string { return names[i] }, + }, func(i int) { ui.Text(c, names[i]) }) + v.Size(240, 200) + if v.Changed() { + key, _ := state.Selected() + fmt.Println("chose", key) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDataTable() { + type relic struct { + ID int + Name string + Age int + } + rows := []relic{{1, "Censer", 300}, {2, "Altar", 120}, {3, "Bell", 80}} + var state mujica.DataTableState[int] + opts := mujica.DataTableOptions[relic, int]{ + Key: func(r relic) int { return r.ID }, + Columns: []mujica.DataColumn[relic]{ + {ID: "name", Title: "Name", Sortable: true, + Text: func(r relic) string { return r.Name }, + Compare: func(a, b relic) int { return cmp.Compare(a.Name, b.Name) }}, + {ID: "age", Title: "Age", Width: 80, Align: ui.End, Sortable: true, + Text: func(r relic) string { return strconv.Itoa(r.Age) }, + Compare: func(a, b relic) int { return cmp.Compare(a.Age, b.Age) }}, + }, + } + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.DataTable(c, &state, rows, opts).Size(360, 240) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTree() { + kids := map[string][]string{"Nave": {"Pulpit", "Pews"}} + var state mujica.TreeState[string] + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Tree(c, &state, mujica.TreeOptions[string]{ + Roots: []string{"Nave", "Sacristy"}, + Children: func(n string) []string { return kids[n] }, + Label: func(n string) string { return n }, + }).Size(280, 240) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTreeTable() { + kids := map[string][]string{"Vestry": {"Stole", "Alb"}} + size := map[string]int{"Vestry": 9, "Stole": 2, "Alb": 5, "Organ": 7} + var state mujica.TreeTableState[string] + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.TreeTable(c, &state, mujica.TreeTableOptions[string]{ + Roots: []string{"Vestry", "Organ"}, + Children: func(n string) []string { return kids[n] }, + Label: func(n string) string { return n }, + Columns: []mujica.TreeColumn[string]{ + {ID: "name", Title: "Name", Cell: func(n string) { ui.Text(c, n) }, + Compare: cmp.Compare[string]}, + {ID: "size", Title: "Size", Width: 80, Align: ui.End, + Cell: func(n string) { ui.Text(c, strconv.Itoa(size[n])) }}, + }, + }).Size(360, 240) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAccordion() { + open := []string{"choir"} + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Accordion(c, &open, []mujica.AccordionItem{ + {ID: "choir", Title: "Choir", Content: func() { ui.Text(c, "Twelve voices.") }}, + {ID: "nave", Title: "Nave", Content: func() { ui.Text(c, "Black oak pews.") }}, + }, mujica.AccordionOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCollapsible() { + open := false + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Collapsible(c, &open, mujica.CollapsibleOptions{Title: "Details"}, func() { + ui.Text(c, "Candles are lit at the seventh bell.") + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTimeline() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Timeline(c, []mujica.TimelineItem{ + {Time: "06:00", Title: "Matins sung", Status: mujica.TimelineDone}, + {Time: "09:00", Title: "Relics moved", Content: "From the crypt to the altar.", Status: mujica.TimelineActive}, + {Time: "18:00", Title: "Vespers"}, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDescriptionList() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.DescriptionList(c, []mujica.DescriptionItem{ + {Label: "Archivist", Value: "Sister Agnès", Copyable: true}, + {Label: "Shelf", Value: "North vault, row 7"}, + {Label: "Seal", Value: "Wine-red wax", Span: -1}, + }, mujica.DescriptionListOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleStatistic() { + change := 12.5 + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Statistic(c, 12840, mujica.StatisticOptions{ + Title: "Pilgrims", Unit: "souls", Precision: 0, + Change: &change, ChangeUnit: "%", ChangeNote: "vs last feast", + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleImageViewer() { + picture := ui.MustParseSVG([]byte(``)) + open, index := true, 0 + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.ImageViewer(c, &open, &index, []mujica.ViewerImage{{Source: picture, Alt: "Rose window"}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCarousel() { + index := 0 + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Carousel(c, &index, 3, mujica.CarouselOptions{Label: "Windows", Autoplay: true}, func(i int) { + ui.Text(c, fmt.Sprintf("Rose window %d", i+1)).Padding(24) + }).Size(360, 220) + } + ui.Render(view, 400, 300, 1) +} diff --git a/examples/gallery/pages_data.go b/examples/gallery/pages_data.go index 00b222e..7e7a5df 100644 --- a/examples/gallery/pages_data.go +++ b/examples/gallery/pages_data.go @@ -1,3 +1,81 @@ package main -var dataPages = []page{} +import ( + "github.com/egoist/mygo/ui" + + mujica "mujicaui" + "mujicaui/theme" +) + +const dataCategory = "Data" + +var dataPages = []page{ + {ID: "069", Name: "Card", ZhName: "卡片", Category: dataCategory, Demo: dataCardDemo, Code: dataCardCode, + Keys: "A card takes no focus itself; Tab moves through the controls inside it."}, + {ID: "070", Name: "List", ZhName: "列表", Category: dataCategory, Demo: dataListDemo, Code: dataListCode, + Keys: "Up/Down (arrows in grid mode), Home/End and PageUp/PageDown move the choice; typing jumps by label; Enter submits; Cmd/Ctrl-click and Shift-click choose several; Cmd/Ctrl+A chooses all."}, + {ID: "071", Name: "DataTable", ZhName: "数据表格", Category: dataCategory, Demo: dataTableDemo, Code: dataTableCode, + Keys: "Up/Down, Home/End move the choice; Enter submits; Shift and Cmd/Ctrl extend it; click a header to sort, again to reverse; drag header edges to resize and headers to move columns."}, + {ID: "072", Name: "Tree", ZhName: "树", Category: dataCategory, Demo: dataTreeDemo, Code: dataTreeCode, + Keys: "Up/Down move; Right opens or goes to the first child; Left closes or goes to the parent; Option/Shift with Right/Left opens or closes all inside; typing jumps by label."}, + {ID: "073", Name: "TreeTable", ZhName: "树形表格", Category: dataCategory, Demo: dataTreeTableDemo, Code: dataTreeTableCode, + Keys: "As Tree for rows; click a header to sort siblings; drag header edges to resize and headers to move columns."}, + {ID: "074", Name: "Accordion", ZhName: "手风琴", Category: dataCategory, Demo: dataAccordionDemo, Code: dataAccordionCode, + Keys: "Tab reaches a header; Up/Down, Home/End move between headers, skipping disabled ones; Enter or Space opens and closes."}, + {ID: "075", Name: "Collapsible", ZhName: "折叠面板", Category: dataCategory, Demo: dataCollapsibleDemo, Code: dataCollapsibleCode, + Keys: "Enter or Space on the trigger opens and closes; closed content leaves the Tab order."}, + {ID: "076", Name: "Timeline", ZhName: "时间线", Category: dataCategory, Demo: dataTimelineDemo, Code: dataTimelineCode, + Keys: "Display only: no keys."}, + {ID: "077", Name: "DescriptionList", ZhName: "描述列表", Category: dataCategory, Demo: dataDescriptionDemo, Code: dataDescriptionCode, + Keys: "Tab reaches each copy button; Enter or Space copies its value."}, + {ID: "078", Name: "Statistic", ZhName: "统计数值", Category: dataCategory, Demo: dataStatisticDemo, Code: dataStatisticCode, + Keys: "Display only: no keys."}, + {ID: "079", Name: "ImageViewer", ZhName: "图片查看器", Category: dataCategory, Demo: dataViewerDemo, Code: dataViewerCode, + Keys: "+ and - zoom, 0 fits; Left/Right switch images; Shift+arrows pan; the wheel zooms; drag pans; Escape closes."}, + {ID: "080", Name: "Carousel", ZhName: "走马灯", Category: dataCategory, Demo: dataCarouselDemo, Code: dataCarouselCode, + Keys: "With focus inside: Left/Right step, Home/End jump; arrows move among the position dots; Enter or Space activates buttons."}, +} + +// dataSection lays out a titled block of a demo. +func dataSection(c *ui.Context, title string, fn func()) { + ui.Column(c).Gap(12).Children(func() { + ui.Text(c, title).Font(theme.SerifFont).FontSize(theme.H3Size) + fn() + }) +} + +// dataStates lays labeled variants side by side. +func dataStates(c *ui.Context, fn func(state func(name string, body func()))) { + k := mujica.Tokens(c) + ui.Row(c).Gap(24).Wrap().AlignItems(ui.Start).Children(func() { + fn(func(name string, body func()) { + ui.Column(c).Gap(6).AlignItems(ui.Start).Children(func() { + ui.Text(c, name).FontSize(theme.CaptionSize).TextColor(k.TextMuted) + body() + }) + }) + }) +} + +func dataControls(c *ui.Context, fn func()) { + ui.Row(c).Gap(16).Wrap().AlignItems(ui.Center).Children(fn) +} + +var dataStatuses = []string{"Ready", "Loading", "Loading more", "Failed", "Empty"} + +// dataStatus maps a status choice to the status and whether rows show. +func dataStatus(s string) (mujica.DataStatus, bool) { + switch s { + case "Ready": + return mujica.DataReady, true + case "Loading": + return mujica.DataLoading, false + case "Loading more": + return mujica.DataLoadingMore, true + case "Failed": + return mujica.DataFailed, true + case "Empty": + return mujica.DataReady, false + } + panic("gallery: unknown status " + s) +} diff --git a/examples/gallery/pages_data_code.go b/examples/gallery/pages_data_code.go new file mode 100644 index 0000000..2cde508 --- /dev/null +++ b/examples/gallery/pages_data_code.go @@ -0,0 +1,144 @@ +package main + +// Snippets are the bodies of the Example functions in example_data_test.go. + +const dataCardCode = `view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Card(c, mujica.CardOptions{Title: "Archive", Decorated: true, Footer: func() { + mujica.Button(c, "Open", mujica.ButtonOptions{}) + }}, func() { + ui.Text(c, "Letters of the court, sealed in wax.") + }).Width(280) +} +ui.Render(view, 400, 300, 1)` + +const dataListCode = `names := []string{"Matins", "Lauds", "Prime", "Terce"} +var state mujica.ListState[string] +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + v := mujica.List(c, &state, len(names), mujica.ListOptions[string]{ + Key: func(i int) string { return names[i] }, + Label: func(i int) string { return names[i] }, + }, func(i int) { ui.Text(c, names[i]) }) + v.Size(240, 200) + if v.Changed() { + key, _ := state.Selected() + fmt.Println("chose", key) + } +} +ui.Render(view, 400, 300, 1)` + +const dataTableCode = `type relic struct { + ID int + Name string + Age int +} +rows := []relic{{1, "Censer", 300}, {2, "Altar", 120}, {3, "Bell", 80}} +var state mujica.DataTableState[int] +opts := mujica.DataTableOptions[relic, int]{ + Key: func(r relic) int { return r.ID }, + Columns: []mujica.DataColumn[relic]{ + {ID: "name", Title: "Name", Sortable: true, + Text: func(r relic) string { return r.Name }, + Compare: func(a, b relic) int { return cmp.Compare(a.Name, b.Name) }}, + {ID: "age", Title: "Age", Width: 80, Align: ui.End, Sortable: true, + Text: func(r relic) string { return strconv.Itoa(r.Age) }, + Compare: func(a, b relic) int { return cmp.Compare(a.Age, b.Age) }}, + }, +} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.DataTable(c, &state, rows, opts).Size(360, 240) +} +ui.Render(view, 400, 300, 1)` + +const dataTreeCode = `kids := map[string][]string{"Nave": {"Pulpit", "Pews"}} +var state mujica.TreeState[string] +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Tree(c, &state, mujica.TreeOptions[string]{ + Roots: []string{"Nave", "Sacristy"}, + Children: func(n string) []string { return kids[n] }, + Label: func(n string) string { return n }, + }).Size(280, 240) +} +ui.Render(view, 400, 300, 1)` + +const dataTreeTableCode = `kids := map[string][]string{"Vestry": {"Stole", "Alb"}} +size := map[string]int{"Vestry": 9, "Stole": 2, "Alb": 5, "Organ": 7} +var state mujica.TreeTableState[string] +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.TreeTable(c, &state, mujica.TreeTableOptions[string]{ + Roots: []string{"Vestry", "Organ"}, + Children: func(n string) []string { return kids[n] }, + Label: func(n string) string { return n }, + Columns: []mujica.TreeColumn[string]{ + {ID: "name", Title: "Name", Cell: func(n string) { ui.Text(c, n) }, + Compare: cmp.Compare[string]}, + {ID: "size", Title: "Size", Width: 80, Align: ui.End, + Cell: func(n string) { ui.Text(c, strconv.Itoa(size[n])) }}, + }, + }).Size(360, 240) +} +ui.Render(view, 400, 300, 1)` + +const dataAccordionCode = `open := []string{"choir"} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Accordion(c, &open, []mujica.AccordionItem{ + {ID: "choir", Title: "Choir", Content: func() { ui.Text(c, "Twelve voices.") }}, + {ID: "nave", Title: "Nave", Content: func() { ui.Text(c, "Black oak pews.") }}, + }, mujica.AccordionOptions{}) +} +ui.Render(view, 400, 300, 1)` + +const dataCollapsibleCode = `open := false +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Collapsible(c, &open, mujica.CollapsibleOptions{Title: "Details"}, func() { + ui.Text(c, "Candles are lit at the seventh bell.") + }) +} +ui.Render(view, 400, 300, 1)` + +const dataTimelineCode = `view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Timeline(c, []mujica.TimelineItem{ + {Time: "06:00", Title: "Matins sung", Status: mujica.TimelineDone}, + {Time: "09:00", Title: "Relics moved", Content: "From the crypt to the altar.", Status: mujica.TimelineActive}, + {Time: "18:00", Title: "Vespers"}, + }) +} +ui.Render(view, 400, 300, 1)` + +const dataDescriptionCode = `view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.DescriptionList(c, []mujica.DescriptionItem{ + {Label: "Archivist", Value: "Sister Agnès", Copyable: true}, + {Label: "Shelf", Value: "North vault, row 7"}, + {Label: "Seal", Value: "Wine-red wax", Span: -1}, + }, mujica.DescriptionListOptions{}) +} +ui.Render(view, 400, 300, 1)` + +const dataStatisticCode = `change := 12.5 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Statistic(c, 12840, mujica.StatisticOptions{ + Title: "Pilgrims", Unit: "souls", Precision: 0, + Change: &change, ChangeUnit: "%", ChangeNote: "vs last feast", + }) +} +ui.Render(view, 400, 300, 1)` + +const dataViewerCode = "picture := ui.MustParseSVG([]byte(``))\nopen, index := true, 0\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tmujica.ImageViewer(c, &open, &index, []mujica.ViewerImage{{Source: picture, Alt: \"Rose window\"}})\n}\nui.Render(view, 400, 300, 1)" + +const dataCarouselCode = `index := 0 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Carousel(c, &index, 3, mujica.CarouselOptions{Label: "Windows", Autoplay: true}, func(i int) { + ui.Text(c, fmt.Sprintf("Rose window %d", i+1)).Padding(24) + }).Size(360, 220) +} +ui.Render(view, 400, 300, 1)` diff --git a/examples/gallery/pages_data_collections.go b/examples/gallery/pages_data_collections.go new file mode 100644 index 0000000..bfc442c --- /dev/null +++ b/examples/gallery/pages_data_collections.go @@ -0,0 +1,303 @@ +package main + +import ( + "cmp" + "fmt" + "strconv" + "strings" + "time" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +var dataOffices = []string{"Matins", "Lauds", "Prime", "Terce", "Sext", "None", "Vespers", "Compline"} + +func dataListDemo(c *ui.Context, log func(string)) { + type state struct { + big, grid, multiple bool + status, query string + list mujica.ListState[int] + } + s := ui.Local(c.Root(), "070", func() state { return state{status: "Ready"} }) + n := 24 + if s.big { + n = 100_000 + } + label := func(i int) string { return fmt.Sprintf("%s %d", dataOffices[i%len(dataOffices)], i+1) } + st, show := dataStatus(s.status) + if !show { + n = 0 + } + dataSection(c, "List", func() { + dataControls(c, func() { + ui.Checkbox(c, &s.big, "100,000 items") + ui.Checkbox(c, &s.grid, "Grid") + ui.Checkbox(c, &s.multiple, "Multiple") + ui.Select(c, &s.status, dataStatuses) + }) + dataControls(c, func() { + ui.TextInput(c, &s.query).Width(200).Placeholder("Find by label") + if mujica.Button(c, "Find", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + for i := range n { + if strings.Contains(strings.ToLower(label(i)), strings.ToLower(s.query)) { + s.list.ScrollTo(i) + log(fmt.Sprintf("found %q at %d", s.query, i)) + break + } + } + } + }) + v := mujica.List(c, &s.list, n, mujica.ListOptions[int]{ + Key: func(i int) int { return i }, Label: label, Grid: s.grid, Multiple: s.multiple, + ItemWidth: 140, ItemHeight: 72, Status: st, + }, func(i int) { + ui.Text(c, label(i)).SingleLine() + if s.grid { + ui.Text(c, "Office").TextColor(mujica.Tokens(c).TextMuted) + } + }) + v.Size(480, 320) + if v.Changed() { + k, _ := s.list.Selected() + log(fmt.Sprintf("selected %d, %d chosen", k, s.list.Selection.Len())) + } + if v.Submitted() { + k, _ := s.list.Selected() + log(fmt.Sprintf("submitted %d", k)) + } + if v.Retried() { + log("retry") + } + }) +} + +type dataRelic struct { + ID int + Name string + Office string + Year int + Sealed bool +} + +func dataRelics(n int) []dataRelic { + rows := make([]dataRelic, n) + for i := range rows { + rows[i] = dataRelic{ID: i + 1, Name: fmt.Sprintf("Relic %05d", (i*7919)%n+1), Office: dataOffices[i%len(dataOffices)], + Year: 1200 + (i*37)%600, Sealed: i%3 == 0} + } + return rows +} + +func dataTableDemo(c *ui.Context, log func(string)) { + type state struct { + big, multiple bool + status string + small, large []dataRelic + table mujica.DataTableState[int] + } + s := ui.Local(c.Root(), "071", func() state { return state{status: "Ready", small: dataRelics(60)} }) + rows := s.small + if s.big { + if s.large == nil { + s.large = dataRelics(100_000) + } + rows = s.large + } + st, show := dataStatus(s.status) + if !show { + rows = nil + } + k := mujica.Tokens(c) + cols := []mujica.DataColumn[dataRelic]{ + {ID: "id", Title: "ID", Width: 80, Align: ui.End, Sortable: true, Text: func(r dataRelic) string { return strconv.Itoa(r.ID) }, + Compare: func(a, b dataRelic) int { return cmp.Compare(a.ID, b.ID) }}, + {ID: "name", Title: "Name", MinWidth: 120, Sortable: true, Text: func(r dataRelic) string { return r.Name }, + Compare: func(a, b dataRelic) int { return cmp.Compare(a.Name, b.Name) }}, + {ID: "office", Title: "Office", Width: 110, Sortable: true, Text: func(r dataRelic) string { return r.Office }, + Compare: func(a, b dataRelic) int { return cmp.Compare(a.Office, b.Office) }}, + {ID: "year", Title: "Year", Width: 80, Align: ui.End, Sortable: true, Text: func(r dataRelic) string { return strconv.Itoa(r.Year) }, + Compare: func(a, b dataRelic) int { return cmp.Compare(a.Year, b.Year) }}, + {ID: "seal", Title: "Seal", Width: 100, Text: func(r dataRelic) string { return map[bool]string{true: "Sealed", false: "Open"}[r.Sealed] }, + Cell: func(r dataRelic) { + color := k.Success + if r.Sealed { + color = k.Warning + } + ui.Text(c, map[bool]string{true: "◆ Sealed", false: "◇ Open"}[r.Sealed]).TextColor(color).SingleLine() + }}, + } + dataSection(c, "DataTable", func() { + dataControls(c, func() { + ui.Checkbox(c, &s.big, "100,000 rows") + ui.Checkbox(c, &s.multiple, "Multiple") + ui.Select(c, &s.status, dataStatuses) + ui.TextInput(c, &s.table.Filter).Width(180).Placeholder("Filter") + mujica.DataTableColumnPicker(c, &s.table, cols) + }) + v := mujica.DataTable(c, &s.table, rows, mujica.DataTableOptions[dataRelic, int]{ + Columns: cols, Key: func(r dataRelic) int { return r.ID }, Multiple: s.multiple, Status: st, + }) + v.Size(620, 360) + ui.Text(c, fmt.Sprintf("%d of %d rows shown", s.table.Rows(), len(rows))).TextColor(k.TextMuted) + if v.SortChanged() { + log(fmt.Sprintf("sort %s descending=%v", s.table.Sort.Column, s.table.Sort.Descending)) + } + if v.Changed() { + id, _ := s.table.Selected() + log(fmt.Sprintf("selected row %d, %d chosen", id, s.table.Selection.Len())) + } + if v.Submitted() { + id, _ := s.table.Selected() + log(fmt.Sprintf("opened row %d", id)) + } + if v.Retried() { + log("retry") + } + }) +} + +// dataLazy simulates children that arrive 600 ms after they are asked for. +type dataLazy struct { + kids map[string][]string + status map[string]mujica.DataStatus + due map[string]time.Time + failed map[string]bool +} + +func newDataLazy() *dataLazy { + return &dataLazy{ + kids: map[string][]string{"Nave": {"Pulpit", "Pews", "Font"}, "Crypt": {}, "Archive": {}, + "Pulpit": nil, "Pews": nil, "Font": nil, "Sacristy": nil}, + status: map[string]mujica.DataStatus{"Crypt": mujica.DataUnloaded, "Archive": mujica.DataUnloaded}, + due: map[string]time.Time{}, failed: map[string]bool{}, + } +} + +func (d *dataLazy) load(c *ui.Context, log func(string)) func(string) { + return func(n string) { + d.status[n] = mujica.DataLoading + d.due[n] = c.Now().Add(600 * time.Millisecond) + log("load " + n) + } +} + +// tick finishes due loads; the Archive fails once to show retry. +func (d *dataLazy) tick(c *ui.Context) { + for n, at := range d.due { + if c.Now().Before(at) { + c.After(at.Sub(c.Now())) + continue + } + delete(d.due, n) + if n == "Archive" && !d.failed[n] { + d.failed[n], d.status[n] = true, mujica.DataFailed + continue + } + d.status[n] = mujica.DataReady + d.kids[n] = []string{n + " north", n + " south"} + } +} + +func dataTreeDemo(c *ui.Context, log func(string)) { + type state struct { + big, multiple bool + lazy *dataLazy + tree mujica.TreeState[string] + bigTree mujica.TreeState[string] + } + s := ui.Local(c.Root(), "072", func() state { return state{lazy: newDataLazy()} }) + s.lazy.tick(c) + opts := mujica.TreeOptions[string]{ + Roots: []string{"Nave", "Crypt", "Archive", "Sacristy"}, Children: func(n string) []string { return s.lazy.kids[n] }, + Label: func(n string) string { return n }, ItemStatus: func(n string) mujica.DataStatus { return s.lazy.status[n] }, + Load: s.lazy.load(c, log), Multiple: s.multiple, + } + ts := &s.tree + if s.big { + opts = dataBigTree(s.multiple) + ts = &s.bigTree + } + dataSection(c, "Tree", func() { + dataControls(c, func() { + ui.Checkbox(c, &s.big, "100,000 nodes") + ui.Checkbox(c, &s.multiple, "Multiple") + }) + v := mujica.Tree(c, ts, opts) + v.Size(420, 340) + if v.Changed() { + n, _ := ts.Selected() + log(fmt.Sprintf("selected %s, %d chosen", n, ts.Selection.Len())) + } + if v.Submitted() { + n, _ := ts.Selected() + log("submitted " + n) + } + }) +} + +// dataBigTree has 100 roots of 999 children: 100,000 nodes, built as they open. +func dataBigTree(multiple bool) mujica.TreeOptions[string] { + roots := make([]string, 100) + for i := range roots { + roots[i] = fmt.Sprintf("Wing %d", i+1) + } + return mujica.TreeOptions[string]{ + Roots: roots, Label: func(n string) string { return n }, Multiple: multiple, + Children: func(n string) []string { + if !strings.HasPrefix(n, "Wing ") || strings.Contains(n, "·") { + return nil + } + return dataWing(n) + }, + } +} + +var dataWings = map[string][]string{} + +func dataWing(n string) []string { + if kids, ok := dataWings[n]; ok { + return kids + } + kids := make([]string, 999) + for i := range kids { + kids[i] = fmt.Sprintf("%s · cell %d", n, i+1) + } + dataWings[n] = kids + return kids +} + +func dataTreeTableDemo(c *ui.Context, log func(string)) { + type state struct { + multiple bool + lazy *dataLazy + table mujica.TreeTableState[string] + } + s := ui.Local(c.Root(), "073", func() state { return state{lazy: newDataLazy()} }) + s.lazy.tick(c) + size := func(n string) int { return len(n)*7 + len(s.lazy.kids[n])*13 } + text := func(n string) { ui.Text(c, n).SingleLine() } + dataSection(c, "TreeTable", func() { + dataControls(c, func() { ui.Checkbox(c, &s.multiple, "Multiple") }) + v := mujica.TreeTable(c, &s.table, mujica.TreeTableOptions[string]{ + Roots: []string{"Nave", "Crypt", "Archive", "Sacristy"}, Children: func(n string) []string { return s.lazy.kids[n] }, + Label: func(n string) string { return n }, ItemStatus: func(n string) mujica.DataStatus { return s.lazy.status[n] }, + Load: s.lazy.load(c, log), Multiple: s.multiple, + Columns: []mujica.TreeColumn[string]{ + {ID: "name", Title: "Name", MinWidth: 160, Cell: text, Compare: cmp.Compare[string]}, + {ID: "size", Title: "Size", Width: 90, Align: ui.End, Cell: func(n string) { ui.Text(c, fmt.Sprintf("%d m²", size(n))) }, + Compare: func(a, b string) int { return cmp.Compare(size(a), size(b)) }}, + {ID: "kids", Title: "Rooms", Width: 80, Align: ui.End, Cell: func(n string) { ui.Text(c, strconv.Itoa(len(s.lazy.kids[n]))) }}, + }, + }) + v.Size(520, 320) + if v.SortChanged() { + log(fmt.Sprintf("sort %s descending=%v", s.table.Sort.Column, s.table.Sort.Descending)) + } + if v.Changed() { + n, _ := s.table.Selected() + log(fmt.Sprintf("selected %s, %d chosen", n, s.table.Selection.Len())) + } + }) +} diff --git a/examples/gallery/pages_data_content.go b/examples/gallery/pages_data_content.go new file mode 100644 index 0000000..57a4612 --- /dev/null +++ b/examples/gallery/pages_data_content.go @@ -0,0 +1,239 @@ +package main + +import ( + "fmt" + "strconv" + "time" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +func dataCardDemo(c *ui.Context, log func(string)) { + type state struct { + decorated, footer bool + title string + } + s := ui.Local(c.Root(), "069", func() state { return state{footer: true, title: "Archive"} }) + dataSection(c, "Card", func() { + dataControls(c, func() { + ui.TextInput(c, &s.title).Width(180).Placeholder("Title") + ui.Checkbox(c, &s.decorated, "Decorated") + ui.Checkbox(c, &s.footer, "Footer") + }) + card := func(decorated bool) { + opts := mujica.CardOptions{Title: s.title, Decorated: decorated} + if s.footer { + opts.Footer = func() { + if mujica.Button(c, "Open", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + log(fmt.Sprintf("open clicked (decorated=%v)", decorated)) + } + } + } + mujica.Card(c, opts, func() { + ui.Text(c, "Letters of the court, sealed in wine-red wax and kept in the north vault.") + }).Width(280) + } + dataStates(c, func(state func(string, func())) { + state("Options above", func() { card(s.decorated) }) + state("Plain", func() { card(false) }) + state("Decorated", func() { card(true) }) + }) + }) +} + +func dataAccordionDemo(c *ui.Context, log func(string)) { + type state struct { + multiple, keepOne bool + open []string + } + s := ui.Local(c.Root(), "074", func() state { return state{open: []string{"choir"}} }) + dataSection(c, "Accordion", func() { + dataControls(c, func() { + ui.Checkbox(c, &s.multiple, "Multiple") + ui.Checkbox(c, &s.keepOne, "Keep one open") + if mujica.Button(c, "Close all", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + s.open = nil + log("closed all from outside") + } + }) + if mujica.Accordion(c, &s.open, []mujica.AccordionItem{ + {ID: "choir", Title: "Choir", Content: func() { ui.Text(c, "Twelve voices, two at the altar.") }}, + {ID: "nave", Title: "Nave", Content: func() { ui.Text(c, "Pews of black oak, polished for the feast.") }}, + {ID: "crypt", Title: "Crypt (disabled)", Disabled: true, Content: func() {}}, + {ID: "bells", Title: "Bells", Content: func() { + mujica.Button(c, "Ring", mujica.ButtonOptions{}) + }}, + }, mujica.AccordionOptions{Multiple: s.multiple, KeepOne: s.keepOne}).Width(420).Changed() { + log(fmt.Sprintf("open %v", s.open)) + } + }) +} + +func dataCollapsibleDemo(c *ui.Context, log func(string)) { + type state struct{ open, disabled bool } + s := ui.Local(c.Root(), "075", func() state { return state{} }) + dataSection(c, "Collapsible", func() { + dataControls(c, func() { + if mujica.Button(c, "Open from outside", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + s.open = true + log("opened from outside") + } + ui.Checkbox(c, &s.disabled, "Disabled") + }) + if mujica.Collapsible(c, &s.open, mujica.CollapsibleOptions{Title: "Rites of the evening", Disabled: s.disabled}, func() { + ui.Text(c, "Candles are lit at the seventh bell; the choir enters from the north door.") + mujica.Button(c, "Light candles", mujica.ButtonOptions{Variant: mujica.Outline}) + }).Changed() { + log(fmt.Sprintf("open=%v", s.open)) + } + }) +} + +func dataTimelineDemo(c *ui.Context, log func(string)) { + type state struct{ items []mujica.TimelineItem } + s := ui.Local(c.Root(), "076", func() state { + return state{items: []mujica.TimelineItem{ + {Time: "06:00", Title: "Matins sung", Content: "The choir gathered before dawn.", Status: mujica.TimelineDone}, + {Time: "09:00", Title: "Relics moved", Status: mujica.TimelineActive, + Content: "The reliquaries travel from the crypt to the high altar, carried in procession by four acolytes in black and wine."}, + {Time: "12:00", Title: "Bell cracked", Status: mujica.TimelineError}, + {Time: "15:00", Title: "Storm warning", Status: mujica.TimelineWarning}, + {Time: "18:00", Title: "Vespers"}, + }} + }) + dataSection(c, "Timeline", func() { + dataControls(c, func() { + if mujica.Button(c, "Add event", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + s.items = append(s.items, mujica.TimelineItem{Time: fmt.Sprintf("%02d:00", 19+len(s.items)%5), Title: "Compline"}) + log(fmt.Sprintf("%d events", len(s.items))) + } + }) + ui.Column(c).Width(420).Children(func() { mujica.Timeline(c, s.items) }) + }) +} + +func dataDescriptionDemo(c *ui.Context, log func(string)) { + type state struct { + columns string + width float64 + } + s := ui.Local(c.Root(), "077", func() state { return state{columns: "2", width: 560} }) + dataSection(c, "DescriptionList", func() { + dataControls(c, func() { + ui.Select(c, &s.columns, []string{"1", "2", "3"}) + ui.Text(c, "Width") + ui.Slider(c, &s.width, 240, 720).Width(200) + }) + cols, _ := strconv.Atoi(s.columns) + ui.Column(c).Width(float32(s.width)).Children(func() { + if i, ok := mujica.DescriptionList(c, []mujica.DescriptionItem{ + {Label: "Archivist", Value: "Sister Agnès", Copyable: true}, + {Label: "Shelf", Value: "North vault, row 7"}, + {Label: "Catalogue", Value: "MS-1347-B", Copyable: true}, + {Label: "Seal", Value: "Wine-red wax, cracked at the rim", Span: -1}, + }, mujica.DescriptionListOptions{Columns: cols}).Copied(); ok { + log(fmt.Sprintf("copied item %d", i)) + } + }) + }) +} + +func dataStatisticDemo(c *ui.Context, log func(string)) { + type state struct { + value, change float64 + precision string + } + s := ui.Local(c.Root(), "078", func() state { return state{value: 12840, change: 12.5, precision: "0"} }) + dataSection(c, "Statistic", func() { + dataControls(c, func() { + ui.Text(c, "Value") + if ui.Slider(c, &s.value, 0, 100000).Width(160).Changed() { + log(fmt.Sprintf("value %.0f", s.value)) + } + ui.Text(c, "Change") + ui.Slider(c, &s.change, -50, 50).Width(160) + ui.Select(c, &s.precision, []string{"0", "1", "2"}) + }) + p, _ := strconv.Atoi(s.precision) + change := s.change + dataStates(c, func(state func(string, func())) { + state("With change", func() { + mujica.Statistic(c, s.value, mujica.StatisticOptions{Title: "Pilgrims", Unit: "souls", Precision: p, + Change: &change, ChangeUnit: "%", ChangeNote: "vs last feast"}) + }) + state("Prefix", func() { + mujica.Statistic(c, s.value/1000, mujica.StatisticOptions{Title: "Tithe", Prefix: "£", Suffix: "k", Precision: p}) + }) + }) + }) +} + +var dataPictures = func() []mujica.ViewerImage { + art := func(bg, fg string, shape string) *ui.SVG { + return ui.MustParseSVG([]byte(`` + + ``)) + } + return []mujica.ViewerImage{ + {Source: art("#AD9164", "#7D2034", "M240 40 L380 160 L240 280 L100 160 Z"), Alt: "Rose window"}, + {Source: art("#211C1D", "#BDA172", "M80 280 L80 120 L240 40 L400 120 L400 280 Z"), Alt: "Chapel arch"}, + {Source: art("#F2EBDD", "#651729", "M60 60 H420 V100 H60 Z M60 140 H420 V180 H60 Z M60 220 H420 V260 H60 Z"), Alt: "Choir stalls"}, + } +}() + +func dataViewerDemo(c *ui.Context, log func(string)) { + type state struct { + open bool + index int + } + s := ui.Local(c.Root(), "079", func() state { return state{} }) + dataSection(c, "ImageViewer", func() { + k := mujica.Tokens(c) + ui.Text(c, "Choose a picture to open the viewer.").TextColor(k.TextMuted) + ui.Row(c).Gap(12).Children(func() { + for i, p := range dataPictures { + thumb := ui.ButtonBase(c).Label("Open "+p.Alt).Radius(4).Border(1, k.Border).Clip() + thumb.Children(func() { ui.Image(c, p.Source).Size(144, 96) }) + if thumb.Clicked() { + s.open, s.index = true, i + log("open " + p.Alt) + } + } + }) + wasOpen := s.open + if mujica.ImageViewer(c, &s.open, &s.index, dataPictures).Changed() { + log(fmt.Sprintf("image %d", s.index+1)) + } + if wasOpen && !s.open { + log("closed") + } + }) +} + +func dataCarouselDemo(c *ui.Context, log func(string)) { + type state struct { + index int + autoplay, loop bool + interval string + } + s := ui.Local(c.Root(), "080", func() state { return state{interval: "3s"} }) + dataSection(c, "Carousel", func() { + dataControls(c, func() { + ui.Checkbox(c, &s.autoplay, "Autoplay") + ui.Checkbox(c, &s.loop, "Loop") + ui.Select(c, &s.interval, []string{"2s", "3s", "5s"}) + }) + d, err := time.ParseDuration(s.interval) + if err != nil { + panic(err) + } + v := mujica.Carousel(c, &s.index, len(dataPictures), mujica.CarouselOptions{Label: "Windows", Autoplay: s.autoplay, Loop: s.loop, Interval: d}, func(i int) { + ui.Image(c, dataPictures[i].Source).Grow(1).Label(dataPictures[i].Alt) + }) + v.Size(520, 300) + if v.Changed() { + log(fmt.Sprintf("slide %d", s.index+1)) + } + }) +} diff --git a/image_viewer.go b/image_viewer.go new file mode 100644 index 0000000..30a10e2 --- /dev/null +++ b/image_viewer.go @@ -0,0 +1,193 @@ +package mujica + +import ( + "fmt" + "log/slog" + "math" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// ViewerImage is one picture of an ImageViewer. +type ViewerImage struct { + // Source is a *ui.Bitmap or *ui.SVG; required. + Source ui.ImageSource + // Alt describes the picture for assistive technology and the caption. + Alt string +} + +// ImageViewerView is a built ImageViewer and what happened to it in this frame. +// Element is nil while the viewer is closed. +type ImageViewerView struct { + *ui.Element + changed bool +} + +// Changed reports that another image was chosen. +func (v ImageViewerView) Changed() bool { return v.changed } + +// Zoom limits, as multiples of the fitted size. +const ( + viewerMinZoom = 0.25 + viewerMaxZoom = 8 + viewerZoomStep = 1.25 + viewerPanStep = 48 +) + +type viewerState struct { + image int + zoom, x, y float32 +} + +// ImageViewer shows images[*index] over the window while *open, with zoom, pan and switching. +func ImageViewer(c *ui.Context, open *bool, index *int, images []ViewerImage) ImageViewerView { + var v ImageViewerView + if !*open { + return v + } + if len(images) == 0 || *index < 0 || *index >= len(images) { + panic("mujica: ImageViewer index out of range") + } + k := Tokens(c) + u := Density(c).Unit() + w, h := c.Size() + v.Element = ui.DialogBase(c, open, func(back, panel *ui.Element) { + back.Background(ui.RGBA(0, 0, 0, 0.55)) + panel.Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.PopupRadius). + Size(w-u*12, h-u*12).Padding(u * 3).Gap(u * 3).Label(msg(c, "image_viewer.title")) + st := ui.Local(panel, "view", func() viewerState { return viewerState{image: -1} }) + if st.image != *index { + *st = viewerState{image: *index, zoom: 1} + } + goTo := func(i int) { + if i < 0 || i >= len(images) || i == *index { + return + } + *index, v.changed = i, true + *st = viewerState{image: i, zoom: 1} + c.Announce(fmt.Sprintf(msg(c, "image_viewer.position"), i+1, len(images))) + slog.Debug("mujica: image_viewer image", "index", i) + } + zoom := func(f float32) { + st.zoom = float32(math.Max(viewerMinZoom, math.Min(viewerMaxZoom, float64(st.zoom*f)))) + } + viewerKeys(panel, st, zoom, goTo, *index) + img := images[*index] + tool := func(icon, label string, disabled bool) bool { + return Button(c, "", ButtonOptions{Variant: Ghost, Icon: icons.Must(icon), Label: msg(c, label), Disabled: disabled}).Clicked() + } + ui.Row(c).Gap(u).AlignItems(ui.Center).Children(func() { + ui.Text(c, fmt.Sprintf(msg(c, "image_viewer.position"), *index+1, len(images))).TextColor(k.TextMuted).Grow(1) + if tool("zoom-out", "image_viewer.zoomOut", st.zoom <= viewerMinZoom) { + zoom(1 / viewerZoomStep) + } + ui.Text(c, fmt.Sprintf("%d%%", int(math.Round(float64(st.zoom*100))))).Width(48).TextAlign(ui.Center).SingleLine() + if tool("zoom-in", "image_viewer.zoomIn", st.zoom >= viewerMaxZoom) { + zoom(viewerZoomStep) + } + if tool("maximize", "image_viewer.fit", st.zoom == 1 && st.x == 0 && st.y == 0) { + *st = viewerState{image: *index, zoom: 1} + } + if tool("x", "image_viewer.close", false) { + *open = false + } + }) + ui.Row(c).Grow(1).MinHeight(0).Gap(u * 2).AlignItems(ui.Center).Children(func() { + if tool("chevron-left", "image_viewer.prev", *index == 0) { + goTo(*index - 1) + } + stage := ui.Box(c).Grow(1).FillHeight().MinWidth(0).Focusable().AutoFocus().Clip(). + Radius(theme.ControlRadius).Background(k.Background).Role(ui.RoleImage).Label(img.Alt).Cursor(ui.CursorGrab) + internal.Ring(stage, k.Focus, theme.ControlRadius) + if dx, dy, ok := stage.Dragged(); ok { + st.x, st.y = st.x+dx, st.y+dy + } + stage.HandleInput(func(ev ui.InputEvent) bool { + if ev.Kind != ui.InputScroll || ev.DY == 0 { + return false + } + if ev.DY < 0 { + zoom(viewerZoomStep) + } else { + zoom(1 / viewerZoomStep) + } + return true + }) + iw, ih := viewerSize(img.Source) + place := func(r ui.Rect) (dw, dh, mx, my float32) { + fit := float32(math.Min(float64(r.W/iw), float64(r.H/ih))) + dw, dh = iw*fit*st.zoom, ih*fit*st.zoom + // The picture cannot be dragged past its edges. + return dw, dh, max(0, (dw-r.W)/2), max(0, (dh-r.H)/2) + } + if b := stage.Bounds(); b.W > 0 { + _, _, mx, my := place(b) + st.x, st.y = min(mx, max(-mx, st.x)), min(my, max(-my, st.y)) + } + x, y := st.x, st.y + stage.Draw(func(p *ui.Painter, r ui.Rect) { + dw, dh, mx, my := place(r) + ox, oy := min(mx, max(-mx, x)), min(my, max(-my, y)) + p.Clip(r, theme.ControlRadius, func() { + p.Image(img.Source, ui.Rect{X: r.X + (r.W-dw)/2 + ox, Y: r.Y + (r.H-dh)/2 + oy, W: dw, H: dh}, ui.FillBox) + }) + }) + if tool("chevron-right", "image_viewer.next", *index == len(images)-1) { + goTo(*index + 1) + } + }) + if img.Alt != "" { + ui.Text(c, img.Alt).TextColor(k.TextMuted).TextAlign(ui.Center).FillWidth() + } + }) + if !*open { + slog.Debug("mujica: image_viewer close") + } + return v +} + +// viewerKeys, anywhere in the panel, zooms with + - 0, switches with Left and Right, and pans with Shift+arrows. +func viewerKeys(panel *ui.Element, st *viewerState, zoom func(float32), goTo func(int), at int) { + switch { + case panel.Shortcut(0, ui.KeyEqual), panel.Shortcut(ui.Shift, ui.KeyEqual): + zoom(viewerZoomStep) + case panel.Shortcut(0, ui.KeyMinus): + zoom(1 / viewerZoomStep) + case panel.Shortcut(0, ui.Key0): + *st = viewerState{image: st.image, zoom: 1} + case panel.Shortcut(0, ui.KeyLeft): + goTo(at - 1) + case panel.Shortcut(0, ui.KeyRight): + goTo(at + 1) + case panel.Shortcut(ui.Shift, ui.KeyLeft): + st.x += viewerPanStep + case panel.Shortcut(ui.Shift, ui.KeyRight): + st.x -= viewerPanStep + case panel.Shortcut(ui.Shift, ui.KeyUp): + st.y += viewerPanStep + case panel.Shortcut(ui.Shift, ui.KeyDown): + st.y -= viewerPanStep + } +} + +// viewerSize returns a picture's own size in DIPs. +func viewerSize(src ui.ImageSource) (float32, float32) { + var w, h float32 + switch s := src.(type) { + case *ui.Bitmap: + iw, ih := s.Size() + w, h = float32(iw), float32(ih) + case *ui.SVG: + w, h = s.Size() + default: + panic(fmt.Sprintf("mujica: ImageViewer source %T is not a *ui.Bitmap or *ui.SVG", src)) + } + if w <= 0 || h <= 0 { + panic("mujica: ImageViewer source without a size") + } + return w, h +} diff --git a/image_viewer_test.go b/image_viewer_test.go new file mode 100644 index 0000000..3b42018 --- /dev/null +++ b/image_viewer_test.go @@ -0,0 +1,157 @@ +package mujica + +import ( + "image" + "image/color" + "testing" + + "github.com/egoist/mygo/ui" +) + +// viewerPicture is red on the left half and blue on the right. +func viewerPicture() *ui.Bitmap { + img := image.NewRGBA(image.Rect(0, 0, 200, 100)) + for y := range 100 { + for x := range 200 { + c := color.RGBA{200, 30, 30, 255} + if x >= 100 { + c = color.RGBA{30, 30, 200, 255} + } + img.SetRGBA(x, y, c) + } + } + return ui.NewBitmap(img) +} + +type viewerHarness struct { + open bool + index int + images []ViewerImage + changes int + stage ui.Rect +} + +func newViewerHarness() *viewerHarness { + pic := viewerPicture() + return &viewerHarness{open: true, images: []ViewerImage{{pic, "Rose window"}, {pic, "Choir stalls"}, {pic, "Crypt door"}}} +} + +func (h *viewerHarness) view(c *ui.Context) { + Use(c, Settings{Mode: Light}) + if ImageViewer(c, &h.open, &h.index, h.images).Changed() { + h.changes++ + } +} + +func centerPixel(tt *ui.Tester) color.RGBA { + r, _ := tt.Find("Rose window") // the stage, by its Label + return color.RGBAModel.Convert(tt.Image().At(int(r.X+r.W/2), int(r.Y+r.H/2))).(color.RGBA) +} + +func TestImageViewerSwitchAndClose(t *testing.T) { + h := newViewerHarness() + tt := ui.NewTester(h.view, 640, 480) + if !tt.HasText("Image 1 of 3") { + t.Fatalf("texts %q", tt.Texts()) + } + _ = tt.Click("Previous image") + if h.index != 0 { + t.Fatal("previous moved before the first image") + } + _ = tt.Click("Next image") + if h.index != 1 || h.changes != 1 || !tt.HasText("Choir stalls") { + t.Fatalf("next: index %d changes %d", h.index, h.changes) + } + tt.Key(0, ui.KeyRight) + if h.index != 2 { + t.Fatalf("Right: %d", h.index) + } + tt.Key(0, ui.KeyLeft) + tt.Key(0, ui.KeyEscape) + if h.open || h.index != 1 { + t.Fatalf("Escape: open %v index %d", h.open, h.index) + } + h.open = true + tt.Frame() + _ = tt.Click("Close") + if h.open { + t.Fatal("close button left it open") + } +} + +func TestImageViewerZoomAndPan(t *testing.T) { + h := newViewerHarness() + tt := ui.NewTester(h.view, 640, 480) + tt.Key(0, ui.KeyEqual) + if !tt.HasText("125%") { + t.Fatalf("+ did not zoom: %q", tt.Texts()) + } + _ = tt.Click("Zoom in") + _ = tt.Click("Zoom in") + if !tt.HasText("195%") { + t.Fatalf("zoom in: %q", tt.Texts()) + } + for range 10 { + tt.Key(ui.Shift, ui.KeyLeft) // the picture moves right: its left half shows + } + if p := centerPixel(tt); p.R < 150 || p.B > 100 { + t.Fatalf("pan right shows %v, want red", p) + } + for range 20 { + tt.Key(ui.Shift, ui.KeyRight) + } + if p := centerPixel(tt); p.B < 150 || p.R > 100 { + t.Fatalf("pan left shows %v, want blue", p) + } + tt.Key(0, ui.Key0) + if !tt.HasText("100%") { + t.Fatalf("0 did not fit: %q", tt.Texts()) + } + for range 20 { + tt.Key(0, ui.KeyMinus) + } + if !tt.HasText("25%") { + t.Fatalf("zoom floor: %q", tt.Texts()) + } +} + +func TestImageViewerClosedAndBad(t *testing.T) { + h := newViewerHarness() + h.open = false + tt := ui.NewTester(h.view, 300, 200) + if len(tt.Texts()) != 0 { + t.Fatalf("closed viewer shows %q", tt.Texts()) + } + defer func() { + if recover() == nil { + t.Fatal("no panic on bad index") + } + }() + h.open, h.index = true, 9 + tt.Frame() +} + +// viewerArt is a wine diamond on old gold, for the snapshot. +func viewerArt() *ui.Bitmap { + img := image.NewRGBA(image.Rect(0, 0, 240, 160)) + for y := range 160 { + for x := range 240 { + c := color.RGBA{173, 145, 100, 255} + if viewerAbs(x-120)*2/3+viewerAbs(y-80) < 60 { + c = color.RGBA{125, 32, 52, 255} + } + img.SetRGBA(x, y, c) + } + } + return ui.NewBitmap(img) +} + +func viewerAbs(v int) int { return max(v, -v) } + +func TestImageViewerSnapshot(t *testing.T) { + pic := viewerArt() + snapshot(t, "image_viewer", 560, 360, func(c *ui.Context) { + open, i := true, 0 + ImageViewer(c, &open, &i, []ViewerImage{{pic, "Rose window"}, {pic, "Choir stalls"}}) + }) +} diff --git a/list.go b/list.go new file mode 100644 index 0000000..bedea8d --- /dev/null +++ b/list.go @@ -0,0 +1,235 @@ +package mujica + +import ( + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// DataStatus is the load state of a data view's rows. +type DataStatus int + +const ( + DataReady DataStatus = iota + // DataLoading is the first load, before any row came. + DataLoading + // DataLoadingMore appends rows below those shown. + DataLoadingMore + // DataFailed shows the error with a retry action. + DataFailed + // DataUnloaded marks a tree item whose children were not asked for yet. + DataUnloaded +) + +// ListOptions configure a List of items keyed by K. +type ListOptions[K comparable] struct { + // Key returns item i's stable identity; required. + Key func(i int) K + // Label returns item i's text, read out and matched by typing; required. + Label func(i int) string + Multiple bool + // Grid lays items out in columns at least ItemWidth wide, ItemHeight high. + Grid bool + ItemWidth, ItemHeight float32 + // Empty replaces the built-in "No items" text. + Empty string + Status DataStatus + // Error describes a DataFailed status; empty uses the built-in text. + Error string +} + +// ListState is a List's selection and place. Its zero value selects nothing. +type ListState[K comparable] struct { + // Selection holds the chosen keys of a Multiple list. + Selection ui.Selection[K] + rows ui.ListState + grid ui.GridState + cursor int + started bool + key K + has bool +} + +// Selected returns the chosen item's key, or the last chosen of a Multiple list. +func (s *ListState[K]) Selected() (K, bool) { return s.key, s.has } + +// ScrollTo brings item i to the top of the list. +func (s *ListState[K]) ScrollTo(i int) { + s.rows.ScrollTo(i, ui.Start) + s.grid.ScrollTo(i, ui.Start) +} + +// ListView is a built List and what happened to it in this frame. +type ListView struct { + *ui.Element + changed, submitted, retry bool +} + +// Changed reports a new selection. +func (v ListView) Changed() bool { return v.changed } + +// Submitted reports Enter or a double click on an item. +func (v ListView) Submitted() bool { return v.submitted } + +// Retried reports a click on the retry action of a failed load. +func (v ListView) Retried() bool { return v.retry } + +// List shows n items, building only those in view, as rows or a grid. +func List[K comparable](c *ui.Context, s *ListState[K], n int, opts ListOptions[K], item func(i int)) ListView { + if opts.Key == nil || opts.Label == nil { + panic("mujica: List needs Key and Label") + } + if n < 0 { + panic("mujica: List with negative item count") + } + if !s.started { + s.started, s.cursor = true, -1 + } + k := Tokens(c) + var v ListView + v.Element = ui.Column(c).MinHeight(0) + v.Children(func() { + if n == 0 { + v.retry = dataPlaceholder(c, opts.Status, opts.Error, opts.Empty, msg(c, "list.empty")) + return + } + var e *ui.Element + if opts.Grid { + e = listGrid(c, s, n, opts, item) + } else { + e = listRows(c, s, n, opts, item) + } + e.Grow(1).MinHeight(0).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius) + internal.Ring(e, k.Focus, theme.ControlRadius) + v.changed, v.submitted = e.Changed(), e.Submitted() + v.retry = dataFooter(c, opts.Status, opts.Error) + }) + s.has = s.cursor >= 0 && s.cursor < n + if s.has { + s.key = opts.Key(s.cursor) + } + if v.changed { + slog.Debug("mujica: list changed", "selected", s.cursor, "count", s.Selection.Len()) + } + return v +} + +func listRows[K comparable](c *ui.Context, s *ListState[K], n int, opts ListOptions[K], item func(i int)) *ui.Element { + d := Density(c) + s.rows.Key = func(i int) any { return opts.Key(i) } + s.rows.Label = opts.Label + s.rows.Selected = &s.cursor + if opts.Multiple { + s.rows.Selection = &s.Selection + } else { + s.rows.Selection = nil + } + return ui.List(c, &s.rows, n, func(i int) { + ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { + ui.Row(c).Grow(1).MinWidth(0).AlignItems(ui.Center).Gap(d.Unit() * 2).Children(func() { item(i) }) + if listChosen(s, opts, i) { + ui.Icon(c, icons.Must("check")).Size(16, 16).Shrink(0) + } + }) + }).Padding(d.Unit()) +} + +func listGrid[K comparable](c *ui.Context, s *ListState[K], n int, opts ListOptions[K], item func(i int)) *ui.Element { + d := Density(c) + w, h := opts.ItemWidth, opts.ItemHeight + if w <= 0 { + w = 160 + } + if h <= 0 { + h = 120 + } + s.grid.Key = func(i int) any { return opts.Key(i) } + s.grid.Label = opts.Label + s.grid.Selected = &s.cursor + if opts.Multiple { + s.grid.Selection = &s.Selection + } else { + s.grid.Selection = nil + } + // Chosen items take AccentText borders, legible on both surfaces. + saved := c.Theme() + t := *saved + t.Accent = Tokens(c).AccentText + c.SetTheme(&t) + e := ui.GridView(c, &s.grid, n, w, h, func(i int) { + ui.Column(c).Grow(1).Padding(d.Unit() * 2).Gap(d.Unit()).Children(func() { + item(i) + if listChosen(s, opts, i) { + ui.Icon(c, icons.Must("check")).Size(16, 16).Absolute().Top(d.Unit()).Right(d.Unit()) + } + }) + }).Padding(d.Unit() * 2) + c.SetTheme(saved) + return e +} + +func listChosen[K comparable](s *ListState[K], opts ListOptions[K], i int) bool { + if opts.Multiple { + return s.Selection.Has(opts.Key(i)) + } + return s.cursor == i +} + +// dataPlaceholder fills an empty view with its status and reports a retry. +func dataPlaceholder(c *ui.Context, st DataStatus, errText, empty, emptyDefault string) bool { + k := Tokens(c) + u := Density(c).Unit() + retry := false + ui.Column(c).Grow(1).MinHeight(u * 30).Center().Gap(u * 3).Padding(u * 6).Children(func() { + switch st { + case DataLoading, DataLoadingMore, DataUnloaded: + Spinner(c, SpinnerOptions{Label: msg(c, "list.loading"), ShowLabel: true}) + case DataFailed: + ui.Row(c).Gap(u * 2).AlignItems(ui.Center).TextColor(k.Danger).Children(func() { + ui.Icon(c, icons.Must("circle-alert")).Size(20, 20) + ui.Text(c, dataError(c, errText)).Role(ui.RoleStatus) + }) + retry = Button(c, msg(c, "list.retry"), ButtonOptions{Variant: Secondary, Icon: icons.Must("refresh-cw")}).Clicked() + default: + if empty == "" { + empty = emptyDefault + } + ui.Icon(c, icons.Must("inbox")).Size(24, 24).TextColor(k.TextMuted) + ui.Text(c, empty).TextColor(k.TextMuted) + } + }) + return retry +} + +// dataFooter shows appending and failed loads below rows and reports a retry. +func dataFooter(c *ui.Context, st DataStatus, errText string) bool { + k := Tokens(c) + u := Density(c).Unit() + retry := false + switch st { + case DataLoadingMore: + ui.Row(c).Padding(u*2, u*3).Shrink(0).Children(func() { + Spinner(c, SpinnerOptions{Label: msg(c, "list.loadingMore"), ShowLabel: true}) + }) + case DataFailed: + ui.Row(c).Padding(u*2, u*3).Gap(u * 3).AlignItems(ui.Center).Shrink(0).Children(func() { + ui.Row(c).Gap(u * 2).AlignItems(ui.Center).TextColor(k.Danger).Grow(1).Children(func() { + ui.Icon(c, icons.Must("circle-alert")).Size(16, 16) + ui.Text(c, dataError(c, errText)).Role(ui.RoleStatus) + }) + retry = Button(c, msg(c, "list.retry"), ButtonOptions{Variant: Ghost, Icon: icons.Must("refresh-cw")}).Clicked() + }) + } + return retry +} + +func dataError(c *ui.Context, s string) string { + if s == "" { + return msg(c, "list.failed") + } + return s +} diff --git a/list_test.go b/list_test.go new file mode 100644 index 0000000..d146136 --- /dev/null +++ b/list_test.go @@ -0,0 +1,215 @@ +package mujica + +import ( + "fmt" + "testing" + + "github.com/egoist/mygo/ui" +) + +type listHarness struct { + ids []int + st ListState[int] + opts ListOptions[int] + built int + changes int + submits int + retries int + listSize [2]float32 +} + +func newListHarness(n int) *listHarness { + h := &listHarness{listSize: [2]float32{300, 360}} + for i := range n { + h.ids = append(h.ids, i) + } + h.opts.Key = func(i int) int { return h.ids[i] } + h.opts.Label = func(i int) string { return fmt.Sprintf("Item %d", h.ids[i]) } + return h +} + +func (h *listHarness) view(c *ui.Context) { + Use(c, Settings{Mode: Light}) + h.built = 0 + ui.Column(c).Padding(10).Children(func() { + v := List(c, &h.st, len(h.ids), h.opts, func(i int) { + h.built++ + ui.Text(c, h.opts.Label(i)).SingleLine() + }) + v.Size(h.listSize[0], h.listSize[1]) + if v.Changed() { + h.changes++ + } + if v.Submitted() { + h.submits++ + } + if v.Retried() { + h.retries++ + } + }) +} + +func TestListSingleSelect(t *testing.T) { + h := newListHarness(20) + tt := ui.NewTester(h.view, 400, 400) + if _, ok := h.st.Selected(); ok { + t.Fatal("zero state has a selection") + } + if err := tt.Click("Item 3"); err != nil { + t.Fatal(err) + } + if k, ok := h.st.Selected(); !ok || k != 3 || h.changes != 1 { + t.Fatalf("selected %v %v, changes %d", k, ok, h.changes) + } + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyDown) + if k, _ := h.st.Selected(); k != 5 { + t.Fatalf("after Down×2 selected %d, want 5", k) + } + tt.Key(0, ui.KeyEnd) + if k, _ := h.st.Selected(); k != 19 { + t.Fatalf("End selected %d", k) + } + tt.Key(0, ui.KeyEnter) + if h.submits != 1 { + t.Fatalf("submits %d", h.submits) + } +} + +func TestListKeyFollowsInsert(t *testing.T) { + h := newListHarness(10) + tt := ui.NewTester(h.view, 400, 400) + if err := tt.Click("Item 4"); err != nil { + t.Fatal(err) + } + h.ids = append([]int{100, 101}, h.ids...) + tt.Frame() + if k, _ := h.st.Selected(); k != 4 { + t.Fatalf("selection moved to %d after insert", k) + } + h.ids = append(h.ids[:5], h.ids[7:]...) // drops items 3 and 4 + tt.Frame() + if k, ok := h.st.Selected(); ok { + t.Fatalf("removed item still selected: %d", k) + } +} + +func TestListMultiple(t *testing.T) { + h := newListHarness(10) + h.opts.Multiple = true + tt := ui.NewTester(h.view, 400, 400) + if err := tt.Click("Item 1"); err != nil { + t.Fatal(err) + } + if err := tt.ClickWith(ui.Cmd, "Item 4"); err != nil { + t.Fatal(err) + } + if err := tt.ClickWith(ui.Shift, "Item 6"); err != nil { + t.Fatal(err) + } + // Shift extends from the anchor click (MyGo keeps the pivot at item 1). + for id := 1; id <= 6; id++ { + if !h.st.Selection.Has(id) { + t.Fatalf("item %d not chosen; %d chosen", id, h.st.Selection.Len()) + } + } + if h.st.Selection.Len() != 6 { + t.Fatalf("chosen %d, want 6", h.st.Selection.Len()) + } + tt.Key(ui.Cmd, ui.KeyA) + if h.st.Selection.Len() != 10 { + t.Fatalf("select all chose %d", h.st.Selection.Len()) + } +} + +func TestListGridKeyboard(t *testing.T) { + h := newListHarness(12) + h.opts.Grid = true + h.opts.ItemWidth, h.opts.ItemHeight = 80, 60 + h.listSize = [2]float32{380, 300} + tt := ui.NewTester(h.view, 420, 340) + if err := tt.Click("Item 0"); err != nil { + t.Fatal(err) + } + tt.Key(0, ui.KeyRight) + if k, _ := h.st.Selected(); k != 1 { + t.Fatalf("Right chose %d", k) + } + tt.Key(0, ui.KeyDown) + if k, _ := h.st.Selected(); k <= 1 { + t.Fatalf("Down chose %d, want a later row", k) + } +} + +func TestListStatuses(t *testing.T) { + h := newListHarness(0) + tt := ui.NewTester(h.view, 400, 400) + if !tt.HasText("No items") { + t.Fatalf("empty: %q", tt.Texts()) + } + h.opts.Empty = "Nothing in the vault" + tt.Frame() + if !tt.HasText("Nothing in the vault") { + t.Fatalf("custom empty: %q", tt.Texts()) + } + h.opts.Status = DataLoading + tt.Frame() + if !tt.HasText("Loading") || tt.HasText("Nothing") { + t.Fatalf("loading: %q", tt.Texts()) + } + h.opts.Status, h.opts.Error = DataFailed, "Vault sealed" + tt.Frame() + if err := tt.Click("Retry"); err != nil || h.retries != 1 || !tt.HasText("Vault sealed") { + t.Fatalf("failed: %v retries %d texts %q", err, h.retries, tt.Texts()) + } + h.ids = []int{1, 2} + h.opts.Status = DataLoadingMore + tt.Frame() + if !tt.HasText("Item 2") || !tt.HasText("Loading more") { + t.Fatalf("loading more: %q", tt.Texts()) + } +} + +func TestListLargeIsVirtualized(t *testing.T) { + h := newListHarness(100_000) + tt := ui.NewTester(h.view, 400, 400) + // Rows are at least 36 DIPs: 360 DIPs show 10, plus overscan. + const bound = 24 + if h.built == 0 || h.built > bound { + t.Fatalf("first frame built %d items, bound %d", h.built, bound) + } + h.st.ScrollTo(73_000) + tt.Frame() + if h.built > bound || !tt.HasText("Item 73000") { + t.Fatalf("after scroll built %d, texts %q", h.built, tt.Texts()) + } + if err := tt.Click("Item 73001"); err != nil { + t.Fatal(err) + } + if k, _ := h.st.Selected(); k != 73001 { + t.Fatalf("selected %d", k) + } +} + +func TestListSnapshot(t *testing.T) { + names := []string{"Matins", "Lauds", "Prime", "Terce", "Sext", "None"} + snapshot(t, "list", 640, 260, func(c *ui.Context) { + st := ui.Local(c.Root(), "st", func() *ListState[int] { + s := &ListState[int]{} + s.Selection.Add(1) + s.Selection.Add(2) + return s + }) + g := ui.Local(c.Root(), "g", func() *ListState[int] { return &ListState[int]{started: true, cursor: 4} }) + opts := ListOptions[int]{Key: func(i int) int { return i }, Label: func(i int) string { return names[i] }, Multiple: true} + ui.Row(c).Padding(16).Gap(16).Children(func() { + List(c, *st, len(names), opts, func(i int) { ui.Text(c, names[i]) }).Size(220, 228) + gopts := opts + gopts.Grid, gopts.Multiple, gopts.ItemWidth, gopts.ItemHeight = true, false, 100, 64 + List(c, *g, len(names), gopts, func(i int) { + ui.Text(c, names[i]).FontWeight(600) + ui.Text(c, "Office").TextColor(Tokens(c).TextMuted) + }).Size(360, 228) + }) + }) +} diff --git a/messages_data.go b/messages_data.go new file mode 100644 index 0000000..d5fe967 --- /dev/null +++ b/messages_data.go @@ -0,0 +1,36 @@ +package mujica + +func init() { + addMessages(map[string][2]string{ + "list.empty": {"No items", "暂无数据"}, + "list.loading": {"Loading", "加载中"}, + "list.loadingMore": {"Loading more", "正在加载更多"}, + "list.failed": {"Could not load", "加载失败"}, + "list.retry": {"Retry", "重试"}, + "data_table.noMatch": {"No matching rows", "没有匹配的行"}, + "data_table.selectRow": {"Select row", "选择行"}, + "data_table.columns": {"Columns", "列"}, + "timeline.done": {"Done", "已完成"}, + "timeline.active": {"Current", "进行中"}, + "timeline.warning": {"Warning", "警告"}, + "timeline.error": {"Error", "错误"}, + "description_list.copy": {"Copy", "复制"}, + "description_list.copied": {"Copied", "已复制"}, + "statistic.up": {"up", "上升"}, + "statistic.down": {"down", "下降"}, + "statistic.flat": {"unchanged", "持平"}, + "carousel.prev": {"Previous slide", "上一项"}, + "carousel.next": {"Next slide", "下一项"}, + "carousel.pause": {"Pause autoplay", "暂停自动播放"}, + "carousel.play": {"Resume autoplay", "继续自动播放"}, + "carousel.position": {"Slide %d of %d", "第 %d 项,共 %d 项"}, + "image_viewer.title": {"Image viewer", "图片查看器"}, + "image_viewer.position": {"Image %d of %d", "第 %d 张,共 %d 张"}, + "image_viewer.zoomIn": {"Zoom in", "放大"}, + "image_viewer.zoomOut": {"Zoom out", "缩小"}, + "image_viewer.fit": {"Fit to window", "适应窗口"}, + "image_viewer.close": {"Close", "关闭"}, + "image_viewer.prev": {"Previous image", "上一张"}, + "image_viewer.next": {"Next image", "下一张"}, + }) +} diff --git a/statistic.go b/statistic.go new file mode 100644 index 0000000..2530b5c --- /dev/null +++ b/statistic.go @@ -0,0 +1,113 @@ +package mujica + +import ( + "math" + "strconv" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/theme" +) + +// StatisticOptions configure a Statistic. It formats values; it computes none. +type StatisticOptions struct { + Title string + // Precision is the number of decimals, 0 or more. + Precision int + // Prefix and Suffix wrap the number, as "$" or "×"; Unit follows it, smaller. + Prefix, Suffix, Unit string + // Format replaces the built-in grouped decimal formatting. + Format func(v float64) string + // Change, when set, shows the caller's change with a sign and an arrow. + Change *float64 + ChangeUnit string + // ChangeNote explains the change, as "vs last week". + ChangeNote string +} + +// Statistic shows a headline number with its title, unit and change. +func Statistic(c *ui.Context, value float64, opts StatisticOptions) *ui.Element { + if opts.Precision < 0 { + panic("mujica: Statistic with negative Precision") + } + k := Tokens(c) + u := Density(c).Unit() + format := opts.Format + if format == nil { + format = func(v float64) string { return statisticFormat(v, opts.Precision) } + } + num := opts.Prefix + format(value) + opts.Suffix + spoken := strings.TrimSpace(opts.Title + " " + num + " " + opts.Unit) + e := ui.Column(c).Gap(u).Role(ui.RoleGroup) + e.Children(func() { + if opts.Title != "" { + ui.Text(c, opts.Title).FontSize(theme.CaptionSize).TextColor(k.TextMuted) + } + ui.Row(c).Gap(u).AlignItems(ui.End).Children(func() { + ui.Text(c, num).Font(theme.SerifFont).FontSize(theme.H1Size).FontWeight(600).LineHeight(1.2).SingleLine() + if opts.Unit != "" { + ui.Text(c, opts.Unit).TextColor(k.TextMuted).Padding(0, 0, u, 0) + } + }) + if opts.Change == nil { + return + } + ch := *opts.Change + col, icon, word := k.TextMuted, "minus", msg(c, "statistic.flat") + switch { + case ch > 0: + col, icon, word = k.Success, "arrow-up", msg(c, "statistic.up") + case ch < 0: + col, icon, word = k.Danger, "arrow-down", msg(c, "statistic.down") + } + text := statisticSigned(ch, opts.Precision) + opts.ChangeUnit + spoken += ", " + word + " " + text + ui.Row(c).Gap(u).AlignItems(ui.Center).Wrap().Children(func() { + ui.Row(c).Gap(u / 2).AlignItems(ui.Center).TextColor(col).Children(func() { + ui.Icon(c, icons.Must(icon)).Size(14, 14) + ui.Text(c, text).FontWeight(600).SingleLine() + }) + if opts.ChangeNote != "" { + ui.Text(c, opts.ChangeNote).FontSize(theme.CaptionSize).TextColor(k.TextMuted) + } + }) + }) + return e.Label(spoken) +} + +// statisticFormat writes v with p decimals and comma-grouped thousands. +func statisticFormat(v float64, p int) string { + if math.IsNaN(v) || math.IsInf(v, 0) { + return strconv.FormatFloat(v, 'f', -1, 64) + } + s := strconv.FormatFloat(math.Abs(v), 'f', p, 64) + whole, frac, _ := strings.Cut(s, ".") + var b strings.Builder + if v < 0 && strings.Trim(s, "0.") != "" { + b.WriteString("−") + } + for i, r := range whole { + if i > 0 && (len(whole)-i)%3 == 0 { + b.WriteByte(',') + } + b.WriteRune(r) + } + if frac != "" { + b.WriteString("." + frac) + } + return b.String() +} + +// statisticSigned writes a change with an explicit sign. +func statisticSigned(v float64, p int) string { + s := statisticFormat(math.Abs(v), p) + switch { + case v > 0: + return "+" + s + case v < 0: + return "−" + s + } + return s +} diff --git a/statistic_test.go b/statistic_test.go new file mode 100644 index 0000000..d72c92b --- /dev/null +++ b/statistic_test.go @@ -0,0 +1,81 @@ +package mujica + +import ( + "math" + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestStatisticFormat(t *testing.T) { + for _, tc := range []struct { + v float64 + p int + want string + }{ + {1234567.891, 2, "1,234,567.89"}, + {999, 0, "999"}, + {1000, 0, "1,000"}, + {-42.5, 1, "−42.5"}, + {-0.001, 1, "0.0"}, + {0, 3, "0.000"}, + {math.Inf(1), 0, "+Inf"}, + } { + if got := statisticFormat(tc.v, tc.p); got != tc.want { + t.Errorf("format(%v, %d) = %q, want %q", tc.v, tc.p, got, tc.want) + } + } +} + +func TestStatisticChange(t *testing.T) { + for _, tc := range []struct { + change float64 + text string + word string + }{{3.26, "+3.3%", "up"}, {-1.5, "−1.5%", "down"}, {0, "0.0%", "unchanged"}} { + ch := tc.change + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Statistic(c, 12840, StatisticOptions{Title: "Pilgrims", Prefix: "≈", Unit: "souls", Precision: 1, Change: &ch, ChangeUnit: "%", ChangeNote: "vs last feast"}) + }, 400, 200) + for _, s := range []string{"Pilgrims", "≈12,840.0", "souls", tc.text, "vs last feast"} { + if !tt.HasText(s) { + t.Fatalf("%v: missing %q in %q", tc.change, s, tt.Texts()) + } + } + want := "Pilgrims ≈12,840.0 souls, " + tc.word + " " + tc.text + if _, ok := tt.Find(want); !ok { + t.Fatalf("spoken label %q missing", want) + } + } +} + +func TestStatisticCustomFormatAndBadPrecision(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Statistic(c, 0.5, StatisticOptions{Format: func(v float64) string { return "half" }}) + }, 200, 100) + if !tt.HasText("half") { + t.Fatalf("custom format: %q", tt.Texts()) + } + defer func() { + if recover() == nil { + t.Fatal("no panic") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Statistic(c, 1, StatisticOptions{Precision: -1}) + }, 100, 100) +} + +func TestStatisticSnapshot(t *testing.T) { + up, down := 12.5, -3.0 + snapshot(t, "statistic", 560, 140, func(c *ui.Context) { + ui.Row(c).Padding(20).Gap(40).AlignItems(ui.Start).Children(func() { + Statistic(c, 12840, StatisticOptions{Title: "Pilgrims", Unit: "souls", Change: &up, ChangeUnit: "%", Precision: 1, ChangeNote: "vs last feast"}) + Statistic(c, 3.75, StatisticOptions{Title: "Tithe", Prefix: "£", Precision: 2, Change: &down, ChangeUnit: "%"}) + Statistic(c, 7, StatisticOptions{Title: "Bells"}) + }) + }) +} diff --git a/testdata/snapshots/darwin/accordion-dark.png b/testdata/snapshots/darwin/accordion-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..4c96e3421760c3831a9a4715ceeeb167a1de9898 GIT binary patch literal 11341 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/card-comfortable-dark.png b/testdata/snapshots/darwin/card-comfortable-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..10d4d65d9c781c2a31569507f494d9bcf7655726 GIT binary patch literal 9358 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/card-comfortable-light.png b/testdata/snapshots/darwin/card-comfortable-light.png new file mode 100644 index 0000000000000000000000000000000000000000..c4168ec092d14bcfb4c328533c574254ca12c1bc GIT binary patch literal 9052 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 G@P7do6qY*x literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/card-compact-dark.png b/testdata/snapshots/darwin/card-compact-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..dd1317fa1bbf572ef0556ce116fb5c31e36f0d6f GIT binary patch literal 9355 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 I7e;~q1t#14VgLXD literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/card-compact-light.png b/testdata/snapshots/darwin/card-compact-light.png new file mode 100644 index 0000000000000000000000000000000000000000..bafc0a0737ae760c41e9b6d582c8849727c3b0d8 GIT binary patch literal 9029 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 HGYR}(P12{y literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/card-dark.png b/testdata/snapshots/darwin/card-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..5c1aca26c42df7e8bac5029565ebb86be3ef3cc7 GIT binary patch literal 18950 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/carousel-dark.png b/testdata/snapshots/darwin/carousel-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..5b767a699b00b2dc26744ddb644283b7b8be4cce GIT binary patch literal 5713 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/carousel-light.png b/testdata/snapshots/darwin/carousel-light.png new file mode 100644 index 0000000000000000000000000000000000000000..21726d2417834176643437ad9f43ca455c9ead8b GIT binary patch literal 5656 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/collapsible-dark.png b/testdata/snapshots/darwin/collapsible-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a2867f89dd25906e313222d3878130eafcb6ba0f GIT binary patch literal 9453 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 Ls&tK{dC30*IyXdG literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/data_table-dark.png b/testdata/snapshots/darwin/data_table-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..df9f8dad5d513a22e9cb5dda1da22660c819ac59 GIT binary patch literal 12573 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 I!X)&60Z&~O*Z=?k literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/data_table-light.png b/testdata/snapshots/darwin/data_table-light.png new file mode 100644 index 0000000000000000000000000000000000000000..49c7d33dee2aa1ccfc6595c5935813de2cf1df58 GIT binary patch literal 12230 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/statistic-dark.png b/testdata/snapshots/darwin/statistic-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b5f86683590c49143d45fae0b44259d9ae834350 GIT binary patch literal 13556 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 F{|_4LX!-yE literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/statistic-light.png b/testdata/snapshots/darwin/statistic-light.png new file mode 100644 index 0000000000000000000000000000000000000000..89d0effac09f28f688922f170e187d2b15e5c77e GIT binary patch literal 13125 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%0ABL-;N2CN1`Hv}$F)@By9?1R+%OCnL%J8LSW!?|j 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 NNGSZM5c?JIzW^F}5XArh literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/timeline-dark.png b/testdata/snapshots/darwin/timeline-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..6746ee2857192661258cfb1a0e327aa66f27ecde GIT binary patch literal 28085 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 Ang9R* literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/timeline-light.png b/testdata/snapshots/darwin/timeline-light.png new file mode 100644 index 0000000000000000000000000000000000000000..c81e718cca3e39b6627bb71ad5e31239fe7c5602 GIT binary patch literal 26700 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

bArqj7nzK8@5P}pYNokm84g^*fOq8;@L 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 I%bAA!7pM*7dH?_b literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/tree-dark.png b/testdata/snapshots/darwin/tree-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..896dd199f70e5aa22fccee6bab261fb4bfeba5cf GIT binary patch literal 9132 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/tree_table-dark.png b/testdata/snapshots/darwin/tree_table-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..e2eb09e708dc8beb35626bc2238169062f01f7a2 GIT binary patch literal 9912 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 PwqU6%Yb#YKSbzE-)nSE} literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/tree_table-light.png b/testdata/snapshots/darwin/tree_table-light.png new file mode 100644 index 0000000000000000000000000000000000000000..8d71e7bba583e3ff6ae2c24062b4d20865eef969 GIT binary patch literal 9545 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 literal 0 HcmV?d00001 diff --git a/timeline.go b/timeline.go new file mode 100644 index 0000000..115f077 --- /dev/null +++ b/timeline.go @@ -0,0 +1,115 @@ +package mujica + +import ( + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// TimelineStatus marks an event's state with a shape and a word, not only color. +type TimelineStatus int + +const ( + TimelineDefault TimelineStatus = iota + TimelineDone + TimelineActive + TimelineWarning + TimelineError +) + +// TimelineItem is one event of a Timeline. +type TimelineItem struct { + Time string + Title string + Content string + Status TimelineStatus +} + +// Timeline lists events top to bottom with markers joined by a line; long text wraps. +func Timeline(c *ui.Context, items []TimelineItem) *ui.Element { + k := Tokens(c) + u := Density(c).Unit() + return ui.Column(c).Role(ui.RoleList).Children(func() { + for i, it := range items { + last := i == len(items)-1 + ui.Row(c).Gap(u * 3).AlignItems(ui.Stretch).Role(ui.RoleListItem).Children(func() { + ui.Column(c).Width(20).Shrink(0).AlignItems(ui.Center).Children(func() { + timelineMarker(c, it.Status) + if !last { + ui.Box(c).Width(1).Grow(1).MinHeight(u * 3).Background(k.Border) + } + }) + ui.Column(c).Grow(1).MinWidth(0).Gap(u).Padding(0, 0, u*5, 0).Children(func() { + ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Wrap().Children(func() { + if it.Time != "" { + ui.Text(c, it.Time).FontSize(theme.CaptionSize).TextColor(k.TextMuted) + } + if w := timelineWord(c, it.Status); w != "" { + ui.Text(c, w).FontSize(theme.CaptionSize).TextColor(timelineColor(k, it.Status)).FontWeight(600) + } + }) + if it.Title != "" { + ui.Text(c, it.Title).FontWeight(600) + } + if it.Content != "" { + ui.Text(c, it.Content).TextColor(k.TextMuted) + } + }) + }) + } + }) +} + +func timelineColor(k theme.Tokens, s TimelineStatus) ui.Color { + switch s { + case TimelineDone: + return k.Success + case TimelineActive: + return k.AccentText + case TimelineWarning: + return k.Warning + case TimelineError: + return k.Danger + } + return k.TextMuted +} + +func timelineWord(c *ui.Context, s TimelineStatus) string { + switch s { + case TimelineDone: + return msg(c, "timeline.done") + case TimelineActive: + return msg(c, "timeline.active") + case TimelineWarning: + return msg(c, "timeline.warning") + case TimelineError: + return msg(c, "timeline.error") + } + return "" +} + +// timelineMarker draws a hollow diamond, or a status icon. +func timelineMarker(c *ui.Context, s TimelineStatus) { + k := Tokens(c) + col := timelineColor(k, s) + box := ui.Box(c).Size(20, 20).Shrink(0).Center().TextColor(col) + switch s { + case TimelineDone: + box.Children(func() { ui.Icon(c, icons.Must("circle-check")).Size(18, 18) }) + case TimelineWarning: + box.Children(func() { ui.Icon(c, icons.Must("triangle-alert")).Size(18, 18) }) + case TimelineError: + box.Children(func() { ui.Icon(c, icons.Must("circle-x")).Size(18, 18) }) + default: + box.Draw(func(p *ui.Painter, r ui.Rect) { + cx, cy := internal.Snap(p, r.X+r.W/2), internal.Snap(p, r.Y+r.H/2) + if s == TimelineActive { + p.FillPath(internal.Diamond(cx, cy, 12), col) + } else { + p.StrokePath(internal.Diamond(cx, cy, 11), 1.5, col) + } + }) + } +} diff --git a/timeline_test.go b/timeline_test.go new file mode 100644 index 0000000..e7a6a92 --- /dev/null +++ b/timeline_test.go @@ -0,0 +1,50 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +var timelineItems = []TimelineItem{ + {Time: "06:00", Title: "Matins sung", Content: "The choir gathered before dawn.", Status: TimelineDone}, + {Time: "09:00", Title: "Relics moved", Status: TimelineActive, + Content: "The reliquaries travel from the crypt to the high altar, carried in procession by four acolytes in black and wine."}, + {Time: "12:00", Title: "Bell cracked", Status: TimelineError}, + {Time: "18:00", Title: "Vespers"}, +} + +func TestTimelineOrderAndStatus(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Width(260).Children(func() { Timeline(c, timelineItems) }) + }, 300, 600) + var ys []float32 + for _, title := range []string{"Matins sung", "Relics moved", "Bell cracked", "Vespers"} { + r, ok := tt.Find(title) + if !ok { + t.Fatalf("missing %q", title) + } + ys = append(ys, r.Y) + } + for i := 1; i < len(ys); i++ { + if ys[i] <= ys[i-1] { + t.Fatalf("not top to bottom: %v", ys) + } + } + for _, w := range []string{"Done", "Current", "Error"} { + if !tt.HasText(w) { + t.Fatalf("status word %q missing", w) + } + } + long, _ := tt.Find(timelineItems[1].Content) + if long.H < 3*14 || long.W > 260 { + t.Fatalf("long content did not wrap: %v", long) + } +} + +func TestTimelineSnapshot(t *testing.T) { + snapshot(t, "timeline", 420, 380, func(c *ui.Context) { + ui.Column(c).Padding(20).Children(func() { Timeline(c, timelineItems) }) + }) +} diff --git a/tree.go b/tree.go new file mode 100644 index 0000000..d659ceb --- /dev/null +++ b/tree.go @@ -0,0 +1,183 @@ +package mujica + +import ( + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// TreeOptions configure a Tree of items of type K, which are also their keys. +type TreeOptions[K comparable] struct { + Roots []K + // Children returns an item's children: nil for a leaf, empty while unloaded; required. + Children func(item K) []K + // Label returns an item's text, read out and matched by typing; required. + Label func(item K) string + // ItemStatus returns the load state of an item's children; nil means loaded. + ItemStatus func(item K) DataStatus + // Load asks for an item's children when it opens DataUnloaded, and on retry. + Load func(item K) + Multiple bool + // Empty replaces the built-in "No items" text. + Empty string + Status DataStatus + Error string +} + +// TreeState is a Tree's open items, selection and place. +type TreeState[K comparable] struct { + // Selection holds the chosen items of a Multiple tree. + Selection ui.Selection[K] + tree treeCore[K] +} + +// Selected returns the chosen item, or the last chosen of a Multiple tree. +func (s *TreeState[K]) Selected() (K, bool) { return s.tree.sel, s.tree.has } + +// SetOpen opens or closes an item. +func (s *TreeState[K]) SetOpen(item K, open bool) { s.tree.setOpen(item, open) } + +// IsOpen reports whether an item shows its children. +func (s *TreeState[K]) IsOpen(item K) bool { return s.tree.outline.Open.Has(item) } + +// treeCore is the outline state shared by Tree and TreeTable. +type treeCore[K comparable] struct { + outline ui.OutlineState[K] + cursor int + started bool + sel K + has bool + // asked holds the open items whose children Load was asked for. + asked map[K]bool +} + +func (t *treeCore[K]) setOpen(item K, open bool) { + if open { + t.outline.Open.Add(item) + } else { + t.outline.Open.Remove(item) + } +} + +// begin wires the outline's selection before it is built. +func (t *treeCore[K]) begin(label func(K) string, sel *ui.Selection[K], multiple bool) { + if !t.started { + t.started, t.cursor, t.asked = true, -1, map[K]bool{} + } + t.outline.List.Selected = &t.cursor + t.outline.List.Label = func(i int) string { return label(t.outline.Item(i)) } + t.outline.List.Selection = nil + if multiple { + t.outline.List.Selection = sel + } +} + +// end reads the chosen item after the outline was built. +func (t *treeCore[K]) end() { + t.has = t.cursor >= 0 && t.cursor < t.outline.Rows() + if t.has { + t.sel = t.outline.Item(t.cursor) + } +} + +// chosen reports whether item shows as chosen. +func (t *treeCore[K]) chosen(item K, sel *ui.Selection[K], multiple bool) bool { + if multiple { + return sel.Has(item) + } + return t.cursor >= 0 && t.cursor < t.outline.Rows() && t.outline.Item(t.cursor) == item +} + +// itemStatus asks for unloaded children of an open item and shows loading or failure. +func (t *treeCore[K]) itemStatus(c *ui.Context, item K, status func(K) DataStatus, load func(K)) { + if status == nil { + return + } + st := status(item) + open := t.outline.Open.Has(item) + if st != DataUnloaded || !open { + delete(t.asked, item) + } + if st == DataUnloaded && open && !t.asked[item] { + if load == nil { + panic("mujica: tree item unloaded without Load") + } + t.asked[item] = true + slog.Debug("mujica: tree load", "item", item) + load(item) + } + k := Tokens(c) + u := Density(c).Unit() + switch st { + case DataLoading: + Spinner(c, SpinnerOptions{Size: 14, Label: msg(c, "list.loading"), ShowLabel: true}) + case DataFailed: + ui.Row(c).Gap(u).AlignItems(ui.Center).TextColor(k.Danger).Children(func() { + ui.Icon(c, icons.Must("circle-alert")).Size(14, 14) + ui.Text(c, msg(c, "list.failed")).FontSize(theme.CaptionSize).SingleLine() + }) + if Button(c, msg(c, "list.retry"), ButtonOptions{Variant: Ghost}).MinHeight(theme.MinHitSize).Clicked() { + slog.Debug("mujica: tree retry", "item", item) + load(item) + } + } +} + +// TreeView is a built Tree and what happened to it in this frame. +type TreeView struct { + *ui.Element + changed, submitted, retry bool +} + +// Changed reports a new selection. +func (v TreeView) Changed() bool { return v.changed } + +// Submitted reports Enter or a double click on an item. +func (v TreeView) Submitted() bool { return v.submitted } + +// Retried reports a click on the retry action of a failed first load. +func (v TreeView) Retried() bool { return v.retry } + +// Tree shows items in a hierarchy, building only the rows in view. +func Tree[K comparable](c *ui.Context, s *TreeState[K], opts TreeOptions[K]) TreeView { + if opts.Children == nil || opts.Label == nil { + panic("mujica: Tree needs Children and Label") + } + k := Tokens(c) + d := Density(c) + u := d.Unit() + t := &s.tree + t.begin(opts.Label, &s.Selection, opts.Multiple) + var v TreeView + v.Element = ui.Column(c).MinHeight(0) + v.Children(func() { + if len(opts.Roots) == 0 { + v.retry = dataPlaceholder(c, opts.Status, opts.Error, opts.Empty, msg(c, "list.empty")) + return + } + // Outline rows pad by one unit above and below. + h := d.ControlHeight() - 2*c.Theme().Space(1) + e := ui.Outline(c, &t.outline, opts.Roots, opts.Children, func(item K) { + ui.Row(c).Grow(1).MinWidth(0).MinHeight(h).Gap(u * 2).AlignItems(ui.Center).Children(func() { + ui.Text(c, opts.Label(item)).SingleLine().Grow(1).MinWidth(0) + t.itemStatus(c, item, opts.ItemStatus, opts.Load) + if t.chosen(item, &s.Selection, opts.Multiple) { + ui.Icon(c, icons.Must("check")).Size(16, 16).Shrink(0) + } + }) + }) + e.Grow(1).MinHeight(0).Padding(u).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius) + internal.Ring(e, k.Focus, theme.ControlRadius) + v.changed, v.submitted = e.Changed(), e.Submitted() + v.retry = dataFooter(c, opts.Status, opts.Error) + }) + t.end() + if v.changed { + slog.Debug("mujica: tree changed", "row", t.cursor, "count", s.Selection.Len()) + } + return v +} diff --git a/tree_table.go b/tree_table.go new file mode 100644 index 0000000..43313bb --- /dev/null +++ b/tree_table.go @@ -0,0 +1,177 @@ +package mujica + +import ( + "log/slog" + "slices" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// TreeColumn is a column of a TreeTable; the first shows the arrows. +type TreeColumn[K comparable] struct { + // ID identifies the column in sort and layout; required, unique. + ID string + Title string + // Width is in DIPs; 0 shares the room left. MinWidth bounds resizing. + Width, MinWidth float32 + Align ui.Align + // Compare orders siblings, as cmp.Compare does; nil makes the column unsortable. + Compare func(a, b K) int + // Cell builds the item's cell in this column; required. + Cell func(item K) +} + +// TreeTableOptions configure a TreeTable of items of type K. +type TreeTableOptions[K comparable] struct { + Columns []TreeColumn[K] + Roots []K + // Children returns an item's children: nil for a leaf, empty while unloaded; required. + // Return a new slice when they change: sorted copies are cached by slice. + Children func(item K) []K + // Label returns an item's text, read out and matched by typing; required. + Label func(item K) string + // ItemStatus returns the load state of an item's children; nil means loaded. + ItemStatus func(item K) DataStatus + // Load asks for an item's children when it opens DataUnloaded, and on retry. + Load func(item K) + Multiple bool + Empty string + Status DataStatus + Error string +} + +// TreeTableState holds a TreeTable's sort, layout, open items and selection. +type TreeTableState[K comparable] struct { + // Sort orders siblings by a column; the header shows it. + Sort ui.SortOrder + // Layout is the column order and widths the user set. + Layout ui.TableLayout + // Selection holds the chosen items of a Multiple table. + Selection ui.Selection[K] + tree treeCore[K] + sorted map[K]treeSorted[K] + roots treeSorted[K] +} + +// treeSorted caches one sibling list sorted, until its source or the sort changes. +type treeSorted[K comparable] struct { + src *K + n int + sort ui.SortOrder + out []K +} + +// Selected returns the chosen item, or the last chosen of a Multiple table. +func (s *TreeTableState[K]) Selected() (K, bool) { return s.tree.sel, s.tree.has } + +// SetOpen opens or closes an item. +func (s *TreeTableState[K]) SetOpen(item K, open bool) { s.tree.setOpen(item, open) } + +// IsOpen reports whether an item shows its children. +func (s *TreeTableState[K]) IsOpen(item K) bool { return s.tree.outline.Open.Has(item) } + +// TreeTable shows a hierarchy in sortable, resizable columns, building only rows in view. +func TreeTable[K comparable](c *ui.Context, s *TreeTableState[K], opts TreeTableOptions[K]) DataTableView { + treeTableCheck(opts) + k := Tokens(c) + d := Density(c) + t := &s.tree + t.begin(opts.Label, &s.Selection, opts.Multiple) + if s.sorted == nil { + s.sorted = map[K]treeSorted[K]{} + } + var cmp func(a, b K) int + cols := make([]ui.TableColumn, len(opts.Columns)) + for i, col := range opts.Columns { + cols[i] = ui.TableColumn{ID: col.ID, Title: col.Title, Width: col.Width, MinWidth: col.MinWidth, Align: col.Align, Sortable: col.Compare != nil} + if col.ID == s.Sort.Column && col.Compare != nil { + cmp = col.Compare + } + } + order := s.Sort + sorted := func(src []K, cache *treeSorted[K]) []K { + if cmp == nil || len(src) < 2 { + return src + } + if cache.src == &src[0] && cache.n == len(src) && cache.sort == order { + return cache.out + } + out := slices.Clone(src) + slices.SortStableFunc(out, func(a, b K) int { + if order.Descending { + return cmp(b, a) + } + return cmp(a, b) + }) + *cache = treeSorted[K]{src: &src[0], n: len(src), sort: order, out: out} + return out + } + children := func(item K) []K { + kids := opts.Children(item) + if len(kids) < 2 || cmp == nil { + return kids + } + cache := s.sorted[item] + out := sorted(kids, &cache) + s.sorted[item] = cache + return out + } + var v DataTableView + v.Element = ui.Column(c).MinHeight(0) + v.Children(func() { + if len(opts.Roots) == 0 { + v.retry = dataPlaceholder(c, opts.Status, opts.Error, opts.Empty, msg(c, "list.empty")) + return + } + roots := sorted(opts.Roots, &s.roots) + before := s.Sort + t.outline.List.Sort, t.outline.List.Columns = &s.Sort, s.Layout + saved := c.Theme() + th := *saved + th.Spacing = d.ControlHeight() / 8 + c.SetTheme(&th) + e := ui.OutlineTable(c, &t.outline, cols, roots, children, func(item K, col int) { + if col > 0 { + opts.Columns[col].Cell(item) + return + } + ui.Row(c).Grow(1).MinWidth(0).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { + ui.Row(c).Grow(1).MinWidth(0).AlignItems(ui.Center).Children(func() { opts.Columns[0].Cell(item) }) + t.itemStatus(c, item, opts.ItemStatus, opts.Load) + if t.chosen(item, &s.Selection, opts.Multiple) { + ui.Icon(c, icons.Must("check")).Size(16, 16).Shrink(0) + } + }) + }) + c.SetTheme(saved) + e.Grow(1).MinHeight(0).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius) + internal.Ring(e, k.Focus, theme.ControlRadius) + s.Layout = t.outline.List.Columns + v.sorted = s.Sort != before + v.changed = e.Changed() && !v.sorted + v.submitted = e.Submitted() + v.retry = dataFooter(c, opts.Status, opts.Error) + }) + t.end() + if v.sorted { + slog.Debug("mujica: tree_table sort", "column", s.Sort.Column, "descending", s.Sort.Descending) + } + return v +} + +func treeTableCheck[K comparable](opts TreeTableOptions[K]) { + if opts.Children == nil || opts.Label == nil || len(opts.Columns) == 0 { + panic("mujica: TreeTable needs Columns, Children and Label") + } + seen := map[string]bool{} + for _, col := range opts.Columns { + if col.ID == "" || seen[col.ID] || col.Cell == nil { + panic("mujica: TreeTable column needs a unique ID and Cell: " + col.ID) + } + seen[col.ID] = true + } +} diff --git a/tree_table_test.go b/tree_table_test.go new file mode 100644 index 0000000..1f4f02a --- /dev/null +++ b/tree_table_test.go @@ -0,0 +1,192 @@ +package mujica + +import ( + "cmp" + "fmt" + "strconv" + "testing" + + "github.com/egoist/mygo/ui" +) + +type ttHarness struct { + kids map[string][]string + size map[string]int + status map[string]DataStatus + loads int + st TreeTableState[string] + opts TreeTableOptions[string] + compares int + sorts int + c *ui.Context +} + +func newTTHarness() *ttHarness { + h := &ttHarness{ + kids: map[string][]string{"Vestry": {"Stole", "Alb", "Cope"}, "Belfry": {}}, + size: map[string]int{"Vestry": 9, "Belfry": 4, "Stole": 2, "Alb": 5, "Cope": 1, "Organ": 7}, + status: map[string]DataStatus{"Belfry": DataUnloaded}, + } + text := func(n string) { ui.Text(h.c, n).SingleLine() } + h.opts = TreeTableOptions[string]{ + Roots: []string{"Vestry", "Belfry", "Organ"}, + Children: func(n string) []string { return h.kids[n] }, + Label: func(n string) string { return n }, + ItemStatus: func(n string) DataStatus { return h.status[n] }, + Load: func(n string) { h.loads++; h.status[n] = DataLoading }, + Columns: []TreeColumn[string]{ + {ID: "name", Title: "Name", Cell: text, + Compare: func(a, b string) int { h.compares++; return cmp.Compare(a, b) }}, + {ID: "size", Title: "Size", Width: 80, Align: ui.End, + Cell: func(n string) { ui.Text(h.c, strconv.Itoa(h.size[n])) }, + Compare: func(a, b string) int { h.compares++; return cmp.Compare(h.size[a], h.size[b]) }}, + }, + } + return h +} + +func (h *ttHarness) view(c *ui.Context) { + Use(c, Settings{Mode: Light}) + h.c = c + ui.Column(c).Padding(10).Children(func() { + v := TreeTable(c, &h.st, h.opts) + v.Size(360, 320) + if v.SortChanged() { + h.sorts++ + } + }) +} + +func (h *ttHarness) order(tt *ui.Tester) string { + var out []string + for _, s := range tt.Texts() { + if _, ok := h.size[s]; ok && (len(out) == 0 || out[len(out)-1] != s) { + out = append(out, s) + } + } + return fmt.Sprint(out) +} + +func TestTreeTableSortsSiblings(t *testing.T) { + h := newTTHarness() + tt := ui.NewTester(h.view, 400, 360) + h.st.SetOpen("Vestry", true) + tt.Frame() + if got := h.order(tt); got != "[Vestry Stole Alb Cope Belfry Organ]" { + t.Fatalf("unsorted %s", got) + } + if err := tt.Click("Alb"); err != nil { + t.Fatal(err) + } + if err := tt.Click("Size"); err != nil { + t.Fatal(err) + } + if got := h.order(tt); got != "[Belfry Organ Vestry Cope Stole Alb]" || h.sorts != 1 { + t.Fatalf("by size %s sorts %d", got, h.sorts) + } + if k, _ := h.st.Selected(); k != "Alb" { + t.Fatalf("selection moved to %q", k) + } + n := h.compares + tt.Frame() + tt.Frame() + if h.compares != n { + t.Fatalf("re-sorted without a change: %d", h.compares-n) + } + _ = tt.Click("Size") + if got := h.order(tt); got != "[Vestry Alb Stole Cope Organ Belfry]" { + t.Fatalf("descending %s", got) + } +} + +func TestTreeTableLazyAndKeys(t *testing.T) { + h := newTTHarness() + tt := ui.NewTester(h.view, 400, 360) + _ = tt.Click("Belfry") + tt.Key(0, ui.KeyRight) + if !h.st.IsOpen("Belfry") || h.loads != 1 || !tt.HasText("Loading") { + t.Fatalf("open %v loads %d texts %q", h.st.IsOpen("Belfry"), h.loads, tt.Texts()) + } + h.kids["Belfry"] = []string{"Rope"} + h.size["Rope"] = 3 + h.status["Belfry"] = DataReady + tt.Key(0, ui.KeyRight) + if k, _ := h.st.Selected(); k != "Rope" { + t.Fatalf("Right chose %q", k) + } + tt.Key(0, ui.KeyLeft) + tt.Key(0, ui.KeyLeft) + if h.st.IsOpen("Belfry") || tt.HasText("Rope") { + t.Fatal("Left did not close") + } +} + +func TestTreeTableLayout(t *testing.T) { + h := newTTHarness() + h.st.Layout = ui.TableLayout{Order: []string{"size", "name"}, Widths: map[string]float32{"size": 120}} + tt := ui.NewTester(h.view, 400, 360) + size, _ := tt.Find("Size") + name, _ := tt.Find("Name") + if size.X >= name.X || h.st.Layout.Widths["size"] != 120 { + t.Fatalf("layout lost: size %v name %v %v", size.X, name.X, h.st.Layout) + } +} + +func TestTreeTableBadOptions(t *testing.T) { + h := newTTHarness() + h.opts.Columns[1].Cell = nil + defer func() { + if recover() == nil { + t.Fatal("no panic without Cell") + } + }() + ui.NewTester(h.view, 300, 300) +} + +func TestTreeTableLargeIsVirtualized(t *testing.T) { + roots, kids := bigTree() + var st TreeTableState[int] + rows := map[int]bool{} + cell := func(c *ui.Context) func(int) { + return func(n int) { rows[n] = true; ui.Text(c, strconv.Itoa(n)) } + } + view := func(c *ui.Context) { + Use(c, Settings{}) + clear(rows) + TreeTable(c, &st, TreeTableOptions[int]{Roots: roots, Children: kids, Label: strconv.Itoa, + Columns: []TreeColumn[int]{{ID: "n", Title: "N", Cell: cell(c), Compare: cmp.Compare[int]}, {ID: "m", Title: "M", Cell: cell(c)}}}).Size(320, 360) + } + tt := ui.NewTester(view, 400, 400) + const bound = 26 // see TestTreeLargeIsVirtualized + if len(rows) == 0 || len(rows) > bound || rows[1] { + t.Fatalf("collapsed: %d rows", len(rows)) + } + for _, r := range roots { + st.SetOpen(r, true) + } + tt.Frame() + if len(rows) > bound || st.tree.outline.Rows() != 100_000 { + t.Fatalf("open: %d rows of %d", len(rows), st.tree.outline.Rows()) + } + st.Sort = ui.SortOrder{Column: "n", Descending: true} + tt.Frame() + if len(rows) > bound || st.tree.outline.Item(0) != 99_000 || st.tree.outline.Item(1) != 99_999 { + t.Fatalf("sorted: %d rows, first %d %d", len(rows), st.tree.outline.Item(0), st.tree.outline.Item(1)) + } +} + +func TestTreeTableSnapshot(t *testing.T) { + snapshot(t, "tree_table", 420, 240, func(c *ui.Context) { + h := ui.Local(c.Root(), "h", func() *ttHarness { + h := newTTHarness() + h.st.SetOpen("Vestry", true) + h.st.Sort = ui.SortOrder{Column: "name"} + h.st.tree.started, h.st.tree.cursor, h.st.tree.asked = true, 2, map[string]bool{} + return h + }) + (*h).c = c + ui.Column(c).Padding(16).Children(func() { + TreeTable(c, &(*h).st, (*h).opts).Size(388, 208) + }) + }) +} diff --git a/tree_test.go b/tree_test.go new file mode 100644 index 0000000..d258570 --- /dev/null +++ b/tree_test.go @@ -0,0 +1,222 @@ +package mujica + +import ( + "fmt" + "testing" + + "github.com/egoist/mygo/ui" +) + +type treeHarness struct { + kids map[string][]string + status map[string]DataStatus + loads []string + st TreeState[string] + opts TreeOptions[string] + built map[string]bool + changes int +} + +func newTreeHarness() *treeHarness { + h := &treeHarness{ + kids: map[string][]string{ + "Nave": {"Pulpit", "Pews"}, + "Crypt": {}, // loaded lazily + "Pulpit": nil, + "Pews": nil, + "Sacristy": nil, + }, + status: map[string]DataStatus{"Crypt": DataUnloaded}, + } + h.opts = TreeOptions[string]{ + Roots: []string{"Nave", "Crypt", "Sacristy"}, + Children: func(n string) []string { return h.kids[n] }, + Label: func(n string) string { return n }, + ItemStatus: func(n string) DataStatus { return h.status[n] }, + Load: func(n string) { + h.loads = append(h.loads, n) + h.status[n] = DataLoading + }, + } + return h +} + +func (h *treeHarness) view(c *ui.Context) { + Use(c, Settings{Mode: Light}) + h.built = map[string]bool{} + opts := h.opts + label := opts.Label + opts.Label = func(n string) string { h.built[n] = true; return label(n) } + ui.Column(c).Padding(10).Children(func() { + v := Tree(c, &h.st, opts) + v.Size(320, 360) + if v.Changed() { + h.changes++ + } + }) +} + +func TestTreeExpandAndKeyboard(t *testing.T) { + h := newTreeHarness() + tt := ui.NewTester(h.view, 400, 400) + if tt.HasText("Pulpit") || h.built["Pulpit"] { + t.Fatal("closed subtree built") + } + if err := tt.Click("Nave"); err != nil { + t.Fatal(err) + } + if k, _ := h.st.Selected(); k != "Nave" || h.changes != 1 { + t.Fatalf("selected %q changes %d", k, h.changes) + } + tt.Key(0, ui.KeyRight) + if !h.st.IsOpen("Nave") || !tt.HasText("Pulpit") { + t.Fatalf("Right did not open: %q", tt.Texts()) + } + tt.Key(0, ui.KeyRight) + if k, _ := h.st.Selected(); k != "Pulpit" { + t.Fatalf("Right to first child chose %q", k) + } + tt.Key(0, ui.KeyDown) + if k, _ := h.st.Selected(); k != "Pews" { + t.Fatalf("Down chose %q", k) + } + tt.Key(0, ui.KeyLeft) + if k, _ := h.st.Selected(); k != "Nave" { + t.Fatalf("Left to parent chose %q", k) + } + tt.Key(0, ui.KeyLeft) + if h.st.IsOpen("Nave") || tt.HasText("Pews") { + t.Fatal("Left did not close") + } +} + +func TestTreeLazyLoad(t *testing.T) { + h := newTreeHarness() + tt := ui.NewTester(h.view, 400, 400) + if len(h.loads) != 0 { + t.Fatalf("loaded before opening: %v", h.loads) + } + h.st.SetOpen("Crypt", true) + tt.Frame() + if fmt.Sprint(h.loads) != "[Crypt]" || !tt.HasText("Loading") { + t.Fatalf("loads %v texts %q", h.loads, tt.Texts()) + } + tt.Frame() + if len(h.loads) != 1 { + t.Fatalf("asked twice: %v", h.loads) + } + h.status["Crypt"] = DataFailed + tt.Frame() + if err := tt.Click("Retry"); err != nil { + t.Fatal(err) + } + if len(h.loads) != 2 { + t.Fatalf("retry did not load: %v", h.loads) + } + h.kids["Crypt"] = []string{"Ossuary"} + h.status["Crypt"] = DataReady + tt.Frame() + if !tt.HasText("Ossuary") || tt.HasText("Loading") { + t.Fatalf("children missing: %q", tt.Texts()) + } +} + +func TestTreeMultipleAndEmpty(t *testing.T) { + h := newTreeHarness() + h.opts.Multiple = true + tt := ui.NewTester(h.view, 400, 400) + _ = tt.Click("Nave") + _ = tt.ClickWith(ui.Cmd, "Sacristy") + if h.st.Selection.Len() != 2 || !h.st.Selection.Has("Sacristy") { + t.Fatalf("chose %d", h.st.Selection.Len()) + } + h.opts.Roots = nil + tt.Frame() + if !tt.HasText("No items") { + t.Fatalf("empty: %q", tt.Texts()) + } +} + +func TestTreeBadOptions(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic without Children") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + var s TreeState[int] + Tree(c, &s, TreeOptions[int]{Roots: []int{1}}) + }, 100, 100) +} + +// bigTree has 100 roots of 999 children each: 100,000 nodes. +func bigTree() (roots []int, kids func(int) []int) { + for r := range 100 { + roots = append(roots, r*1000) + } + cache := map[int][]int{} + return roots, func(n int) []int { + if n%1000 != 0 { + return nil + } + if k, ok := cache[n]; ok { + return k + } + var k []int + for i := 1; i < 1000; i++ { + k = append(k, n+i) + } + cache[n] = k + return k + } +} + +func TestTreeLargeIsVirtualized(t *testing.T) { + roots, kids := bigTree() + var st TreeState[int] + built := 0 + view := func(c *ui.Context) { + Use(c, Settings{}) + built = 0 + Tree(c, &st, TreeOptions[int]{Roots: roots, Children: kids, Label: func(n int) string { + built++ + return fmt.Sprintf("Node %d", n) + }}).Size(320, 360) + } + tt := ui.NewTester(view, 400, 400) + // Label runs twice per built row (row name and text). A jump builds a + // view on each side of its target: 2×10 rows of 36 DIPs, plus 2×2 overscan and 2 edges. + const bound = 2 * 26 + if built == 0 || built > bound { + t.Fatalf("collapsed: label calls %d, bound %d", built, bound) + } + for _, r := range roots { + st.SetOpen(r, true) + } + tt.Frame() + if built > bound || st.tree.outline.Rows() != 100_000 { + t.Fatalf("open: label calls %d rows %d", built, st.tree.outline.Rows()) + } + st.tree.outline.List.ScrollTo(64_000, ui.Start) + tt.Frame() + if built > bound || !tt.HasText(fmt.Sprintf("Node %d", st.tree.outline.Item(64_000))) { + t.Fatalf("scrolled: label calls %d texts %q", built, tt.Texts()) + } +} + +func TestTreeSnapshot(t *testing.T) { + snapshot(t, "tree", 400, 260, func(c *ui.Context) { + h := ui.Local(c.Root(), "h", func() *treeHarness { + h := newTreeHarness() + h.st.SetOpen("Nave", true) + h.st.SetOpen("Crypt", true) + h.status["Crypt"] = DataLoading + h.st.tree.started, h.st.tree.cursor, h.st.tree.asked = true, 1, map[string]bool{} + return h + }) + ui.Column(c).Padding(16).Children(func() { + Tree(c, &(*h).st, (*h).opts).Size(360, 228) + }) + }) +}