From 6c399117b010304d18df9135768bb8a00434bed0 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:18:12 -0400 Subject: [PATCH] Display: components 011-020 --- avatar.go | 82 +++ avatar_group.go | 128 +++++ avatar_group_test.go | 90 ++++ avatar_test.go | 62 +++ badge.go | 144 +++++ badge_test.go | 114 ++++ docs/components/011-text.md | 69 +++ docs/components/012-heading.md | 43 ++ docs/components/013-icon.md | 47 ++ docs/components/014-image.md | 60 +++ docs/components/015-avatar.md | 47 ++ docs/components/016-avatar-group.md | 54 ++ docs/components/017-badge.md | 50 ++ docs/components/018-tag.md | 63 +++ docs/components/019-link.md | 53 ++ docs/components/020-kbd.md | 55 ++ example_display_test.go | 134 +++++ examples/gallery/pages_display.go | 495 +++++++++++++++++- examples/gallery/pages_display_parts.go | 32 ++ heading.go | 64 +++ heading_test.go | 78 +++ icon.go | 98 ++++ icon_test.go | 88 ++++ image.go | 86 +++ image_test.go | 113 ++++ internal/display.go | 27 + kbd.go | 80 +++ kbd_test.go | 80 +++ link.go | 81 +++ link_test.go | 95 ++++ messages_display.go | 16 + tag.go | 109 ++++ tag_test.go | 92 ++++ testdata/snapshots/darwin/avatar-dark.png | Bin 0 -> 6314 bytes testdata/snapshots/darwin/avatar-light.png | Bin 0 -> 6194 bytes .../snapshots/darwin/avatar_group-dark.png | Bin 0 -> 7889 bytes .../snapshots/darwin/avatar_group-light.png | Bin 0 -> 7581 bytes testdata/snapshots/darwin/badge-dark.png | Bin 0 -> 6189 bytes testdata/snapshots/darwin/badge-light.png | Bin 0 -> 6239 bytes testdata/snapshots/darwin/heading-dark.png | Bin 0 -> 16066 bytes testdata/snapshots/darwin/heading-light.png | Bin 0 -> 15798 bytes testdata/snapshots/darwin/icon-dark.png | Bin 0 -> 5241 bytes testdata/snapshots/darwin/icon-light.png | Bin 0 -> 5115 bytes testdata/snapshots/darwin/image-dark.png | Bin 0 -> 7050 bytes testdata/snapshots/darwin/image-light.png | Bin 0 -> 6942 bytes testdata/snapshots/darwin/kbd-dark.png | Bin 0 -> 2646 bytes testdata/snapshots/darwin/kbd-light.png | Bin 0 -> 2550 bytes testdata/snapshots/darwin/link-dark.png | Bin 0 -> 10794 bytes testdata/snapshots/darwin/link-light.png | Bin 0 -> 10421 bytes testdata/snapshots/darwin/tag-dark.png | Bin 0 -> 5625 bytes testdata/snapshots/darwin/tag-light.png | Bin 0 -> 5269 bytes testdata/snapshots/darwin/text-dark.png | Bin 0 -> 21270 bytes testdata/snapshots/darwin/text-light.png | Bin 0 -> 20426 bytes text.go | 85 +++ text_test.go | 137 +++++ 55 files changed, 3150 insertions(+), 1 deletion(-) create mode 100644 avatar.go create mode 100644 avatar_group.go create mode 100644 avatar_group_test.go create mode 100644 avatar_test.go create mode 100644 badge.go create mode 100644 badge_test.go create mode 100644 docs/components/011-text.md create mode 100644 docs/components/012-heading.md create mode 100644 docs/components/013-icon.md create mode 100644 docs/components/014-image.md create mode 100644 docs/components/015-avatar.md create mode 100644 docs/components/016-avatar-group.md create mode 100644 docs/components/017-badge.md create mode 100644 docs/components/018-tag.md create mode 100644 docs/components/019-link.md create mode 100644 docs/components/020-kbd.md create mode 100644 example_display_test.go create mode 100644 examples/gallery/pages_display_parts.go create mode 100644 heading.go create mode 100644 heading_test.go create mode 100644 icon.go create mode 100644 icon_test.go create mode 100644 image.go create mode 100644 image_test.go create mode 100644 internal/display.go create mode 100644 kbd.go create mode 100644 kbd_test.go create mode 100644 link.go create mode 100644 link_test.go create mode 100644 messages_display.go create mode 100644 tag.go create mode 100644 tag_test.go create mode 100644 testdata/snapshots/darwin/avatar-dark.png create mode 100644 testdata/snapshots/darwin/avatar-light.png create mode 100644 testdata/snapshots/darwin/avatar_group-dark.png create mode 100644 testdata/snapshots/darwin/avatar_group-light.png create mode 100644 testdata/snapshots/darwin/badge-dark.png create mode 100644 testdata/snapshots/darwin/badge-light.png create mode 100644 testdata/snapshots/darwin/heading-dark.png create mode 100644 testdata/snapshots/darwin/heading-light.png create mode 100644 testdata/snapshots/darwin/icon-dark.png create mode 100644 testdata/snapshots/darwin/icon-light.png create mode 100644 testdata/snapshots/darwin/image-dark.png create mode 100644 testdata/snapshots/darwin/image-light.png create mode 100644 testdata/snapshots/darwin/kbd-dark.png create mode 100644 testdata/snapshots/darwin/kbd-light.png create mode 100644 testdata/snapshots/darwin/link-dark.png create mode 100644 testdata/snapshots/darwin/link-light.png create mode 100644 testdata/snapshots/darwin/tag-dark.png create mode 100644 testdata/snapshots/darwin/tag-light.png create mode 100644 testdata/snapshots/darwin/text-dark.png create mode 100644 testdata/snapshots/darwin/text-light.png create mode 100644 text.go create mode 100644 text_test.go diff --git a/avatar.go b/avatar.go new file mode 100644 index 0000000..fdc2848 --- /dev/null +++ b/avatar.go @@ -0,0 +1,82 @@ +package mujica + +import ( + "unicode" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +// AvatarOptions configure an Avatar. +type AvatarOptions struct { + // Image is the picture; nil falls back to initials, then a person icon. + Image *ui.Bitmap + // Size is the diameter in DIPs; zero means 32. + Size float32 +} + +// Avatar shows a person as a round picture, their initials or an icon. +// Assistive technology reads it as an image named name. +func Avatar(c *ui.Context, name string, opts AvatarOptions) *ui.Element { + size := opts.Size + if size < 0 { + panic("mujica: Avatar Size must not be negative") + } + if size == 0 { + size = 32 + } + k := Tokens(c) + label := name + if label == "" { + label = msg(c, "avatar.unnamed") + } + e := ui.Box(c).Size(size, size).Radius(size / 2).Clip().Shrink(0).Center().Role(ui.RoleImage).Label(label) + initials := avatarInitials(name) + e.Children(func() { + switch { + case opts.Image != nil: + ui.Image(c, opts.Image).Fit(ui.Cover).Size(size, size) + case initials != "": + e.Background(k.Selection).Border(theme.BorderWidth, k.Border) + ui.Text(c, initials).Font(theme.SerifFont).FontSize(size * avatarInitialsScale(initials)).FontWeight(600). + TextColor(k.AccentText).SingleLine() + default: + e.Background(k.SurfaceHover).Border(theme.BorderWidth, k.Border) + Icon(c, "user", IconOptions{Size: size * 0.5, Tone: IconMuted}) + } + }) + return e +} + +// avatarInitials returns the first letters of the first and last words. +func avatarInitials(name string) string { + var first, last rune + inWord := false + for _, r := range name { + letter := unicode.IsLetter(r) || unicode.IsDigit(r) + if letter && !inWord { + if first == 0 { + first = unicode.ToUpper(r) + } else { + last = unicode.ToUpper(r) + } + } + inWord = letter + } + switch { + case first == 0: + return "" + case last == 0: + return string(first) + } + return string([]rune{first, last}) +} + +// avatarInitialsScale fits two letters inside the circle. +func avatarInitialsScale(initials string) float32 { + if len([]rune(initials)) > 1 { + return 0.34 + } + return 0.42 +} diff --git a/avatar_group.go b/avatar_group.go new file mode 100644 index 0000000..0a755f0 --- /dev/null +++ b/avatar_group.go @@ -0,0 +1,128 @@ +package mujica + +import ( + "fmt" + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// AvatarItem is one person of an AvatarGroup. +type AvatarItem struct { + Name string + Image *ui.Bitmap +} + +// AvatarGroupOptions configure an AvatarGroup. +type AvatarGroupOptions struct { + // Max is how many avatars show before the "+N" button; zero means 4. + Max int + // Size is each avatar's diameter; zero means 32. + Size float32 +} + +type avatarGroupKey struct{} + +// AvatarGroupView is a shown AvatarGroup and its full list's state. +type AvatarGroupView struct { + Element *ui.Element + // Open reports whether the full list shows. + Open bool + // Toggled reports the full list opened or closed this frame. + Toggled bool +} + +// AvatarGroup shows overlapping avatars. Past Max, a "+N" button opens a +// popover with the full list of names. +func AvatarGroup(c *ui.Context, items []AvatarItem, opts AvatarGroupOptions) AvatarGroupView { + limit := opts.Max + if limit < 0 { + panic("mujica: AvatarGroup Max must not be negative") + } + if limit == 0 { + limit = 4 + } + size := opts.Size + if size < 0 { + panic("mujica: AvatarGroup Size must not be negative") + } + if size == 0 { + size = 32 + } + k := Tokens(c) + shown := min(limit, len(items)) + ring := float32(2) + row := ui.Row(c).AlignItems(ui.Center).Role(ui.RoleGroup) + open := ui.Local(row, avatarGroupKey{}, func() bool { return false }) + was := *open + row.Children(func() { + for i, it := range items[:shown] { + cell := ui.Box(c).Key(i).Padding(ring).Radius(size/2 + ring).Background(k.Background).Shrink(0) + if i > 0 { + cell.Margin(0, 0, 0, -size/4) + } + cell.Children(func() { Avatar(c, it.Name, AvatarOptions{Image: it.Image, Size: size}) }) + } + rest := len(items) - shown + if rest == 0 { + *open = false + return + } + more := avatarGroupMore(c, k, size, ring, rest, len(items)) + if more.Clicked() { + *open = !*open + } + ui.PopoverBase(c, more, open, func(panel *ui.Element) { + avatarGroupList(c, k, panel, items) + }) + }) + v := AvatarGroupView{Element: row, Open: *open, Toggled: *open != was} + if v.Toggled { + slog.Debug("mujica: avatargroup toggle", "open", v.Open) + } + return v +} + +func avatarGroupMore(c *ui.Context, k theme.Tokens, size, ring float32, rest, total int) *ui.Element { + cell := ui.Box(c).Key("more").Padding(ring).Radius(size/2+ring).Background(k.Background). + Margin(0, 0, 0, -size/4).Shrink(0) + var b *ui.Element + cell.Children(func() { + b = ui.ButtonBase(c).Size(size, size).Radius(size / 2).Label(fmt.Sprintf(msg(c, "avatar_group.more"), total)) + bg := k.Surface + if b.Pressed() { + bg = k.Selection + } else if b.Hovered() { + bg = k.SurfaceHover + } + b.Background(bg).Border(theme.BorderWidth, k.ControlBorder).TextColor(k.Text) + internal.Ring(b, k.Focus, size/2) + internal.Fade(b, Motion(c, theme.StateDuration)) + b.Children(func() { + ui.Text(c, fmt.Sprintf("+%d", rest)).FontSize(size * 0.36).FontWeight(600).SingleLine() + }) + }) + return b +} + +func avatarGroupList(c *ui.Context, k theme.Tokens, panel *ui.Element, items []AvatarItem) { + u := Density(c).Unit() + panel.Margin(u, 0, 0, 0).Padding(u*2).Radius(theme.PopupRadius).Background(k.Surface). + Border(theme.BorderWidth, k.Border).MinWidth(200) + if d := Motion(c, theme.PopupDuration); d > 0 { + panel.Transition(ui.ElementTransition{Duration: d, Enter: &ui.Motion{Y: -4}}) + } + ui.Scroll(c).MaxHeight(240).Children(func() { + ui.Column(c).Gap(u * 2).Role(ui.RoleList).Children(func() { + for i, it := range items { + ui.Row(c).Key(i).Gap(u * 2).AlignItems(ui.Center).Role(ui.RoleListItem).Children(func() { + Avatar(c, it.Name, AvatarOptions{Image: it.Image, Size: 24}) + ui.Text(c, it.Name).SingleLine() + }) + } + }) + }) +} diff --git a/avatar_group_test.go b/avatar_group_test.go new file mode 100644 index 0000000..229add8 --- /dev/null +++ b/avatar_group_test.go @@ -0,0 +1,90 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +var avatarGroupPeople = []AvatarItem{ + {Name: "Sakiko Togawa"}, {Name: "Uika Misumi"}, {Name: "Mutsumi Wakaba"}, + {Name: "Umiri Yahata"}, {Name: "Nyamu Yutenji"}, {Name: "Tomori Takamatsu"}, +} + +func TestAvatarGroupMaxAndRest(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + AvatarGroup(c, avatarGroupPeople, AvatarGroupOptions{Max: 3}) + }, 400, 300) + tt.Frame() + if !tt.HasText("+3") { + t.Fatalf("texts %v", tt.Texts()) + } + if _, ok := tt.Find("Umiri Yahata"); ok { + t.Fatal("fourth avatar shown past Max") + } +} + +func TestAvatarGroupNoOverflowNoButton(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + AvatarGroup(c, avatarGroupPeople[:2], AvatarGroupOptions{}) + }, 400, 300) + tt.Frame() + if tt.HasText("+") { + t.Fatalf("overflow button with 2 of 4: %v", tt.Texts()) + } +} + +func TestAvatarGroupExpandByKeyboard(t *testing.T) { + var toggles []bool + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if v := AvatarGroup(c, avatarGroupPeople, AvatarGroupOptions{Max: 2}); v.Toggled { + toggles = append(toggles, v.Open) + } + }, 400, 400) + tt.Key(0, ui.KeyTab) + if !tt.Focused("Show all 6 people") { + t.Fatal("overflow button not focused") + } + tt.Key(0, ui.KeyEnter) + for _, p := range avatarGroupPeople { + if !tt.HasText(p.Name) { + t.Fatalf("list lacks %q: %v", p.Name, tt.Texts()) + } + } + tt.Key(0, ui.KeyEscape) + if tt.HasText("Tomori Takamatsu") { + t.Fatal("list still open after Escape") + } + if len(toggles) != 2 || !toggles[0] || toggles[1] { + t.Fatalf("toggles %v, want [true false]", toggles) + } +} + +func TestAvatarGroupExpandByClick(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + AvatarGroup(c, avatarGroupPeople, AvatarGroupOptions{Max: 4}) + }, 400, 400) + if err := tt.Click("+2"); err != nil { + t.Fatal(err) + } + if !tt.HasText("Tomori Takamatsu") { + t.Fatal("list not open") + } + tt.ClickAt(390, 390) + if tt.HasText("Tomori Takamatsu") { + t.Fatal("list open after outside click") + } +} + +func TestAvatarGroupSnapshot(t *testing.T) { + snapshot(t, "avatar_group", 320, 260, func(c *ui.Context) { + ui.Column(c).Padding(20).Gap(16).AlignItems(ui.Start).Children(func() { + AvatarGroup(c, avatarGroupPeople[:3], AvatarGroupOptions{}) + AvatarGroup(c, avatarGroupPeople, AvatarGroupOptions{Max: 3, Size: 40}) + }) + }) +} diff --git a/avatar_test.go b/avatar_test.go new file mode 100644 index 0000000..ff44c7d --- /dev/null +++ b/avatar_test.go @@ -0,0 +1,62 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestAvatarInitials(t *testing.T) { + for name, want := range map[string]string{ + "Sakiko Togawa": "ST", + "uika": "U", + "Anon Chihaya Mortis": "AM", + " ": "", + "张伟": "张", + "mutsumi (wakaba) 若叶睦": "M若", + } { + if got := avatarInitials(name); got != want { + t.Errorf("%q: %q, want %q", name, got, want) + } + } +} + +func TestAvatarFallbacks(t *testing.T) { + var size ui.Rect + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Row(c).AlignItems(ui.Start).Children(func() { + size = Avatar(c, "Sakiko Togawa", AvatarOptions{Size: 48}).Bounds() + Avatar(c, "", AvatarOptions{}) + Avatar(c, "Photo Person", AvatarOptions{Image: imageTestBitmap()}) + }) + }, 300, 100) + tt.Frame() + if size.W != 48 || size.H != 48 { + t.Fatalf("size %v", size) + } + if !tt.HasText("ST") { + t.Fatal("initials missing") + } + if _, ok := tt.Find("Unnamed"); !ok { + t.Fatal("unnamed avatar not labeled") + } + if _, ok := tt.Find("Photo Person"); !ok { + t.Fatal("image avatar not labeled") + } + if tt.HasText("PP") { + t.Fatal("initials shown over image") + } +} + +func TestAvatarSnapshot(t *testing.T) { + snapshot(t, "avatar", 320, 90, func(c *ui.Context) { + ui.Row(c).Padding(20).Gap(12).AlignItems(ui.Center).Children(func() { + Avatar(c, "Sakiko Togawa", AvatarOptions{Size: 48}) + Avatar(c, "Uika", AvatarOptions{}) + Avatar(c, "", AvatarOptions{Size: 40}) + Avatar(c, "Photo", AvatarOptions{Image: imageTestBitmap(), Size: 40}) + Avatar(c, "Mutsumi Wakaba", AvatarOptions{Size: 24}) + }) + }) +} diff --git a/badge.go b/badge.go new file mode 100644 index 0000000..8697378 --- /dev/null +++ b/badge.go @@ -0,0 +1,144 @@ +package mujica + +import ( + "fmt" + "strconv" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +// BadgeTone is a badge's emphasis. The zero value is BadgeAccent. +type BadgeTone int + +const ( + BadgeAccent BadgeTone = iota + BadgeNeutral + BadgeSuccess + BadgeWarning + BadgeDanger + BadgeInfo +) + +// BadgePosition places a badge. The zero value lays it out inline. +type BadgePosition int + +const ( + BadgeInline BadgePosition = iota + BadgeTopRight + BadgeTopLeft + BadgeBottomRight + BadgeBottomLeft +) + +// BadgeOptions configure a Badge. +type BadgeOptions struct { + // Text shows as is; when empty, Count shows instead. + Text string + // Count must not be negative; zero with no Text hides the badge. + Count int + // Max caps the shown count as "Max+"; zero means 99. + Max int + Tone BadgeTone + Position BadgePosition + // Label says what the badge counts; it is read before the full value. + Label string +} + +// Badge shows a short text or count. Inline badges take no content; corner +// positions pin the badge on content's corner. +func Badge(c *ui.Context, opts BadgeOptions, content func()) *ui.Element { + if opts.Count < 0 || opts.Max < 0 { + panic("mujica: Badge Count and Max must not be negative") + } + if (opts.Position == BadgeInline) != (content == nil) { + panic("mujica: Badge needs content exactly when Position is not BadgeInline") + } + shown, full := badgeText(opts) + if opts.Position == BadgeInline { + if shown == "" { + return ui.Box(c).Size(0, 0) + } + return badgePill(c, opts, shown, full) + } + at := badgeAnchor(opts.Position) + wrap := ui.Box(c).Shrink(0).AlignSelf(ui.Start).AlignItems(ui.Start) + wrap.Children(func() { + content() + if shown != "" { + badgePill(c, opts, shown, full).Attach(at, ui.AnchorCenter) + } + }) + return wrap +} + +// badgeText returns what the badge shows and what it reads. +func badgeText(opts BadgeOptions) (shown, full string) { + if opts.Text != "" { + return opts.Text, opts.Text + } + if opts.Count == 0 { + return "", "" + } + limit := opts.Max + if limit == 0 { + limit = 99 + } + full = strconv.Itoa(opts.Count) + if opts.Count > limit { + return strconv.Itoa(limit) + "+", full + } + return full, full +} + +func badgePill(c *ui.Context, opts BadgeOptions, shown, full string) *ui.Element { + k := Tokens(c) + bg, fg, border := badgeColors(k, opts.Tone) + label := full + if opts.Label != "" { + label = opts.Label + " " + full + } + p := ui.Box(c).Row().Center().MinWidth(18).Height(18).Padding(0, 5).Radius(theme.CardRadius). + Background(bg).Shrink(0).Role(ui.RoleStatus).Label(label) + if border.A > 0 { + p.Border(theme.BorderWidth, border) + } + p.Children(func() { + ui.Text(c, shown).FontSize(theme.CaptionSize - 1).FontWeight(600).TextColor(fg). + FontFeatures("tnum").SingleLine() + }) + return p +} + +func badgeColors(k theme.Tokens, t BadgeTone) (bg, fg, border ui.Color) { + switch t { + case BadgeAccent: + return k.Accent, k.OnAccent, ui.Color{} + case BadgeNeutral: + return k.SurfaceHover, k.Text, k.Border + case BadgeSuccess: + return k.Surface, k.Success, k.Success + case BadgeWarning: + return k.Surface, k.Warning, k.Warning + case BadgeDanger: + return k.DangerFill, k.OnDanger, ui.Color{} + case BadgeInfo: + return k.Surface, k.Info, k.Info + } + panic(fmt.Sprintf("mujica: unknown BadgeTone %d", t)) +} + +func badgeAnchor(p BadgePosition) ui.Anchor { + switch p { + case BadgeTopRight: + return ui.AnchorTopRight + case BadgeTopLeft: + return ui.AnchorTopLeft + case BadgeBottomRight: + return ui.AnchorBottomRight + case BadgeBottomLeft: + return ui.AnchorBottomLeft + } + panic(fmt.Sprintf("mujica: unknown BadgePosition %d", p)) +} diff --git a/badge_test.go b/badge_test.go new file mode 100644 index 0000000..399de54 --- /dev/null +++ b/badge_test.go @@ -0,0 +1,114 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestBadgeText(t *testing.T) { + for _, tc := range []struct { + opts BadgeOptions + shown, full string + }{ + {BadgeOptions{Count: 7}, "7", "7"}, + {BadgeOptions{Count: 99}, "99", "99"}, + {BadgeOptions{Count: 100}, "99+", "100"}, + {BadgeOptions{Count: 12, Max: 9}, "9+", "12"}, + {BadgeOptions{Count: 0}, "", ""}, + {BadgeOptions{Text: "New", Count: 5}, "New", "New"}, + } { + shown, full := badgeText(tc.opts) + if shown != tc.shown || full != tc.full { + t.Errorf("%+v: %q %q", tc.opts, shown, full) + } + } +} + +func TestBadgeReadsFullValue(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Badge(c, BadgeOptions{Count: 1204, Label: "Unread"}, nil) + }, 300, 100) + tt.Frame() + if !tt.HasText("99+") { + t.Fatalf("texts %v", tt.Texts()) + } + if _, ok := tt.Find("Unread 1204"); !ok { + t.Fatal("full value not read") + } +} + +func TestBadgeCornerPosition(t *testing.T) { + for _, tc := range []struct { + pos BadgePosition + right, down bool + }{{BadgeTopRight, true, false}, {BadgeTopLeft, false, false}, {BadgeBottomRight, true, true}, {BadgeBottomLeft, false, true}} { + var host ui.Rect + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Padding(40).Children(func() { + Badge(c, BadgeOptions{Count: 3, Position: tc.pos}, func() { + host = ui.Box(c).Size(60, 60).Bounds() + }) + }) + }, 300, 300) + tt.Frame() + r, ok := tt.Find("3") + if !ok { + t.Fatalf("%v: badge missing", tc.pos) + } + cx, cy := r.X+r.W/2, r.Y+r.H/2 + wantX, wantY := host.X, host.Y + if tc.right { + wantX += host.W + } + if tc.down { + wantY += host.H + } + if d := cx - wantX; d < -2 || d > 2 { + t.Errorf("%v: x %v, want %v", tc.pos, cx, wantX) + } + if d := cy - wantY; d < -2 || d > 2 { + t.Errorf("%v: y %v, want %v", tc.pos, cy, wantY) + } + } +} + +func TestBadgeBadOptionsPanic(t *testing.T) { + for i, build := range []func(c *ui.Context){ + func(c *ui.Context) { Badge(c, BadgeOptions{Count: -1}, nil) }, + func(c *ui.Context) { Badge(c, BadgeOptions{Count: 1, Position: BadgeTopRight}, nil) }, + func(c *ui.Context) { Badge(c, BadgeOptions{Count: 1}, func() {}) }, + func(c *ui.Context) { Badge(c, BadgeOptions{Count: 1, Tone: BadgeTone(42)}, nil) }, + } { + func() { + defer func() { + if recover() == nil { + t.Fatalf("case %d: no panic", i) + } + }() + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + build(c) + }, 100, 100) + tt.Frame() + }() + } +} + +func TestBadgeSnapshot(t *testing.T) { + snapshot(t, "badge", 460, 100, func(c *ui.Context) { + ui.Row(c).Padding(24).Gap(14).AlignItems(ui.Center).Children(func() { + Badge(c, BadgeOptions{Count: 7}, nil) + Badge(c, BadgeOptions{Count: 250}, nil) + Badge(c, BadgeOptions{Text: "Draft", Tone: BadgeNeutral}, nil) + Badge(c, BadgeOptions{Text: "Live", Tone: BadgeSuccess}, nil) + Badge(c, BadgeOptions{Text: "Late", Tone: BadgeWarning}, nil) + Badge(c, BadgeOptions{Text: "Info", Tone: BadgeInfo}, nil) + Badge(c, BadgeOptions{Count: 3, Tone: BadgeDanger, Position: BadgeTopRight}, func() { + Avatar(c, "Sakiko Togawa", AvatarOptions{}) + }) + }) + }) +} diff --git a/docs/components/011-text.md b/docs/components/011-text.md new file mode 100644 index 0000000..8bb00a7 --- /dev/null +++ b/docs/components/011-text.md @@ -0,0 +1,69 @@ +# 011 Text 文本 + +## 用途 + +正文文本:自动换行、按行数省略、可选择复制。`EditableText` 是 Text 的可编辑模式,就地修改一段短文本(如文件名),并展示调用方的保存进度与保存结果。 + +## 最小示例 + +```go +name := "Coronation.md" +saveErr := "" +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Text(c, "Order ID: 4821-AX", mujica.TextOptions{Selectable: true}) + mujica.Text(c, "A long note that stops after two lines.", mujica.TextOptions{MaxLines: 2}) + if mujica.EditableText(c, &name, mujica.EditableTextOptions{Error: saveErr}).Changed() { + saveErr = "" // save name here; set saveErr on failure + } +} +``` + +## 参数 + +`func Text(c *ui.Context, s string, opts TextOptions) *ui.Element` + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `MaxLines` | `int` | `0` | 超过该行数时以省略号截断;`0` 不截断;`1` 为单行。负数 panic。 | +| `Selectable` | `bool` | `false` | 允许用指针或键盘选择并复制文本。 | + +`func EditableText(c *ui.Context, value *string, opts EditableTextOptions) *ui.Element` + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Saving` | `bool` | `false` | 调用方正在保存:显示加载标记,并禁止再次编辑。 | +| `Error` | `string` | `""` | 调用方保存失败的说明,显示在文本下方并报告给辅助技术。 | +| `Label` | `string` | `""` | 辅助技术读出的名称。 | + +## 状态 + +- 默认:可编辑文本带虚线下边,提示可以编辑。 +- 悬停:背景变为 `SurfaceHover`,尺寸不变。 +- 聚焦:2 DIP 焦点环。 +- 编辑中:显示 MyGo 文本输入框,酒红边框,选中扩展名之前的部分。 +- 保存中(`Saving`):显示加载标记,阻止进入编辑。 +- 错误(`Error`):虚线变为 `Danger`,下方显示错误说明,保留已确认的值。 + +## 事件 + +- `EditableText(...).Changed()`:用户确认了一个新值,`*value` 已更新。调用方在此保存,并通过 `Saving` 与 `Error` 回传结果。 +- 取消编辑不产生事件,`*value` 不变。 + +## 键盘操作 + +| 按键 | 作用 | +| --- | --- | +| Tab | 聚焦可编辑文本 | +| Enter / 双击 | 开始编辑 | +| Enter | 确认 | +| Escape | 取消并恢复原值 | +| 焦点移走 | 视为确认 | +| Cmd/Ctrl+A、Cmd/Ctrl+C | 在 `Selectable` 文本上全选、复制 | + +## 限制 + +- 编辑模式基于 `ui.EditableText`。MyGo 不报告“取消”事件,组件只能保证取消后值不变,不能通知调用方。 +- 编辑模式只支持单行文本。 +- 值为空字符串时文本区域没有可见文字;组件保留最小宽度和虚线,保证仍可点击。 +- MyGo 无法检测文本是否真的被截断,所以截断时不会自动显示完整内容的提示。 diff --git a/docs/components/012-heading.md b/docs/components/012-heading.md new file mode 100644 index 0000000..17425b4 --- /dev/null +++ b/docs/components/012-heading.md @@ -0,0 +1,43 @@ +# 012 Heading 标题 + +## 用途 + +页面与区块的三级衬线标题,可带副标题与旧金色装饰分隔线(对称细线加中央菱形)。 + +## 最小示例 + +```go +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Heading(c, "The Grand Hall", mujica.HeadingOptions{Subtitle: "Winter ceremonies", Divider: true}) + mujica.Heading(c, "Guest Ledger", mujica.HeadingOptions{Level: 2}) +} +``` + +## 参数 + +`func Heading(c *ui.Context, title string, opts HeadingOptions) *ui.Element` + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Level` | `int` | `0`(即 1) | 1、2、3 级,对应 28、22、18 DIP。其他值 panic。 | +| `Subtitle` | `string` | `""` | 标题下方的说明文字,使用 `TextMuted`。 | +| `Divider` | `bool` | `false` | 在下方绘制装饰分隔线,颜色为 `Ornament`。 | + +## 状态 + +纯展示,仅默认状态。字号按系统文本缩放(`Preferences().TextScale`)放大。 + +## 事件 + +无。 + +## 键盘操作 + +不获取焦点。装饰线不进入无障碍树。 + +## 限制 + +- 标题字体为 `theme.SerifFont`(Georgia、宋体类回退);系统缺字时由系统回退字体显示。 +- MyGo v0.2.10 没有 Heading 角色(`ui/access.go` 的 Role 列表中没有),辅助技术把标题读作普通文本,无法按标题层级导航。 +- 分隔线只在 Heading 内绘制,不依赖 Divider 组件。 diff --git a/docs/components/013-icon.md b/docs/components/013-icon.md new file mode 100644 index 0000000..886485f --- /dev/null +++ b/docs/components/013-icon.md @@ -0,0 +1,47 @@ +# 013 Icon 图标 + +## 用途 + +按名称显示 `icons` 包内置的 Lucide SVG 图标,可设尺寸与语义颜色。名称不存在时显示可见的回退标记并写警告日志。 + +## 最小示例 + +```go +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Row(c).Gap(8).Children(func() { + mujica.Icon(c, "star", mujica.IconOptions{}) + mujica.Icon(c, "circle-check", mujica.IconOptions{Size: 20, Tone: mujica.IconSuccess, Label: "Done"}) + }) +} +``` + +## 参数 + +`func Icon(c *ui.Context, name string, opts IconOptions) *ui.Element` + +`name` 必填,空字符串 panic。可用名称见 `icons.Names()`。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Size` | `float32` | `0`(即 16) | 宽高(DIP),建议 16、20、24。负数 panic。 | +| `Tone` | `IconTone` | `IconInherit` | `IconInherit` 沿用文字颜色;另有 `IconMuted`、`IconAccent`、`IconSuccess`、`IconWarning`、`IconDanger`、`IconInfo`。未知值 panic。 | +| `Label` | `string` | `""` | 有含义的图标的无障碍名称;为空时图标视为装饰,不进入无障碍树。 | + +## 状态 + +- 默认:按语义颜色显示图标。 +- 加载失败(名称不存在):在同尺寸方框中用 `Danger` 色绘制虚线框与斜线,提示文字为“缺少图标:名称”,无障碍名称相同。每个元素对每个缺失名称记录一次 `slog.Warn("mujica: icon missing", "name", ...)`。 + +## 事件 + +无。 + +## 键盘操作 + +不获取焦点。 + +## 限制 + +- 只能使用 `icons` 包内置的图标。要显示任意 SVG,可直接用 `ui.Icon`。 +- 回退标记带悬停提示,但图标本身不获取焦点,键盘用户看不到这个提示,只能依赖无障碍名称。 diff --git a/docs/components/014-image.md b/docs/components/014-image.md new file mode 100644 index 0000000..b19325b --- /dev/null +++ b/docs/components/014-image.md @@ -0,0 +1,60 @@ +# 014 Image 图片 + +## 用途 + +显示位图(`*ui.Bitmap`)或 SVG(`*ui.SVG`),可选包含或裁切填充;提供占位、加载中、加载失败三种状态和替代说明。 + +## 最小示例 + +```go +var photo *ui.Bitmap // decode once with ui.DecodeBitmap, outside the view +loading, loadErr := false, errors.New("timeout") +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + img := mujica.Image(c, photo, mujica.ImageOptions{ + Width: 160, Height: 120, Crop: true, Alt: "Hall of mirrors", + Loading: loading, Err: loadErr, + }) + if img.Retry { + loading, loadErr = true, nil // start loading again here + } +} +``` + +## 参数 + +`func Image(c *ui.Context, src ui.ImageSource, opts ImageOptions) ImageView` + +`src` 为 `nil`(未赋值的接口)时显示占位。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Width`, `Height` | `float32` | `0` | 盒子尺寸(DIP)。为 0 时图片使用自身尺寸;占位、加载、错误状态为 160×120。负数 panic。 | +| `Crop` | `bool` | `false` | `true` 覆盖并裁切(`ui.Cover`);`false` 完整放入(`ui.Contain`)。 | +| `Alt` | `string` | `""` | 替代说明,供辅助技术读取;在其他状态下作为描述。 | +| `Loading` | `bool` | `false` | 调用方正在加载。 | +| `Err` | `error` | `nil` | 调用方加载失败;优先于 `Loading`。 | + +返回值 `ImageView`:`Element` 为外层元素;`Retry` 表示本帧点击了重试。 + +## 状态 + +优先级:错误 > 加载中 > 占位 > 图片。 + +- 占位:浅色框内显示图片图标,名称“暂无图片”。 +- 加载中:框内显示 Spinner,名称“正在加载图片”。 +- 错误:显示错误图标、“图片加载失败”和“重试”按钮,`Err.Error()` 作为无障碍错误说明。 + +## 事件 + +- `ImageView.Retry`:用户点击了重试,或在按钮上按 Enter/Space。 + +## 键盘操作 + +只有错误状态的“重试”按钮可以聚焦(Tab),用 Enter 或 Space 触发。 + +## 限制 + +- 组件不读取文件,也不访问网络。解码(`ui.DecodeBitmap`、`ui.ParseSVG`)和异步加载由调用方完成,结果通过 `Window.Update` 回到界面线程,再设置 `Loading` 与 `Err`。 +- SVG 的绘制范围以 MyGo 支持的部分为准:不支持文本、嵌入图片、pattern、marker 和 filter。 +- 接口里装着 nil 指针(如 `(*ui.Bitmap)(nil)`)时不算 `nil`,不会显示占位。 diff --git a/docs/components/015-avatar.md b/docs/components/015-avatar.md new file mode 100644 index 0000000..fedac25 --- /dev/null +++ b/docs/components/015-avatar.md @@ -0,0 +1,47 @@ +# 015 Avatar 头像 + +## 用途 + +圆形人物头像。依次回退:图片 → 姓名首字母 → 默认人物图标。 + +## 最小示例 + +```go +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Row(c).Gap(8).Children(func() { + mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{Size: 40}) + mujica.Avatar(c, "", mujica.AvatarOptions{}) + }) +} +``` + +## 参数 + +`func Avatar(c *ui.Context, name string, opts AvatarOptions) *ui.Element` + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Image` | `*ui.Bitmap` | `nil` | 头像图片,按圆形裁切填充。 | +| `Size` | `float32` | `0`(即 32) | 直径(DIP)。负数 panic。 | + +`name` 用作无障碍名称和首字母来源;为空时无障碍名称是“未命名”。 + +## 状态 + +- 图片:圆形裁切。 +- 首字母:取第一个和最后一个词的首字母(大写),衬线字体,`AccentText` 文字配 `Selection` 底色。 +- 无可用首字母:`SurfaceHover` 底色加人物图标。 + +## 事件 + +无。 + +## 键盘操作 + +不获取焦点。辅助技术把头像读作以 `name` 命名的图片。 + +## 限制 + +- 中文姓名没有空格时按一个词处理,只取第一个字,例如“张伟”显示为“张”。 +- 头像不使用 MyGo `ui.Avatar`,因为它的尺寸固定为字号的 2.25 倍,无法配置(`ui/indicators.go:383`)。 diff --git a/docs/components/016-avatar-group.md b/docs/components/016-avatar-group.md new file mode 100644 index 0000000..4924c2f --- /dev/null +++ b/docs/components/016-avatar-group.md @@ -0,0 +1,54 @@ +# 016 AvatarGroup 头像组 + +## 用途 + +把多个头像重叠排列。超过最大数量时显示“+N”按钮,点开后在弹层中列出完整名单。 + +## 最小示例 + +```go +people := []mujica.AvatarItem{ + {Name: "Sakiko Togawa"}, {Name: "Uika Misumi"}, {Name: "Mutsumi Wakaba"}, + {Name: "Umiri Yahata"}, {Name: "Nyamu Yutenji"}, +} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.AvatarGroup(c, people, mujica.AvatarGroupOptions{Max: 3}) +} +``` + +## 参数 + +`func AvatarGroup(c *ui.Context, items []AvatarItem, opts AvatarGroupOptions) AvatarGroupView` + +`AvatarItem`:`Name string`、`Image *ui.Bitmap`。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Max` | `int` | `0`(即 4) | 最多直接显示的头像数。负数 panic。 | +| `Size` | `float32` | `0`(即 32) | 每个头像的直径。负数 panic。 | + +返回值 `AvatarGroupView`:`Element`;`Open` 表示完整名单正在显示;`Toggled` 表示本帧打开或关闭了名单。 + +## 状态 + +- 默认:头像之间重叠四分之一直径,用 2 DIP 窗口背景色描边分隔。 +- “+N”按钮:悬停、按下改变背景;聚焦时显示 2 DIP 焦点环。 +- 展开:按钮下方弹出 6 DIP 圆角面板,列出全部头像与姓名,超过 240 DIP 时滚动;遵循减少动画设置。 + +## 事件 + +- `AvatarGroupView.Toggled` / `Open`:名单展开或关闭(点击按钮、Enter、Space、Escape、点击外部)。同时记录 `slog.Debug("mujica: avatargroup toggle")`。 + +## 键盘操作 + +| 按键 | 作用 | +| --- | --- | +| Tab | 聚焦“+N”按钮(读作“显示全部 N 人”) | +| Enter / Space | 展开或收起完整名单 | +| Escape | 关闭名单 | + +## 限制 + +- 分隔描边使用窗口 `Background` 颜色,放在 `Surface` 卡片上时描边颜色会与卡片背景不同。 +- 名单中的条目只用于展示,不可选择。 diff --git a/docs/components/017-badge.md b/docs/components/017-badge.md new file mode 100644 index 0000000..ebba68b --- /dev/null +++ b/docs/components/017-badge.md @@ -0,0 +1,50 @@ +# 017 Badge 徽标 + +## 用途 + +显示短文字或计数。计数超过上限时显示为“上限+”。徽标可以内联排列,也可以钉在内容的某个角上。辅助技术朗读完整数值。 + +## 最小示例 + +```go +unread := 120 +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Badge(c, mujica.BadgeOptions{Text: "New", Tone: mujica.BadgeNeutral}, nil) + mujica.Badge(c, mujica.BadgeOptions{Count: unread, Label: "Unread", Position: mujica.BadgeTopRight}, func() { + mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{}) + }) +} +``` + +## 参数 + +`func Badge(c *ui.Context, opts BadgeOptions, content func()) *ui.Element` + +`Position` 为 `BadgeInline` 时 `content` 必须为 `nil`;为角落位置时 `content` 必须不为 `nil`。否则 panic。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Text` | `string` | `""` | 显示的文字;非空时忽略 `Count`。 | +| `Count` | `int` | `0` | 计数。`Text` 为空且计数为 0 时不显示徽标。负数 panic。 | +| `Max` | `int` | `0`(即 99) | 显示上限,超过时显示 `Max+`。负数 panic。 | +| `Tone` | `BadgeTone` | `BadgeAccent` | `BadgeAccent`、`BadgeNeutral`、`BadgeSuccess`、`BadgeWarning`、`BadgeDanger`、`BadgeInfo`。 | +| `Position` | `BadgePosition` | `BadgeInline` | `BadgeTopRight`、`BadgeTopLeft`、`BadgeBottomRight`、`BadgeBottomLeft`:徽标中心对准内容的对应角。 | +| `Label` | `string` | `""` | 计数的含义,朗读时放在完整数值前,如“Unread 1204”。 | + +## 状态 + +纯展示。强调和危险色调使用实色填充(`OnAccent` 配 `Accent`,`OnDanger` 配 `DangerFill`);成功、警告、信息色调只把语义色用在文字和边框上。 + +## 事件 + +无。 + +## 键盘操作 + +不获取焦点。徽标的无障碍名称为 `Label` 加完整数值。 + +## 限制 + +- 角落徽标的位置相对于 `content` 所在的盒子。内容本身有外边距时,徽标不会跟着偏移。 +- 徽标使用 `RoleStatus`,部分屏幕阅读器可能在数值变化时主动播报。 diff --git a/docs/components/018-tag.md b/docs/components/018-tag.md new file mode 100644 index 0000000..f551e6c --- /dev/null +++ b/docs/components/018-tag.md @@ -0,0 +1,63 @@ +# 018 Tag 标签 + +## 用途 + +短标签,可带图标。可以点击,也可以关闭,点击标签和关闭标签是两个独立的操作,各有自己的焦点停靠点。 + +## 最小示例 + +```go +tags := []string{"Baroque", "Gothic"} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Row(c).Gap(8).Children(func() { + for i, t := range tags { + v := mujica.Tag(c, t, mujica.TagOptions{Icon: "star", Clickable: true, Closable: true}) + v.Element.Key(t) + if v.Closed { + tags = append(tags[:i:i], tags[i+1:]...) + } + } + }) +} +``` + +## 参数 + +`func Tag(c *ui.Context, label string, opts TagOptions) TagView` + +`label` 必填,空字符串 panic。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Icon` | `string` | `""` | 图标名称,见 Icon 组件。 | +| `Clickable` | `bool` | `false` | 标签文字部分作为按钮,可以聚焦和点击。 | +| `Closable` | `bool` | `false` | 显示 28×28 DIP 的关闭按钮。 | +| `Disabled` | `bool` | `false` | 禁用点击和关闭,并移出 Tab 顺序。 | + +返回值 `TagView`:`Element`;`Clicked` 表示标签被激活;`Closed` 表示请求关闭。 + +## 状态 + +- 默认:`Surface` 底色、细边框、2 DIP 圆角。 +- 悬停 / 按下:可点击部分和关闭按钮分别变为 `SurfaceHover` / `Selection`。 +- 聚焦:对应部分显示 2 DIP 焦点环。 +- 禁用:由 MyGo 统一调暗,不响应输入。 + +## 事件 + +- `TagView.Clicked`:点击标签文字,或在聚焦时按 Enter/Space。点击关闭按钮不会触发它。 +- `TagView.Closed`:点击关闭按钮,或在关闭按钮上按 Enter/Space,或在聚焦的可点击标签上按 Delete/Backspace。组件不会自行移除标签,由调用方删除数据。 + +## 键盘操作 + +| 按键 | 作用 | +| --- | --- | +| Tab | 先聚焦标签文字(仅 `Clickable`),再聚焦关闭按钮 | +| Enter / Space | 激活当前聚焦的部分 | +| Delete / Backspace | 在可点击标签上请求关闭(仅 `Closable`) | + +## 限制 + +- 不可点击的标签不获取焦点,只能通过关闭按钮操作。 +- 标签不提供“选中”状态;需要可选标签时请使用 Toggle 类组件。 diff --git a/docs/components/019-link.md b/docs/components/019-link.md new file mode 100644 index 0000000..a6a760f --- /dev/null +++ b/docs/components/019-link.md @@ -0,0 +1,53 @@ +# 019 Link 链接 + +## 用途 + +带下划线的链接文本:打开外部地址、在 MyGo Router 中跳转路径,或作为应用内动作。可以显示打开失败的反馈。 + +## 最小示例 + +```go +details := false +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Link(c, "Read the charter", mujica.LinkOptions{URL: "https://example.com/charter"}) + if mujica.Link(c, "Show details", mujica.LinkOptions{}).Clicked() { + details = !details + } +} +``` + +## 参数 + +`func Link(c *ui.Context, label string, opts LinkOptions) *ui.Element` + +`label` 必填,空字符串 panic。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `URL` | `string` | `""` | 有协议的地址用系统默认应用打开。没有协议的路径(如 `/notes/42`、`?tab=info`)在 MyGo Router 中跳转。为空时是应用内动作。 | +| `Error` | `string` | `""` | 调用方提供的打开失败说明,显示在链接下方,也报告给辅助技术。 | + +## 状态 + +- 默认:`AccentText` 颜色,始终带 1 DIP 下划线。外部地址后面带“外部链接”小图标。 +- 悬停 / 按下:文字与下划线变为 `Text` 颜色,布局不变。 +- 聚焦:2 DIP 焦点环。 +- 错误:格式不正确的地址(无法解析,或 http/https 缺少主机名)不会打开,下方显示“无法打开此链接:地址”;`Error` 非空时优先显示 `Error`。 + +## 事件 + +- `Clicked()`:每次点击或按 Enter 都会报告,无论是否打开了地址。同时记录 `slog.Debug("mujica: link activate")`。 + +## 键盘操作 + +| 按键 | 作用 | +| --- | --- | +| Tab | 聚焦链接 | +| Enter | 激活 | + +## 限制 + +- MyGo v0.2.10 不报告打开失败。`ui.Link` 和 `Context.OpenURL` 不返回结果(`ui/context.go:270`、`ui/window.go:583`)。窗口宿主在 goroutine 中调用 `Shell.OpenExternal` 并丢弃它的错误(`content.go:55`)。因此组件只能自行检查地址格式,系统打开失败无法自动显示。需要这种反馈时,传空 `URL`,在 `Clicked()` 后由调用方在 goroutine 中调用 `mygo.Shell.OpenExternal(url)`。它会返回 error,把结果通过 `Window.Update` 写回 `Error`。 +- 路径是否交给 Router 由 MyGo 决定。没有 Router 时,路径会被当作 URL 交给系统打开。 +- 链接不能嵌入 `ui.RichText` 句子中作为行内元素,因为外部图标和错误说明需要额外的行布局。行内链接请直接使用 `ui.Link`。 diff --git a/docs/components/020-kbd.md b/docs/components/020-kbd.md new file mode 100644 index 0000000..cda7863 --- /dev/null +++ b/docs/components/020-kbd.md @@ -0,0 +1,55 @@ +# 020 Kbd 快捷键 + +## 用途 + +把单键或组合键显示为键帽。按平台显示:macOS 用 ⌘ ⇧ ⌥ ⌃ 等符号且不加连接符;其他平台显示 Ctrl、Shift、Alt,键之间用“+”连接。只做展示,不注册快捷键。 + +## 最小示例 + +```go +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Row(c).Gap(6).Children(func() { + mujica.Text(c, "Command palette", mujica.TextOptions{}) + mujica.Kbd(c, "Mod", "Shift", "P") + }) +} +``` + +## 参数 + +`func Kbd(c *ui.Context, keys ...string) *ui.Element` + +至少要传一个键,键名不能为空,否则 panic。键名不区分大小写: + +| 键名 | macOS | 其他平台 | +| --- | --- | --- | +| `Mod` | ⌘(Command) | Ctrl | +| `Ctrl` | ⌃ | Ctrl | +| `Alt` | ⌥(Option) | Alt | +| `Shift` | ⇧ | Shift | +| `Enter` | ↩ | Enter | +| `Esc` | esc | Esc | +| `Tab` | ⇥ | Tab | +| `Backspace` | ⌫ | Backspace | +| `Delete` | ⌦ | Delete | +| `Up` `Down` `Left` `Right` | ↑ ↓ ← → | ↑ ↓ ← → | + +其他键名按原样显示;单个字符转为大写。 + +## 状态 + +纯展示。键帽使用等宽字体、`Surface` 底色、`ControlBorder` 边框,底边 2 DIP。 + +## 事件 + +无。 + +## 键盘操作 + +不获取焦点,也不注册任何快捷键。辅助技术读出完整的组合键文字,例如“Command+Shift+P”或“Ctrl+Shift+P”。 + +## 限制 + +- 平台在编译时由 `runtime.GOOS` 决定。Gallery 的网页镜像显示的是运行 Go 进程那台机器的平台。 +- `Mod` 与 MyGo 的 `ui.Cmd` 一致:macOS 上是 Command,其他平台是 Control。 diff --git a/example_display_test.go b/example_display_test.go new file mode 100644 index 0000000..a3e05f4 --- /dev/null +++ b/example_display_test.go @@ -0,0 +1,134 @@ +package mujica_test + +import ( + "errors" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +func ExampleText() { + name := "Coronation.md" + saveErr := "" + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Text(c, "Order ID: 4821-AX", mujica.TextOptions{Selectable: true}) + mujica.Text(c, "A long note that stops after two lines.", mujica.TextOptions{MaxLines: 2}) + if mujica.EditableText(c, &name, mujica.EditableTextOptions{Error: saveErr}).Changed() { + saveErr = "" // save name here; set saveErr on failure + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleHeading() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Heading(c, "The Grand Hall", mujica.HeadingOptions{Subtitle: "Winter ceremonies", Divider: true}) + mujica.Heading(c, "Guest Ledger", mujica.HeadingOptions{Level: 2}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleIcon() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Row(c).Gap(8).Children(func() { + mujica.Icon(c, "star", mujica.IconOptions{}) + mujica.Icon(c, "circle-check", mujica.IconOptions{Size: 20, Tone: mujica.IconSuccess, Label: "Done"}) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleImage() { + var photo *ui.Bitmap // decode once with ui.DecodeBitmap, outside the view + loading, loadErr := false, errors.New("timeout") + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + img := mujica.Image(c, photo, mujica.ImageOptions{ + Width: 160, Height: 120, Crop: true, Alt: "Hall of mirrors", + Loading: loading, Err: loadErr, + }) + if img.Retry { + loading, loadErr = true, nil // start loading again here + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAvatar() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Row(c).Gap(8).Children(func() { + mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{Size: 40}) + mujica.Avatar(c, "", mujica.AvatarOptions{}) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAvatarGroup() { + people := []mujica.AvatarItem{ + {Name: "Sakiko Togawa"}, {Name: "Uika Misumi"}, {Name: "Mutsumi Wakaba"}, + {Name: "Umiri Yahata"}, {Name: "Nyamu Yutenji"}, + } + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.AvatarGroup(c, people, mujica.AvatarGroupOptions{Max: 3}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBadge() { + unread := 120 + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Badge(c, mujica.BadgeOptions{Text: "New", Tone: mujica.BadgeNeutral}, nil) + mujica.Badge(c, mujica.BadgeOptions{Count: unread, Label: "Unread", Position: mujica.BadgeTopRight}, func() { + mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{}) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTag() { + tags := []string{"Baroque", "Gothic"} + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Row(c).Gap(8).Children(func() { + for i, t := range tags { + v := mujica.Tag(c, t, mujica.TagOptions{Icon: "star", Clickable: true, Closable: true}) + v.Element.Key(t) + if v.Closed { + tags = append(tags[:i:i], tags[i+1:]...) + } + } + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleLink() { + details := false + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Link(c, "Read the charter", mujica.LinkOptions{URL: "https://example.com/charter"}) + if mujica.Link(c, "Show details", mujica.LinkOptions{}).Clicked() { + details = !details + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleKbd() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Row(c).Gap(6).Children(func() { + mujica.Text(c, "Command palette", mujica.TextOptions{}) + mujica.Kbd(c, "Mod", "Shift", "P") + }) + } + ui.Render(view, 400, 300, 1) +} diff --git a/examples/gallery/pages_display.go b/examples/gallery/pages_display.go index dde3607..cf3f0a8 100644 --- a/examples/gallery/pages_display.go +++ b/examples/gallery/pages_display.go @@ -1,3 +1,496 @@ package main -var displayPages = []page{} +import ( + "errors" + "fmt" + "strings" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +var displayPages = []page{ + {ID: "011", Name: "Text", ZhName: "文本", Category: "Display", Demo: textDemo, Code: textCode, + Keys: "Selectable 文本:点击后 Cmd/Ctrl+A 全选、Cmd/Ctrl+C 复制。可编辑:Tab 聚焦,Enter 或双击开始编辑,Enter 确认,Escape 取消,移走焦点也确认。"}, + {ID: "012", Name: "Heading", ZhName: "标题", Category: "Display", Demo: headingDemo, Code: headingCode, + Keys: "纯展示,不获取焦点。"}, + {ID: "013", Name: "Icon", ZhName: "图标", Category: "Display", Demo: iconDemo, Code: iconCode, + Keys: "纯展示,不获取焦点。"}, + {ID: "014", Name: "Image", ZhName: "图片", Category: "Display", Demo: imageDemo, Code: imageCode, + Keys: "错误状态的「重试」按钮可 Tab 聚焦,Enter 或 Space 触发。"}, + {ID: "015", Name: "Avatar", ZhName: "头像", Category: "Display", Demo: avatarDemo, Code: avatarCode, + Keys: "纯展示,不获取焦点。"}, + {ID: "016", Name: "AvatarGroup", ZhName: "头像组", Category: "Display", Demo: avatarGroupDemo, Code: avatarGroupCode, + Keys: "Tab 聚焦「+N」按钮,Enter 或 Space 展开完整名单,Escape 或点击外部关闭。"}, + {ID: "017", Name: "Badge", ZhName: "徽标", Category: "Display", Demo: badgeDemo, Code: badgeCode, + Keys: "纯展示,不获取焦点;辅助技术朗读完整数值。"}, + {ID: "018", Name: "Tag", ZhName: "标签", Category: "Display", Demo: tagDemo, Code: tagCode, + Keys: "可点击标签:Tab 聚焦,Enter 或 Space 点击,Delete 或 Backspace 关闭。关闭按钮是独立的 Tab 停靠点。"}, + {ID: "019", Name: "Link", ZhName: "链接", Category: "Display", Demo: linkDemo, Code: linkCode, + Keys: "Tab 聚焦链接,Enter 激活。"}, + {ID: "020", Name: "Kbd", ZhName: "快捷键", Category: "Display", Demo: kbdDemo, Code: kbdCode, + Keys: "纯展示,不注册快捷键,不获取焦点。"}, +} + +type textState struct { + maxLines float64 + selectable bool + name string + saving bool + failNext bool + saveErr string +} + +func textDemo(c *ui.Context, log func(string)) { + s := ui.Local(c.Root(), "011", func() textState { return textState{maxLines: 0, selectable: true, name: "Coronation.md"} }) + mujica.Heading(c, "Text", mujica.HeadingOptions{Subtitle: "换行、省略、选择复制与可编辑模式", Divider: true}) + displaySection(c, "Parameters", func() { + displayControls(c, func() { + ui.Text(c, fmt.Sprintf("MaxLines %d", int(s.maxLines))) + ui.StepSlider(c, &s.maxLines, 0, 4, 1).Width(160) + ui.Checkbox(c, &s.selectable, "Selectable") + ui.Checkbox(c, &s.saving, "Saving") + ui.Checkbox(c, &s.failNext, "Next save fails") + }) + }) + displaySection(c, "Display", func() { + ui.Box(c).Width(360).Children(func() { + mujica.Text(c, "Velvet curtains fall over the long gallery. Every candle leans toward the open door, and the quartet plays on while the guests take their places beneath the chandeliers.", + mujica.TextOptions{MaxLines: int(s.maxLines), Selectable: s.selectable}) + }) + }) + displaySection(c, "Editable", func() { + e := mujica.EditableText(c, &s.name, mujica.EditableTextOptions{Saving: s.saving, Error: s.saveErr, Label: "File name"}) + if e.Changed() { + if s.failNext { + s.saveErr = "Save failed: the name is already taken." + log("save failed: " + s.name) + } else { + s.saveErr = "" + log("saved: " + s.name) + } + } + }) + displayCode(c, textCode, log) +} + +const textCode = `mujica.Text(c, "Order ID: 4821-AX", mujica.TextOptions{Selectable: true}) +mujica.Text(c, "A long note that stops after two lines.", mujica.TextOptions{MaxLines: 2}) +if mujica.EditableText(c, &name, mujica.EditableTextOptions{Error: saveErr}).Changed() { + saveErr = "" // save name here; set saveErr on failure +}` + +type headingState struct { + level float64 + subtitle, divide bool + scale float64 +} + +func headingDemo(c *ui.Context, log func(string)) { + s := ui.Local(c.Root(), "012", func() headingState { return headingState{level: 1, subtitle: true, divide: true} }) + mujica.Heading(c, "Heading", mujica.HeadingOptions{Subtitle: "三级衬线标题、副标题与装饰分隔线", Divider: true}) + displaySection(c, "Parameters", func() { + displayControls(c, func() { + ui.Text(c, fmt.Sprintf("Level %d", int(s.level))) + if ui.StepSlider(c, &s.level, 1, 3, 1).Width(120).Changed() { + log(fmt.Sprintf("level %d", int(s.level))) + } + ui.Checkbox(c, &s.subtitle, "Subtitle") + ui.Checkbox(c, &s.divide, "Divider") + }) + }) + displaySection(c, "Preview", func() { + sub := "" + if s.subtitle { + sub = "Ceremonies of the winter season" + } + mujica.Heading(c, "The Grand Hall", mujica.HeadingOptions{Level: int(s.level), Subtitle: sub, Divider: s.divide}) + }) + displaySection(c, "Levels", func() { + for l := 1; l <= 3; l++ { + mujica.Heading(c, fmt.Sprintf("Level %d · 宫廷", l), mujica.HeadingOptions{Level: l}) + } + }) + displayCode(c, headingCode, log) +} + +const headingCode = `mujica.Heading(c, "The Grand Hall", mujica.HeadingOptions{Subtitle: "Winter ceremonies", Divider: true}) +mujica.Heading(c, "Guest Ledger", mujica.HeadingOptions{Level: 2})` + +type iconState struct { + name string + size string + tone string + label bool +} + +var iconTones = map[string]mujica.IconTone{"Inherit": mujica.IconInherit, "Muted": mujica.IconMuted, "Accent": mujica.IconAccent, + "Success": mujica.IconSuccess, "Warning": mujica.IconWarning, "Danger": mujica.IconDanger, "Info": mujica.IconInfo} + +func iconDemo(c *ui.Context, log func(string)) { + s := ui.Local(c.Root(), "013", func() iconState { return iconState{name: "star", size: "24", tone: "Accent"} }) + mujica.Heading(c, "Icon", mujica.HeadingOptions{Subtitle: "按名称取图标;尺寸与语义颜色;缺失时可见回退", Divider: true}) + displaySection(c, "Parameters", func() { + displayControls(c, func() { + if ui.Select(c, &s.name, []string{"star", "bell", "settings", "moon", "sun", "no-such-icon"}).Changed() { + log("name " + s.name) + } + if ui.Select(c, &s.size, []string{"16", "20", "24"}).Changed() { + log("size " + s.size) + } + if ui.Select(c, &s.tone, []string{"Inherit", "Muted", "Accent", "Success", "Warning", "Danger", "Info"}).Changed() { + log("tone " + s.tone) + } + ui.Checkbox(c, &s.label, "Label") + }) + }) + displaySection(c, "Preview", func() { + var size float32 + fmt.Sscan(s.size, &size) + label := "" + if s.label { + label = "Preview icon" + } + mujica.Icon(c, s.name, mujica.IconOptions{Size: size, Tone: iconTones[s.tone], Label: label}) + }) + displaySection(c, "Tones and fallback", func() { + ui.Row(c).Gap(14).Children(func() { + for _, t := range []string{"Inherit", "Muted", "Accent", "Success", "Warning", "Danger", "Info"} { + mujica.Icon(c, "circle-check", mujica.IconOptions{Size: 20, Tone: iconTones[t]}).Key(t) + } + mujica.Icon(c, "missing-name", mujica.IconOptions{Size: 20}) + }) + }) + displayCode(c, iconCode, log) +} + +const iconCode = `ui.Row(c).Gap(8).Children(func() { + mujica.Icon(c, "star", mujica.IconOptions{}) + mujica.Icon(c, "circle-check", mujica.IconOptions{Size: 20, Tone: mujica.IconSuccess, Label: "Done"}) +})` + +type imageState struct { + crop, loading, fail, empty bool + err error +} + +var displayPicture = func() *ui.SVG { + return ui.MustParseSVG([]byte(` + +`)) +}() + +func imageDemo(c *ui.Context, log func(string)) { + s := ui.Local(c.Root(), "014", func() imageState { return imageState{crop: true} }) + mujica.Heading(c, "Image", mujica.HeadingOptions{Subtitle: "位图与 SVG;包含或裁切;占位、加载、错误;替代说明", Divider: true}) + displaySection(c, "Parameters", func() { + displayControls(c, func() { + ui.Checkbox(c, &s.crop, "Crop") + ui.Checkbox(c, &s.empty, "No source") + ui.Checkbox(c, &s.loading, "Loading") + if ui.Checkbox(c, &s.fail, "Error").Changed() { + s.err = nil + if s.fail { + s.err = errors.New("connection reset") + } + } + }) + }) + displaySection(c, "Preview", func() { + var src ui.ImageSource = displayPicture + if s.empty { + src = nil + } + v := mujica.Image(c, src, mujica.ImageOptions{Width: 200, Height: 200, Crop: s.crop, Alt: "Gothic emblem", Loading: s.loading, Err: s.err}) + if v.Retry { + s.fail, s.err = false, nil + log("retry") + } + }) + displaySection(c, "States", func() { + ui.Row(c).Gap(12).Wrap().Children(func() { + mujica.Image(c, displayPicture, mujica.ImageOptions{Width: 120, Height: 120}) + mujica.Image(c, displayPicture, mujica.ImageOptions{Width: 120, Height: 120, Crop: true}) + mujica.Image(c, nil, mujica.ImageOptions{Width: 120, Height: 120}) + mujica.Image(c, nil, mujica.ImageOptions{Width: 120, Height: 120, Loading: true}) + if mujica.Image(c, nil, mujica.ImageOptions{Width: 160, Height: 140, Err: errors.New("404")}).Retry { + log("retry (static example)") + } + }) + }) + displayCode(c, imageCode, log) +} + +const imageCode = `img := mujica.Image(c, photo, mujica.ImageOptions{ + Width: 160, Height: 120, Crop: true, Alt: "Hall of mirrors", + Loading: loading, Err: loadErr, +}) +if img.Retry { + loading, loadErr = true, nil // start loading again here +}` + +type avatarState struct { + name string + size float64 + img bool +} + +var displayBitmap = func() *ui.Bitmap { + return ui.NewBitmap(ui.Render(func(c *ui.Context) { + mujica.Use(c, mujica.Settings{Mode: mujica.Dark}) + ui.Box(c).Fill().Background(ui.Hex("#7D2034")) + }, 64, 64, 1)) +}() + +func avatarDemo(c *ui.Context, log func(string)) { + s := ui.Local(c.Root(), "015", func() avatarState { return avatarState{name: "Sakiko Togawa", size: 48} }) + mujica.Heading(c, "Avatar", mujica.HeadingOptions{Subtitle: "图片、姓名首字母与默认图标回退", Divider: true}) + displaySection(c, "Parameters", func() { + displayControls(c, func() { + if ui.TextInput(c, &s.name).Width(200).Changed() { + log("name " + s.name) + } + ui.Text(c, fmt.Sprintf("Size %d", int(s.size))) + ui.StepSlider(c, &s.size, 24, 80, 4).Width(160) + ui.Checkbox(c, &s.img, "Image") + }) + }) + displaySection(c, "Preview", func() { + var img *ui.Bitmap + if s.img { + img = displayBitmap + } + mujica.Avatar(c, s.name, mujica.AvatarOptions{Size: float32(s.size), Image: img}) + }) + displaySection(c, "Fallbacks", func() { + ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { + mujica.Avatar(c, "Photo", mujica.AvatarOptions{Image: displayBitmap, Size: 40}) + mujica.Avatar(c, "Uika Misumi", mujica.AvatarOptions{Size: 40}) + mujica.Avatar(c, "若叶睦", mujica.AvatarOptions{Size: 40}) + mujica.Avatar(c, "", mujica.AvatarOptions{Size: 40}) + }) + }) + displayCode(c, avatarCode, log) +} + +const avatarCode = `ui.Row(c).Gap(8).Children(func() { + mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{Size: 40}) + mujica.Avatar(c, "", mujica.AvatarOptions{}) +})` + +var displayPeople = []mujica.AvatarItem{ + {Name: "Sakiko Togawa"}, {Name: "Uika Misumi"}, {Name: "Mutsumi Wakaba"}, {Name: "Umiri Yahata"}, + {Name: "Nyamu Yutenji"}, {Name: "Tomori Takamatsu"}, {Name: "Anon Chihaya"}, {Name: "Rana Kaname"}, +} + +type avatarGroupState struct{ max, count float64 } + +func avatarGroupDemo(c *ui.Context, log func(string)) { + s := ui.Local(c.Root(), "016", func() avatarGroupState { return avatarGroupState{max: 3, count: 6} }) + mujica.Heading(c, "AvatarGroup", mujica.HeadingOptions{Subtitle: "最大显示数量、剩余数量与完整名单", Divider: true}) + displaySection(c, "Parameters", func() { + displayControls(c, func() { + ui.Text(c, fmt.Sprintf("Max %d", int(s.max))) + if ui.StepSlider(c, &s.max, 1, 8, 1).Width(160).Changed() { + log(fmt.Sprintf("max %d", int(s.max))) + } + ui.Text(c, fmt.Sprintf("People %d", int(s.count))) + if ui.StepSlider(c, &s.count, 1, 8, 1).Width(160).Changed() { + log(fmt.Sprintf("people %d", int(s.count))) + } + }) + }) + displaySection(c, "Preview", func() { + for i, g := range []mujica.AvatarGroupView{ + mujica.AvatarGroup(c, displayPeople[:int(s.count)], mujica.AvatarGroupOptions{Max: int(s.max)}), + mujica.AvatarGroup(c, displayPeople, mujica.AvatarGroupOptions{Max: 4, Size: 24}), + } { + if g.Toggled { + log(fmt.Sprintf("group %d open=%v", i+1, g.Open)) + } + } + }) + displayCode(c, avatarGroupCode, log) +} + +const avatarGroupCode = `mujica.AvatarGroup(c, people, mujica.AvatarGroupOptions{Max: 3})` + +type badgeState struct { + count, max float64 + text string + tone, pos string +} + +var badgeTones = map[string]mujica.BadgeTone{"Accent": mujica.BadgeAccent, "Neutral": mujica.BadgeNeutral, "Success": mujica.BadgeSuccess, + "Warning": mujica.BadgeWarning, "Danger": mujica.BadgeDanger, "Info": mujica.BadgeInfo} +var badgePositions = map[string]mujica.BadgePosition{"TopRight": mujica.BadgeTopRight, "TopLeft": mujica.BadgeTopLeft, + "BottomRight": mujica.BadgeBottomRight, "BottomLeft": mujica.BadgeBottomLeft} + +func badgeDemo(c *ui.Context, log func(string)) { + s := ui.Local(c.Root(), "017", func() badgeState { return badgeState{count: 120, max: 99, tone: "Accent", pos: "TopRight"} }) + mujica.Heading(c, "Badge", mujica.HeadingOptions{Subtitle: "文字或计数、上限、位置与完整朗读", Divider: true}) + displaySection(c, "Parameters", func() { + displayControls(c, func() { + ui.Text(c, fmt.Sprintf("Count %d", int(s.count))) + ui.StepSlider(c, &s.count, 0, 200, 1).Width(160) + ui.Text(c, fmt.Sprintf("Max %d", int(s.max))) + ui.StepSlider(c, &s.max, 9, 999, 10).Width(120) + ui.TextInput(c, &s.text).Placeholder("Text overrides count").Width(180) + if ui.Select(c, &s.tone, []string{"Accent", "Neutral", "Success", "Warning", "Danger", "Info"}).Changed() { + log("tone " + s.tone) + } + if ui.Select(c, &s.pos, []string{"TopRight", "TopLeft", "BottomRight", "BottomLeft"}).Changed() { + log("position " + s.pos) + } + }) + }) + opts := mujica.BadgeOptions{Text: s.text, Count: int(s.count), Max: int(s.max), Tone: badgeTones[s.tone], Label: "Unread"} + displaySection(c, "Inline", func() { + ui.Row(c).Gap(8).AlignItems(ui.Center).Children(func() { + ui.Text(c, "Inbox") + mujica.Badge(c, opts, nil) + }) + }) + displaySection(c, "Corner", func() { + opts.Position = badgePositions[s.pos] + ui.Box(c).Padding(10).Children(func() { + mujica.Badge(c, opts, func() { mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{Size: 40}) }) + }) + }) + displayCode(c, badgeCode, log) +} + +const badgeCode = `mujica.Badge(c, mujica.BadgeOptions{Text: "New", Tone: mujica.BadgeNeutral}, nil) +mujica.Badge(c, mujica.BadgeOptions{Count: unread, Label: "Unread", Position: mujica.BadgeTopRight}, func() { + mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{}) +})` + +type tagState struct { + tags []string + icon, clickable, closable, dis bool +} + +func tagDemo(c *ui.Context, log func(string)) { + s := ui.Local(c.Root(), "018", func() tagState { + return tagState{tags: []string{"Baroque", "Gothic", "Rococo", "Court"}, icon: true, clickable: true, closable: true} + }) + mujica.Heading(c, "Tag", mujica.HeadingOptions{Subtitle: "标签文字、图标;点击与关闭分离", Divider: true}) + displaySection(c, "Parameters", func() { + displayControls(c, func() { + ui.Checkbox(c, &s.icon, "Icon") + ui.Checkbox(c, &s.clickable, "Clickable") + ui.Checkbox(c, &s.closable, "Closable") + ui.Checkbox(c, &s.dis, "Disabled") + if mujica.Button(c, "Reset tags", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + s.tags = []string{"Baroque", "Gothic", "Rococo", "Court"} + log("reset") + } + }) + }) + displaySection(c, "Preview", func() { + icon := "" + if s.icon { + icon = "star" + } + ui.Row(c).Gap(8).Wrap().Children(func() { + for i := 0; i < len(s.tags); i++ { + t := s.tags[i] + v := mujica.Tag(c, t, mujica.TagOptions{Icon: icon, Clickable: s.clickable, Closable: s.closable, Disabled: s.dis}) + v.Element.Key(t) + if v.Clicked { + log("clicked " + t) + } + if v.Closed { + log("closed " + t) + s.tags = append(s.tags[:i:i], s.tags[i+1:]...) + i-- + } + } + }) + }) + displayCode(c, tagCode, log) +} + +const tagCode = `for i, t := range tags { + v := mujica.Tag(c, t, mujica.TagOptions{Icon: "star", Clickable: true, Closable: true}) + v.Element.Key(t) + if v.Closed { + tags = append(tags[:i:i], tags[i+1:]...) + } +}` + +type linkState struct { + url string + external string +} + +func linkDemo(c *ui.Context, log func(string)) { + s := ui.Local(c.Root(), "019", func() linkState { return linkState{url: "https://example.com/charter"} }) + mujica.Heading(c, "Link", mujica.HeadingOptions{Subtitle: "外部地址或应用内动作;键盘激活;打开失败反馈", Divider: true}) + displaySection(c, "Parameters", func() { + displayControls(c, func() { + ui.TextInput(c, &s.url).Placeholder("URL (empty = in-app action)").Width(320) + if mujica.Button(c, "Simulate open failure", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + s.external = "No application can open this link." + log("open failure set") + } + if mujica.Button(c, "Clear error", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + s.external = "" + } + }) + }) + displaySection(c, "Preview", func() { + if mujica.Link(c, "Read the court charter", mujica.LinkOptions{URL: s.url, Error: s.external}).Clicked() { + log("activated " + s.url) + } + if mujica.Link(c, "Show the guest list", mujica.LinkOptions{}).Clicked() { + log("in-app action") + } + }) + displayCode(c, linkCode, log) +} + +const linkCode = `mujica.Link(c, "Read the charter", mujica.LinkOptions{URL: "https://example.com/charter"}) +if mujica.Link(c, "Show details", mujica.LinkOptions{}).Clicked() { + details = !details +}` + +type kbdState struct{ combo string } + +func kbdDemo(c *ui.Context, log func(string)) { + s := ui.Local(c.Root(), "020", func() kbdState { return kbdState{combo: "Mod+Shift+P"} }) + mujica.Heading(c, "Kbd", mujica.HeadingOptions{Subtitle: "单键与组合键;按平台显示 Command 或 Ctrl", Divider: true}) + displaySection(c, "Parameters", func() { + displayControls(c, func() { + if ui.TextInput(c, &s.combo).Placeholder("Keys joined by +").Width(220).Changed() { + log("keys " + s.combo) + } + }) + }) + displaySection(c, "Preview", func() { + var keys []string + for _, k := range strings.Split(s.combo, "+") { + if k = strings.TrimSpace(k); k != "" { + keys = append(keys, k) + } + } + if len(keys) > 0 { + mujica.Kbd(c, keys...) + } + }) + displaySection(c, "Common", func() { + for _, row := range [][]string{{"Mod", "K"}, {"Mod", "Shift", "P"}, {"Alt", "Up"}, {"Esc"}, {"Ctrl", "Tab"}} { + ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { + mujica.Kbd(c, row...) + ui.Text(c, strings.Join(row, " + ")).TextColor(mujica.Tokens(c).TextMuted) + }) + } + }) + displayCode(c, kbdCode, log) +} + +const kbdCode = `ui.Row(c).Gap(6).Children(func() { + mujica.Text(c, "Command palette", mujica.TextOptions{}) + mujica.Kbd(c, "Mod", "Shift", "P") +})` diff --git a/examples/gallery/pages_display_parts.go b/examples/gallery/pages_display_parts.go new file mode 100644 index 0000000..57d8d45 --- /dev/null +++ b/examples/gallery/pages_display_parts.go @@ -0,0 +1,32 @@ +package main + +import ( + "github.com/egoist/mygo/ui" + + mujica "mujicaui" + "mujicaui/theme" +) + +// displaySection shows a titled block of a demo. +func displaySection(c *ui.Context, title string, fn func()) { + ui.Column(c).Gap(10).Margin(0, 0, 20, 0).Children(func() { + mujica.Heading(c, title, mujica.HeadingOptions{Level: 3}) + fn() + }) +} + +// displayControls lays out a row of parameter controls. +func displayControls(c *ui.Context, fn func()) { + ui.Row(c).Gap(16).Wrap().AlignItems(ui.Center).Children(fn) +} + +// displayCode shows a page's snippet with a copy button. +func displayCode(c *ui.Context, code string, log func(string)) { + displaySection(c, "Code", func() { + if mujica.Button(c, "Copy code", mujica.ButtonOptions{Variant: mujica.Secondary}).AlignSelf(ui.Start).Clicked() { + c.WriteClipboard(code) + log("code copied") + } + mujica.Text(c, code, mujica.TextOptions{Selectable: true}).Font(theme.MonoFont).FontSize(12) + }) +} diff --git a/heading.go b/heading.go new file mode 100644 index 0000000..7734afb --- /dev/null +++ b/heading.go @@ -0,0 +1,64 @@ +package mujica + +import ( + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// HeadingOptions configure a Heading. The zero value is a level 1 heading. +type HeadingOptions struct { + // Level is 1, 2 or 3; zero means 1. + Level int + // Subtitle shows below the title in muted text. + Subtitle string + // Divider draws an ornament rule with a center diamond below. + Divider bool +} + +// Heading shows a serif title that follows the desktop's text scale. +func Heading(c *ui.Context, title string, opts HeadingOptions) *ui.Element { + k := Tokens(c) + size := headingSize(opts.Level) + scale := c.Preferences().TextScale + if scale <= 0 { + scale = 1 + } + u := Density(c).Unit() + col := ui.Column(c).Gap(u).Shrink(0) + col.Children(func() { + ui.Text(c, title).Font(theme.SerifFont).FontSize(size * scale).FontWeight(600). + LineHeight(1.25).TextColor(k.Text) + if opts.Subtitle != "" { + ui.Text(c, opts.Subtitle).FontSize(theme.BodySize * scale).LineHeight(theme.LineHeight).TextColor(k.TextMuted) + } + if opts.Divider { + ui.Box(c).FillWidth().Height(9).Margin(u, 0, 0, 0).Draw(func(p *ui.Painter, r ui.Rect) { + headingOrnament(p, r, k.Ornament) + }) + } + }) + return col +} + +func headingSize(level int) float32 { + switch level { + case 0, 1: + return theme.H1Size + case 2: + return theme.H2Size + case 3: + return theme.H3Size + } + panic("mujica: Heading Level must be 1, 2 or 3") +} + +// headingOrnament draws symmetric rules around a center diamond. +func headingOrnament(p *ui.Painter, r ui.Rect, c ui.Color) { + cx, cy := r.X+r.W/2, r.Y+r.H/2 + const d, gap = 7, 6 + internal.Rule(p, r.X, cx-gap, cy, c) + internal.Rule(p, cx+gap, r.X+r.W, cy, c) + p.FillPath(internal.Diamond(cx, cy, d), c) +} diff --git a/heading_test.go b/heading_test.go new file mode 100644 index 0000000..0121972 --- /dev/null +++ b/heading_test.go @@ -0,0 +1,78 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func headingHeights(t *testing.T, p ui.Preferences) (h1, h2, h3 float32) { + t.Helper() + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).AlignItems(ui.Start).Children(func() { + h1 = Heading(c, "Court", HeadingOptions{}).Bounds().H + h2 = Heading(c, "Court", HeadingOptions{Level: 2}).Bounds().H + h3 = Heading(c, "Court", HeadingOptions{Level: 3}).Bounds().H + }) + }, 400, 400) + tt.SetPreferences(p) + tt.Frame() + return +} + +func TestHeadingLevels(t *testing.T) { + h1, h2, h3 := headingHeights(t, ui.Preferences{TextScale: 1}) + if !(h1 > h2 && h2 > h3 && h3 > 0) { + t.Fatalf("heights %v %v %v not decreasing", h1, h2, h3) + } +} + +func TestHeadingTextScale(t *testing.T) { + h1, _, _ := headingHeights(t, ui.Preferences{TextScale: 1}) + big, _, _ := headingHeights(t, ui.Preferences{TextScale: 2}) + if big < h1*1.8 { + t.Fatalf("scaled height %v, base %v", big, h1) + } +} + +func TestHeadingSubtitleAndDivider(t *testing.T) { + var plain, full float32 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).AlignItems(ui.Start).Children(func() { + plain = Heading(c, "Archive", HeadingOptions{Level: 2}).Bounds().H + full = Heading(c, "Ledger", HeadingOptions{Level: 2, Subtitle: "Entries since 1802", Divider: true}).Bounds().H + }) + }, 400, 300) + tt.Frame() + if !tt.HasText("Entries since 1802") { + t.Fatal("subtitle missing") + } + if full < plain+20 { + t.Fatalf("subtitle and divider add %v DIPs", full-plain) + } +} + +func TestHeadingBadLevelPanics(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic") + } + }() + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Heading(c, "x", HeadingOptions{Level: 4}) + }, 100, 100) + tt.Frame() +} + +func TestHeadingSnapshot(t *testing.T) { + snapshot(t, "heading", 420, 260, func(c *ui.Context) { + ui.Column(c).Padding(20).Gap(16).Width(380).Children(func() { + Heading(c, "The Grand Hall", HeadingOptions{Subtitle: "Ceremonies of the winter season", Divider: true}) + Heading(c, "Guest Ledger", HeadingOptions{Level: 2}) + Heading(c, "宫廷礼仪", HeadingOptions{Level: 3, Subtitle: "第三章"}) + }) + }) +} diff --git a/icon.go b/icon.go new file mode 100644 index 0000000..ec0cc19 --- /dev/null +++ b/icon.go @@ -0,0 +1,98 @@ +package mujica + +import ( + "fmt" + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/theme" +) + +// IconTone is an icon's semantic color. The zero value takes the text color. +type IconTone int + +const ( + IconInherit IconTone = iota + IconMuted + IconAccent + IconSuccess + IconWarning + IconDanger + IconInfo +) + +// IconOptions configure an Icon. +type IconOptions struct { + // Size is the icon's width and height in DIPs; zero means 16. + Size float32 + Tone IconTone + // Label names a meaningful icon for assistive technology; empty hides it. + Label string +} + +type iconMissingKey struct{} + +// Icon shows the named icon of the icons package. A missing name shows a +// dashed fallback box in the Danger color and logs a warning. +func Icon(c *ui.Context, name string, opts IconOptions) *ui.Element { + if name == "" { + panic("mujica: Icon needs a name") + } + size := opts.Size + if size < 0 { + panic("mujica: Icon Size must not be negative") + } + if size == 0 { + size = 16 + } + k := Tokens(c) + svg, ok := icons.Get(name) + if !ok { + return iconMissing(c, name, size, k) + } + e := ui.Icon(c, svg).Size(size, size).Shrink(0) + if col, set := iconColor(k, opts.Tone); set { + e.TextColor(col) + } + if opts.Label != "" { + e.Label(opts.Label) + } + return e +} + +func iconMissing(c *ui.Context, name string, size float32, k theme.Tokens) *ui.Element { + text := fmt.Sprintf(msg(c, "icon.missing"), name) + e := ui.Box(c).Size(size, size).Shrink(0).Role(ui.RoleImage).Label(text).Tooltip(text) + logged := ui.Local(e, iconMissingKey{}, func() string { return "" }) + if *logged != name { + *logged = name + slog.Warn("mujica: icon missing", "name", name) + } + return e.Draw(func(p *ui.Painter, r ui.Rect) { + in := ui.Rect{X: r.X + 1, Y: r.Y + 1, W: r.W - 2, H: r.H - 2} + p.StrokeDashed(in, k.Danger, theme.ControlRadius, 1) + p.Line(in.X+3, in.Y+3, in.X+in.W-3, in.Y+in.H-3, 1, k.Danger) + }) +} + +func iconColor(k theme.Tokens, t IconTone) (ui.Color, bool) { + switch t { + case IconInherit: + return ui.Color{}, false + case IconMuted: + return k.TextMuted, true + case IconAccent: + return k.AccentText, true + case IconSuccess: + return k.Success, true + case IconWarning: + return k.Warning, true + case IconDanger: + return k.Danger, true + case IconInfo: + return k.Info, true + } + panic(fmt.Sprintf("mujica: unknown IconTone %d", t)) +} diff --git a/icon_test.go b/icon_test.go new file mode 100644 index 0000000..32ada91 --- /dev/null +++ b/icon_test.go @@ -0,0 +1,88 @@ +package mujica + +import ( + "bytes" + "log/slog" + "strings" + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestIconSizeAndLabel(t *testing.T) { + var def, big ui.Rect + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Row(c).AlignItems(ui.Start).Children(func() { + def = Icon(c, "star", IconOptions{}).Bounds() + big = Icon(c, "bell", IconOptions{Size: 24, Tone: IconAccent, Label: "Alerts"}).Bounds() + }) + }, 200, 100) + tt.Frame() + if def.W != 16 || def.H != 16 || big.W != 24 || big.H != 24 { + t.Fatalf("sizes %v %v", def, big) + } + if _, ok := tt.Find("Alerts"); !ok { + t.Fatal("label not exposed") + } +} + +func TestIconMissingFallbackIsVisibleAndLogged(t *testing.T) { + var buf bytes.Buffer + old := slog.Default() + slog.SetDefault(slog.New(slog.NewTextHandler(&buf, nil))) + defer slog.SetDefault(old) + var r ui.Rect + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Row(c).AlignItems(ui.Start).Children(func() { + r = Icon(c, "no-such-icon", IconOptions{Size: 20}).Bounds() + }) + }, 200, 100) + tt.Frame() + tt.Frame() + if r.W != 20 || r.H != 20 { + t.Fatalf("fallback size %v", r) + } + if _, ok := tt.Find("Missing icon: no-such-icon"); !ok { + t.Fatal("fallback not labeled") + } + if n := strings.Count(buf.String(), "mujica: icon missing"); n != 1 { + t.Fatalf("logged %d times: %s", n, buf.String()) + } + if !strings.Contains(buf.String(), "name=no-such-icon") { + t.Fatalf("log lacks name: %s", buf.String()) + } +} + +func TestIconBadOptionsPanic(t *testing.T) { + for name, opts := range map[string]IconOptions{"": {}, "star": {Size: -1}, "x": {Tone: IconTone(99)}} { + func() { + defer func() { + if recover() == nil { + t.Fatalf("%q %+v: no panic", name, opts) + } + }() + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Icon(c, name, opts) + }, 100, 100) + tt.Frame() + }() + } +} + +func TestIconSnapshot(t *testing.T) { + snapshot(t, "icon", 360, 80, func(c *ui.Context) { + ui.Row(c).Padding(24).Gap(14).AlignItems(ui.Center).Children(func() { + Icon(c, "star", IconOptions{}) + Icon(c, "star", IconOptions{Size: 20, Tone: IconMuted}) + Icon(c, "star", IconOptions{Size: 24, Tone: IconAccent}) + Icon(c, "circle-check", IconOptions{Size: 20, Tone: IconSuccess}) + Icon(c, "triangle-alert", IconOptions{Size: 20, Tone: IconWarning}) + Icon(c, "circle-x", IconOptions{Size: 20, Tone: IconDanger}) + Icon(c, "info", IconOptions{Size: 20, Tone: IconInfo}) + Icon(c, "no-such-icon", IconOptions{Size: 20}) + }) + }) +} diff --git a/image.go b/image.go new file mode 100644 index 0000000..52edd9f --- /dev/null +++ b/image.go @@ -0,0 +1,86 @@ +package mujica + +import ( + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +// ImageOptions configure an Image. +type ImageOptions struct { + // Width and Height size the box in DIPs; zero keeps the picture's size, + // or 160×120 for the placeholder, loading and error states. + Width, Height float32 + // Crop covers the box and crops the rest; false fits the picture inside. + Crop bool + // Alt describes the picture for assistive technology. + Alt string + // Loading shows the caller's load in progress. + Loading bool + // Err shows the caller's load failure with a retry button. + Err error +} + +// ImageView is a shown Image and what the user did to it this frame. +type ImageView struct { + Element *ui.Element + // Retry reports a click on the retry button of the error state. + Retry bool +} + +// Image shows a *ui.Bitmap or *ui.SVG. A nil src shows a placeholder. +func Image(c *ui.Context, src ui.ImageSource, opts ImageOptions) ImageView { + if opts.Width < 0 || opts.Height < 0 { + panic("mujica: Image Width and Height must not be negative") + } + if opts.Err == nil && !opts.Loading && src != nil { + e := ui.Image(c, src).Radius(theme.ControlRadius) + if opts.Crop { + e.Fit(ui.Cover) + } + if opts.Width > 0 { + e.Width(opts.Width) + } + if opts.Height > 0 { + e.Height(opts.Height) + } + if opts.Alt != "" { + e.Label(opts.Alt) + } + return ImageView{Element: e} + } + k := Tokens(c) + u := Density(c).Unit() + w, h := opts.Width, opts.Height + if w == 0 { + w = 160 + } + if h == 0 { + h = 120 + } + var v ImageView + frame := ui.Column(c).Size(w, h).Shrink(0).Center().Gap(u*2).Padding(u*2).Clip(). + Radius(theme.ControlRadius).Background(k.SurfaceHover).Border(theme.BorderWidth, k.Border) + v.Element = frame + frame.Children(func() { + switch { + case opts.Err != nil: + frame.Role(ui.RoleGroup).Label(msg(c, "image.error")).Description(opts.Alt).Error(opts.Err.Error()) + Icon(c, "circle-alert", IconOptions{Size: 20, Tone: IconDanger}) + ui.Text(c, msg(c, "image.error")).FontSize(theme.CaptionSize).TextColor(k.Danger).TextAlign(ui.Center) + if Button(c, msg(c, "image.retry"), ButtonOptions{Variant: Secondary}).Clicked() { + v.Retry = true + slog.Debug("mujica: image retry", "alt", opts.Alt) + } + case opts.Loading: + frame.Role(ui.RoleImage).Label(msg(c, "image.loading")).Description(opts.Alt) + Spinner(c, SpinnerOptions{Size: 20, Label: msg(c, "image.loading")}) + default: + frame.Role(ui.RoleImage).Label(msg(c, "image.placeholder")).Description(opts.Alt) + Icon(c, "image", IconOptions{Size: 24, Tone: IconMuted}) + } + }) + return v +} diff --git a/image_test.go b/image_test.go new file mode 100644 index 0000000..395fa69 --- /dev/null +++ b/image_test.go @@ -0,0 +1,113 @@ +package mujica + +import ( + "errors" + "image" + "image/color" + "testing" + + "github.com/egoist/mygo/ui" +) + +func imageTestBitmap() *ui.Bitmap { + img := image.NewRGBA(image.Rect(0, 0, 80, 40)) + for y := range 40 { + for x := range 80 { + img.Set(x, y, color.RGBA{uint8(100 + x), 32, 52, 255}) + } + } + return ui.NewBitmap(img) +} + +func TestImageLoadedSizes(t *testing.T) { + bmp := imageTestBitmap() + var natural, sized ui.Rect + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).AlignItems(ui.Start).Children(func() { + natural = Image(c, bmp, ImageOptions{Alt: "Gallery"}).Element.Bounds() + sized = Image(c, bmp, ImageOptions{Width: 50, Height: 50, Crop: true}).Element.Bounds() + }) + }, 300, 300) + tt.Frame() + if natural.W != 80 || natural.H != 40 { + t.Fatalf("natural %v", natural) + } + if sized.W != 50 || sized.H != 50 { + t.Fatalf("sized %v", sized) + } + if _, ok := tt.Find("Gallery"); !ok { + t.Fatal("alt text not exposed") + } +} + +func TestImageStates(t *testing.T) { + for _, tc := range []struct { + name string + src ui.ImageSource + opts ImageOptions + label string + }{ + {"placeholder", nil, ImageOptions{}, "No image"}, + {"loading", imageTestBitmap(), ImageOptions{Loading: true}, "Loading image"}, + {"error", imageTestBitmap(), ImageOptions{Loading: true, Err: errors.New("404")}, "Image failed to load"}, + } { + var r ui.Rect + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).AlignItems(ui.Start).Children(func() { + r = Image(c, tc.src, tc.opts).Element.Bounds() + }) + }, 300, 300) + tt.Frame() + if r.W != 160 || r.H != 120 { + t.Errorf("%s: size %v", tc.name, r) + } + if _, ok := tt.Find(tc.label); !ok { + t.Errorf("%s: no %q", tc.name, tc.label) + } + } +} + +func TestImageRetry(t *testing.T) { + retries := 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if Image(c, nil, ImageOptions{Width: 200, Height: 140, Err: errors.New("timeout")}).Retry { + retries++ + } + }, 300, 300) + if err := tt.Click("Retry"); err != nil { + t.Fatal(err) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + if retries != 2 { + t.Fatalf("retries %d, want 2", retries) + } +} + +func TestImageNegativeSizePanics(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic") + } + }() + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Image(c, nil, ImageOptions{Width: -1}) + }, 100, 100) + tt.Frame() +} + +func TestImageSnapshot(t *testing.T) { + bmp := imageTestBitmap() + snapshot(t, "image", 600, 180, func(c *ui.Context) { + ui.Row(c).Padding(20).Gap(12).AlignItems(ui.Start).Children(func() { + Image(c, bmp, ImageOptions{Width: 120, Height: 120, Crop: true, Alt: "Cropped"}) + Image(c, nil, ImageOptions{Width: 120, Height: 120}) + Image(c, nil, ImageOptions{Width: 120, Height: 120, Loading: true}) + Image(c, nil, ImageOptions{Width: 160, Height: 140, Err: errors.New("404")}) + }) + }) +} diff --git a/internal/display.go b/internal/display.go new file mode 100644 index 0000000..f84e2fc --- /dev/null +++ b/internal/display.go @@ -0,0 +1,27 @@ +package internal + +import ( + "math" + "time" + + "github.com/egoist/mygo/ui" +) + +// Fade animates e's background and border colors over d; zero skips it. +func Fade(e *ui.Element, d time.Duration) *ui.Element { + if d > 0 { + e.Transition(ui.ElementTransition{Colors: true, Duration: d}) + } + return e +} + +// Rule draws a 1 DIP horizontal line from x0 to x1 at y, on device pixels. +func Rule(p *ui.Painter, x0, x1, y float32, c ui.Color) { + s := float64(p.Scale()) + px := math.Floor(float64(y) * s) + if int(math.Round(s))%2 == 1 { + px += 0.5 + } + yy := float32(px / s) + p.Line(x0, yy, x1, yy, 1, c) +} diff --git a/kbd.go b/kbd.go new file mode 100644 index 0000000..5e2575d --- /dev/null +++ b/kbd.go @@ -0,0 +1,80 @@ +package mujica + +import ( + "runtime" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +// Kbd shows a key or key combination as key caps. It registers nothing. +// "Mod" is Command on macOS and Ctrl elsewhere; Ctrl, Alt, Shift, Enter, +// Esc, Tab, Backspace, Delete, Up, Down, Left and Right follow the platform. +func Kbd(c *ui.Context, keys ...string) *ui.Element { + if len(keys) == 0 { + panic("mujica: Kbd needs at least one key") + } + k := Tokens(c) + mac := runtime.GOOS == "darwin" + spoken := make([]string, len(keys)) + for i, key := range keys { + if key == "" { + panic("mujica: Kbd key must not be empty") + } + _, spoken[i] = kbdKey(key, mac) + } + row := ui.Row(c).Gap(2).AlignItems(ui.Center).Shrink(0).Role(ui.RoleText).Label(strings.Join(spoken, "+")) + row.Children(func() { + for i, key := range keys { + if i > 0 && !mac { + ui.Text(c, "+").FontSize(theme.CaptionSize).TextColor(k.TextMuted) + } + face, _ := kbdKey(key, mac) + ui.Box(c).Key(i).Row().Center().MinWidth(20).Height(20).Padding(0, 5). + Radius(theme.ControlRadius).Background(k.Surface).BorderColor(k.ControlBorder). + BorderWidth(1, 1, 2, 1).Children(func() { + ui.Text(c, face).Font(theme.MonoFont).FontSize(theme.CaptionSize).TextColor(k.Text).SingleLine() + }) + } + }) + return row +} + +// kbdNames maps a key to its macOS cap, macOS word and other platforms' cap. +var kbdNames = map[string]struct{ macCap, macWord, other string }{ + "mod": {"⌘", "Command", "Ctrl"}, + "ctrl": {"⌃", "Control", "Ctrl"}, + "alt": {"⌥", "Option", "Alt"}, + "shift": {"⇧", "Shift", "Shift"}, + "enter": {"↩", "Return", "Enter"}, + "esc": {"esc", "Escape", "Esc"}, + "tab": {"⇥", "Tab", "Tab"}, + "backspace": {"⌫", "Delete", "Backspace"}, + "delete": {"⌦", "Forward Delete", "Delete"}, + "up": {"↑", "Up", "↑"}, + "down": {"↓", "Down", "↓"}, + "left": {"←", "Left", "←"}, + "right": {"→", "Right", "→"}, +} + +// kbdKey returns how key shows and how it is read on the platform. +func kbdKey(key string, mac bool) (shown, spoken string) { + n, ok := kbdNames[strings.ToLower(key)] + if !ok { + if len([]rune(key)) == 1 { + key = strings.ToUpper(key) + } + return key, key + } + if mac { + return n.macCap, n.macWord + } + word := n.other + switch strings.ToLower(key) { + case "up", "down", "left", "right": + word = n.macWord + } + return n.other, word +} diff --git a/kbd_test.go b/kbd_test.go new file mode 100644 index 0000000..35ba0e4 --- /dev/null +++ b/kbd_test.go @@ -0,0 +1,80 @@ +package mujica + +import ( + "runtime" + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestKbdKeyPlatforms(t *testing.T) { + for _, tc := range []struct { + key string + mac bool + shown, spoken string + }{ + {"Mod", true, "⌘", "Command"}, + {"Mod", false, "Ctrl", "Ctrl"}, + {"shift", true, "⇧", "Shift"}, + {"Alt", false, "Alt", "Alt"}, + {"Alt", true, "⌥", "Option"}, + {"Up", false, "↑", "Up"}, + {"k", false, "K", "K"}, + {"F5", true, "F5", "F5"}, + } { + shown, spoken := kbdKey(tc.key, tc.mac) + if shown != tc.shown || spoken != tc.spoken { + t.Errorf("%q mac=%v: %q %q", tc.key, tc.mac, shown, spoken) + } + } +} + +func TestKbdShowsPlatformCombo(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Kbd(c, "Mod", "Shift", "p") + }, 300, 100) + tt.Frame() + spoken, mod := "Ctrl+Shift+P", "Ctrl" + if runtime.GOOS == "darwin" { + spoken, mod = "Command+Shift+P", "⌘" + } + if _, ok := tt.Find(spoken); !ok { + t.Fatalf("not read as %q", spoken) + } + if !tt.HasText(mod) || !tt.HasText("P") { + t.Fatalf("texts %v", tt.Texts()) + } + tt.Key(0, ui.KeyTab) + if tt.Focused(spoken) { + t.Fatal("Kbd took focus") + } +} + +func TestKbdEmptyPanics(t *testing.T) { + for _, keys := range [][]string{nil, {"Mod", ""}} { + func() { + defer func() { + if recover() == nil { + t.Fatalf("%q: no panic", keys) + } + }() + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Kbd(c, keys...) + }, 100, 100) + tt.Frame() + }() + } +} + +func TestKbdSnapshot(t *testing.T) { + snapshot(t, "kbd", 360, 80, func(c *ui.Context) { + ui.Row(c).Padding(24).Gap(16).AlignItems(ui.Center).Children(func() { + Kbd(c, "Mod", "K") + Kbd(c, "Mod", "Shift", "P") + Kbd(c, "Esc") + Kbd(c, "Alt", "Up") + }) + }) +} diff --git a/link.go b/link.go new file mode 100644 index 0000000..6b47b66 --- /dev/null +++ b/link.go @@ -0,0 +1,81 @@ +package mujica + +import ( + "fmt" + "log/slog" + "net/url" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// LinkOptions configure a Link. An empty URL makes an in-app action link. +type LinkOptions struct { + // URL opens in the browser, or goes there in a MyGo router for a path. + URL string + // Error is the caller's open failure, shown below the link. + Error string +} + +// Link is underlined text activated by a click or Enter. Clicked reports +// every activation. A malformed URL is not opened and shows an error. +func Link(c *ui.Context, label string, opts LinkOptions) *ui.Element { + if label == "" { + panic("mujica: Link needs a label") + } + k := Tokens(c) + u := Density(c).Unit() + external := false + problem := opts.Error + target := opts.URL + if opts.URL != "" { + ok := false + external, ok = linkCheck(opts.URL) + if !ok { + external, target = false, "" + if problem == "" { + problem = fmt.Sprintf(msg(c, "link.invalid"), opts.URL) + } + } + } + var l *ui.Element + ui.Column(c).Gap(u).AlignItems(ui.Start).Children(func() { + ui.Row(c).Gap(u).AlignItems(ui.Center).Children(func() { + l = ui.Link(c, label, target).Underline().DecorationThickness(1) + internal.Ring(l, k.Focus, theme.ControlRadius) + color := k.AccentText + if l.Pressed() || l.Hovered() { + color = k.Text + } + l.TextColor(color).DecorationColor(color) + if external { + Icon(c, "external-link", IconOptions{Size: 12, Tone: IconAccent}) + } + }) + if problem != "" { + l.Error(problem) + ui.Text(c, problem).TextColor(k.Danger).FontSize(theme.CaptionSize) + } + }) + if l.Clicked() { + slog.Debug("mujica: link activate", "url", opts.URL, "opened", target != "") + } + return l +} + +// linkCheck reports whether raw has a scheme, and whether it can be opened. +func linkCheck(raw string) (external, ok bool) { + p, err := url.Parse(raw) + if err != nil { + return false, false + } + switch p.Scheme { + case "": + return false, p.Path != "" || p.RawQuery != "" + case "http", "https": + return true, p.Host != "" + } + return true, p.Opaque != "" || p.Host != "" || p.Path != "" +} diff --git a/link_test.go b/link_test.go new file mode 100644 index 0000000..3b85ae4 --- /dev/null +++ b/link_test.go @@ -0,0 +1,95 @@ +package mujica + +import ( + "slices" + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestLinkOpensURL(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Link(c, "Charter", LinkOptions{URL: "https://example.com/charter"}) + }, 300, 100) + if err := tt.Click("Charter"); err != nil { + t.Fatal(err) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + want := []string{"https://example.com/charter", "https://example.com/charter"} + if got := tt.OpenedURLs(); !slices.Equal(got, want) { + t.Fatalf("opened %v", got) + } +} + +func TestLinkAction(t *testing.T) { + n := 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if Link(c, "Show details", LinkOptions{}).Clicked() { + n++ + } + }, 300, 100) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + if n != 1 || len(tt.OpenedURLs()) != 0 { + t.Fatalf("clicks %d, opened %v", n, tt.OpenedURLs()) + } +} + +func TestLinkInvalidURLIsNotOpened(t *testing.T) { + n := 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if Link(c, "Broken", LinkOptions{URL: "https://"}).Clicked() { + n++ + } + }, 400, 100) + if err := tt.Click("Broken"); err != nil { + t.Fatal(err) + } + if n != 1 || len(tt.OpenedURLs()) != 0 { + t.Fatalf("clicks %d, opened %v", n, tt.OpenedURLs()) + } + if !tt.HasText("This link cannot be opened: https://") { + t.Fatalf("texts %v", tt.Texts()) + } +} + +func TestLinkCallerError(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Locale: ZhCN}) + Link(c, "手册", LinkOptions{URL: "https://example.com", Error: "没有可以打开此链接的应用"}) + }, 400, 100) + tt.Frame() + if !tt.HasText("没有可以打开此链接的应用") { + t.Fatalf("texts %v", tt.Texts()) + } +} + +func TestLinkCheck(t *testing.T) { + for raw, want := range map[string][2]bool{ + "https://example.com": {true, true}, + "https://": {true, false}, + "mailto:a@b.c": {true, true}, + "/notes/42": {false, true}, + "?tab=info": {false, true}, + "http://[::1": {false, false}, + } { + ext, ok := linkCheck(raw) + if ext != want[0] || ok != want[1] { + t.Errorf("%q: external %v ok %v", raw, ext, ok) + } + } +} + +func TestLinkSnapshot(t *testing.T) { + snapshot(t, "link", 420, 140, func(c *ui.Context) { + ui.Column(c).Padding(20).Gap(12).AlignItems(ui.Start).Children(func() { + Link(c, "Read the court charter", LinkOptions{URL: "https://example.com"}) + Link(c, "Show the guest list", LinkOptions{}) + Link(c, "Broken link", LinkOptions{URL: "https://"}) + }) + }) +} diff --git a/messages_display.go b/messages_display.go new file mode 100644 index 0000000..b92688a --- /dev/null +++ b/messages_display.go @@ -0,0 +1,16 @@ +package mujica + +func init() { + addMessages(map[string][2]string{ + "editable_text.saving": {"Saving", "正在保存"}, + "icon.missing": {"Missing icon: %s", "缺少图标:%s"}, + "image.placeholder": {"No image", "暂无图片"}, + "image.loading": {"Loading image", "正在加载图片"}, + "image.error": {"Image failed to load", "图片加载失败"}, + "image.retry": {"Retry", "重试"}, + "avatar.unnamed": {"Unnamed", "未命名"}, + "avatar_group.more": {"Show all %d people", "显示全部 %d 人"}, + "tag.remove": {"Remove %s", "移除%s"}, + "link.invalid": {"This link cannot be opened: %s", "无法打开此链接:%s"}, + }) +} diff --git a/tag.go b/tag.go new file mode 100644 index 0000000..3ba3161 --- /dev/null +++ b/tag.go @@ -0,0 +1,109 @@ +package mujica + +import ( + "fmt" + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// TagOptions configure a Tag. The zero value is a plain label. +type TagOptions struct { + // Icon names an icon of the icons package shown before the label. + Icon string + // Clickable makes the label a button reported by TagView.Clicked. + Clickable bool + // Closable adds a remove button reported by TagView.Closed. + Closable bool + Disabled bool +} + +// TagView is a shown Tag and what the user did to it this frame. +type TagView struct { + Element *ui.Element + // Clicked reports the label was activated; never set by the remove button. + Clicked bool + // Closed reports the remove button, or Delete or Backspace on the label. + Closed bool +} + +// Tag shows a short label. Clicking the label and removing the tag are +// separate actions with separate focus stops. +func Tag(c *ui.Context, label string, opts TagOptions) TagView { + if label == "" { + panic("mujica: Tag needs a label") + } + k := Tokens(c) + u := Density(c).Unit() + var v TagView + box := ui.Row(c).AlignItems(ui.Center).MinHeight(theme.MinHitSize).Radius(theme.ControlRadius). + Background(k.Surface).Border(theme.BorderWidth, k.Border).Shrink(0).Disabled(opts.Disabled) + v.Element = box + box.Children(func() { + var main *ui.Element + if opts.Clickable { + main = ui.ButtonBase(c).Label(label) + internal.Ring(main, k.Focus, theme.ControlRadius) + } else { + main = ui.Row(c).AlignItems(ui.Center) + } + main.Gap(u*1.5).Padding(0, u*2).FillHeight().Radius(theme.ControlRadius).TextColor(k.Text) + if opts.Clickable && !opts.Disabled { + if main.Pressed() { + main.Background(k.Selection) + } else if main.Hovered() { + main.Background(k.SurfaceHover) + } + internal.Fade(main, Motion(c, theme.StateDuration)) + } + if opts.Closable { + main.Padding(0, u, 0, u*2) + } + main.Children(func() { + if opts.Icon != "" { + Icon(c, opts.Icon, IconOptions{Size: 14}) + } + ui.Text(c, label).FontSize(theme.BodySize - 1).SingleLine() + }) + if opts.Clickable && main.Clicked() { + v.Clicked = true + } + if opts.Closable { + if opts.Clickable && !opts.Disabled && (main.Shortcut(0, ui.KeyDelete) || main.Shortcut(0, ui.KeyBackspace)) { + v.Closed = true + } + if tagClose(c, k, label).Clicked() { + v.Closed = true + } + } + }) + if v.Clicked { + slog.Debug("mujica: tag click", "label", label) + } + if v.Closed { + slog.Debug("mujica: tag close", "label", label) + } + return v +} + +func tagClose(c *ui.Context, k theme.Tokens, label string) *ui.Element { + b := ui.ButtonBase(c).Size(theme.MinHitSize, theme.MinHitSize). + Radius(theme.ControlRadius).Label(fmt.Sprintf(msg(c, "tag.remove"), label)) + fg := k.TextMuted + if b.Pressed() { + b.Background(k.Selection) + fg = k.Text + } else if b.Hovered() { + b.Background(k.SurfaceHover) + fg = k.Text + } + internal.Ring(b, k.Focus, theme.ControlRadius) + internal.Fade(b, Motion(c, theme.StateDuration)) + b.TextColor(fg).Children(func() { + Icon(c, "x", IconOptions{Size: 12}) + }) + return b +} diff --git a/tag_test.go b/tag_test.go new file mode 100644 index 0000000..f2b76d0 --- /dev/null +++ b/tag_test.go @@ -0,0 +1,92 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +type tagCounts struct{ clicked, closed int } + +func tagTester(opts TagOptions, n *tagCounts) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Row(c).AlignItems(ui.Start).Children(func() { + v := Tag(c, "Velvet", opts) + if v.Clicked { + n.clicked++ + } + if v.Closed { + n.closed++ + } + }) + }, 300, 100) +} + +func TestTagClickAndCloseAreSeparate(t *testing.T) { + var n tagCounts + tt := tagTester(TagOptions{Clickable: true, Closable: true, Icon: "star"}, &n) + if err := tt.Click("Remove Velvet"); err != nil { + t.Fatal(err) + } + if n != (tagCounts{0, 1}) { + t.Fatalf("after close click %+v", n) + } + if err := tt.Click("Velvet"); err != nil { + t.Fatal(err) + } + if n != (tagCounts{1, 1}) { + t.Fatalf("after label click %+v", n) + } +} + +func TestTagKeyboard(t *testing.T) { + var n tagCounts + tt := tagTester(TagOptions{Clickable: true, Closable: true}, &n) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + tt.Key(0, ui.KeyDelete) + tt.Key(0, ui.KeyTab) + if !tt.Focused("Remove Velvet") { + t.Fatal("remove button not second focus stop") + } + tt.Key(0, ui.KeySpace) + if n != (tagCounts{1, 2}) { + t.Fatalf("counts %+v", n) + } +} + +func TestTagPlainTakesNoFocus(t *testing.T) { + var n tagCounts + tt := tagTester(TagOptions{}, &n) + _ = tt.Click("Velvet") + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + if n != (tagCounts{}) || tt.Focused("Velvet") { + t.Fatalf("plain tag reacted: %+v", n) + } +} + +func TestTagDisabled(t *testing.T) { + var n tagCounts + tt := tagTester(TagOptions{Clickable: true, Closable: true, Disabled: true}, &n) + _ = tt.Click("Velvet") + _ = tt.Click("Remove Velvet") + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + tt.Key(0, ui.KeyDelete) + if n != (tagCounts{}) { + t.Fatalf("disabled tag reacted: %+v", n) + } +} + +func TestTagSnapshot(t *testing.T) { + snapshot(t, "tag", 480, 80, func(c *ui.Context) { + ui.Row(c).Padding(24).Gap(8).AlignItems(ui.Center).Children(func() { + Tag(c, "Baroque", TagOptions{}) + Tag(c, "Favorite", TagOptions{Icon: "star", Clickable: true}) + Tag(c, "Archive", TagOptions{Closable: true}) + Tag(c, "Locked", TagOptions{Icon: "lock", Closable: true, Disabled: true}) + }) + }) +} diff --git a/testdata/snapshots/darwin/avatar-dark.png b/testdata/snapshots/darwin/avatar-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..07be84f8b2ee0e452ae6c71c1be5630c5377453a GIT binary patch literal 6314 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/avatar-light.png b/testdata/snapshots/darwin/avatar-light.png new file mode 100644 index 0000000000000000000000000000000000000000..588269918ffca0e05655bb45b9bbbc05153d118a GIT binary patch literal 6194 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/avatar_group-dark.png b/testdata/snapshots/darwin/avatar_group-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..14b0a7849a35501270a4c01b8bf7e3be736e6977 GIT binary patch literal 7889 zcmeHs^;aBSv+m#y2@(>5h2WA5?hxE2=-}=UTmpj!5(uur?ZutJT{HNgA-D`q26qc` z<~`p(aDF)JuDjN~y>|Di?!DLUs@hN0eyTe{T~&byp9&uU01zoD%4z}tXdh4~CN4Va z$*idFcL0DkO-WV?49PlJS#VjN?Zi2uBN2wJkE-+j8d}VrU!)$c<1}W8BonQGx;O{t zJ7qMm#Ww|gNND<_72NeAfU8ZcNYTJ*Kk=UrFG^}@M3EMZ;S__hoQ5V_OpA_6Z^tCI z;>E0_VoDk7IN?#G+&7Q0fQhVie>sB38}+UAS7lwrvFfZpB%^#t0589w{U2{o%9<^4 zy{d9`20VUR?Vr8>a4IGCU^2KnF4eV||2Q*nV@@6Zoj2FU+pV)^^8Uf<4pGN++vKjEJIOate6hNq_{d2!!pCPZt z(!fZN8h&P?pO2|TDkJrVp5kId@626?!xwtPLq>EM<(N+u;Tz}jab}RTc}`#$v>_L> z&s29P_nOoB=*E$Bw~o5JGCaw0_w$(Fm~A?Mu;&qb8LX{kY7FCa-j0fOg5FofIxR8D z{E*ewmLsh*==J{3AR{`J-@0?^!y9rd`|VwIAEWR%%jJi2=n(gb5pczp=&fV8VmWCw zw3-4m-uN1<@wk!kF3pH|q-&^B?Y9oAIh5oJ+I;huIo-JkV*OA&`j5T z++WPB{>Pedj52-OG>m52fD(Ekf?EFv>=x+=*1DgcVf!s*>}f?=dC-T^(2cgdA>~lg z(|<<FHRV-3dS!v3++f-zH6Y{l~zcuapJ7Xf0qE+$TR-P z&yV#n6EwCLc6y|s&~@G2?$k`JM9LM#%M2VaS?qT4{1h3^2XEogBJD4s^%El}Buy~S z2!cE`cEyf&jjJ+@yui;h1HQz4HWeBX(cS40A5O0Sh}=Xl1rOz2W5;zv{;cJX;`sU= zBlq0to_E%GA4A+T!o3hP%}&$I40S@bA?>=dRvEK&Pqi$`@P|KBE)zEOLE;cI2Z#Nq zYbVMySGIJ2=VQZd3}h+k0B-d2`P8?rKvq*HJ-@xFK@mac`H8Cu?oHBdRz*{ESRp8-uLw8W$H88A}zJO@hByJN$>) zaj}HP9WS31m6-ZnE(l&P>u4?gn!K5Oa*Sfff9`}joIM3;vor3l2M4Q1@8@a@a*eu{ z#VA_}+LsO)d}~UY@40dU4`*ywAq5f%cJ_AuPkmgcG$Ib~pjru~tch^T-_PS#FS_VDiX?fq>8UP5iVs36(Y zIG5o>bacxT&C8++>)4NKxwN<6!kfIEZK4~Y-CW6;2%Ht5jmH9sS~f#|PLlw`o|A(& zwE}6sQ|1wUe#|A&lbF|U;FwQOmddYk#2i)+po9Gj7Bi>t>sqUf-NC_#KcXb!_xmM* zXJgsd4?Ekb3MIv{+2Z?7Jf;saGSkS5dozb6xK&`2{<8S9>zt{-EU{!$oyNxd8*akS zUZ!Vw+6%+2iH$qb#&>62Au1l{<3~qNg#KbA?nh1!C1K&E4wm)QgkH@}v_fZZp0BJ- zu|50jJbyBUoHmK}^R~LZk^x|wJ)z||s63rNtRPHp*S7GYJmgZ2_gC+t=MMuy$e^x|6n4cmvJu*Cdv3ySXM&Ln~f@Eo$U4;D4w)lC|KY*UC(ou7E z7d`!scM>IVal7tyi_P+i%kQqW+?}eoMXgGGK|u(;*lkeUMed!^qjD@IDz4{P&EOj$ ztiBTxTm$ps#)iI&i=Hq@`;)6kZZnOoK<1^%=KvF7dYrTttKxp2TfWOqW5bu0=E4mD z=R4d$mbyhZ<FY zOOjTFsooE3f6{qn!L-$|I)6vVe3V-^=9GB$kvMEcvE}u!uJ>E*3hs!Aaf<0oi>aZM zofH&!?3^5Z3!zX3Y9F_!uJu!HhP&B-YXqWGo3y_;gNgwK*Ct?RnOO>-^Sg8(Xa5cc zQlyg$BES}LH9q#0@*VXbN$R+A$)K~pwTcbUg3Zp&!OYiGrjnk29;?Jjt}fbc_di9p z1-f4($)%fidk@YXt#;*&i*rao*0ap@ZRHgL@8>sbTXW@gJ2S1-5^Ees6V<27~UC|!Ua>BU2Jn?h~el0h7#_~}{ivx0IY zKZTbK8{Pf{4s~t12~ZBNlH%- zF(FTmpL5fqAZQc3u6zJ_-)Ku?b%ux9)J`6xHclQ?p&g8aKqforO3$q|o`}GTF*;?n zjk#=cF4!j>#M5(+}~BQU&<$7k@ZmZnPCho!FH zJyECut$y^C~)?qSvM!S7g#gisuK(KCr5^FI>qS&oCXv5s8a z-hx}k%mnN{Q)$L*3PqaZhM?0cB{Jfp54yc#CM(h6jbMd)5Hxh3f%!iD+H`MGak?U{ z@3#Z~X<&@Nzyoq-Di)PN7|3H)v26#?r;&sOd?DW#du0ys14^@3>}p?eiB!G73io&B zNEL6^P7_z9k9y7l;X=0L%%A*?fKeT=q?N0>m7PYun)&5LA!?c9)6r{M2l3B3!(qA=Jr&wIkVbsK2bIzXYm+3^_rgr0qzL}u`97zfxPZv^`x0lZa!12=|}HG`fxF|#7{}RX*N8B6BI?t5ep42 zgUp2e684g>apiv;z`5CTvU2j+jf(h_t$Tr3l+BTWZWTQM2fWXX?IE)25=tr|E&gAv zlUS9g72>~1xhPJSWgW9KCs!V?^wli0IN2tZHE3R*alvYmdTZd$Rb`x+X~a^JM$3UM zJ&53ntZl}S;fu&$d%hwriL9qe8L~B+^^G@RFXvCBa>zerN6YvgQk<|F1+~vn`3)Xg zNr^aUVSx8It}(EJw=-hd@77*RUa!`(8Fc+_ZUHy(L*Lh?sd`?$h+h>BQ}et^(E}}I z%uQHJe|ggrg>IBDm`)_0V{*#il*5)7sX$t5p~>bk|Dc^Sz48_8`0<^1p~Wyt41|`9 zn(Lm?_dHvZ19%x6hSNueUNMerX+-mQCY15b%wfZ(JS@?Q3QxS8IiE$#X6`v86aI^U zH(w6rqf)>G$mm23v#44mW-1$VvU2mZhQ;A4YQwJHJ%sh)=^*&SWJgW~VGb?#v>8U!9 zG|sf%r(V;#OZ;JvL)xodGsBir>o#kmyS-K42YotW2u|104Yi&Q%g9)`T!k!K0O@fV zBmtA$LMMYw0`UPSeqw@-h&(>5r~5O4)bfU#xx>X44IgwUtq312zReTfqvN1<%u6=pByu)ckkC+vH(n3I;pmJ)YO%y@!!7C0nEmKiZI?gT}=72)-Yf0Sw!HsHsS3Q1$L zN@Dau;12C4_t!6y<#76@2BxES1vL>P--9mm{5JV8i|W zO*Y{ShIG~IxHxWf?=t-C;SP^V?ZiT(^QudtR=W0D-&vS#P-qVX2szQ)Zyiz` z((@ToN4lq9hE=}4t^h`xFQ{{hD21W{UK9jJGW{S&sPv_=PZ(e5ctkH3=%?rhcn)Jd4&@v zapf8GG5Y2y^oN$)^pa*W}w*sF|hY9@kT47m}!Pf}*wxfd`0N_}8%EZgl%ot(n zb-)O(ll1>nODA!W*8Hp9<#;^kA(igOqEnuF%LBMX(;5v9tn{ZQ0k{$q?)IGT_E19C zIL#EsJplk`M6;U82${xL9lxcV&g#TO)2@dUO>29ZI#NxO&y>w-6JE30IveaWl+DMb zUhTO)o0nOlV+u^FCF8^-PNmiMmDR0Y`%TsB>vk4__m+I=2Xu7je(e6+w5F|t|4h*y zAGNiWl~w20l@-rg0B^)|7FH==2)js_CeK))!ZT$=BYtKkd8sLo7yuLzc#b<7OZ9lw z>jHH-fmd93-sJ_|xiKUdl@E%^=HYZni`OtJ;kMaHwkUr-{wPQ%zI6LglIwfdsM{9y z-B!10Z_iEJBuD~Ct}kspK2|<_ju3WBj_lhN%}NUBBxF(Zgf2C8c6P^&Tb=2m0sI%) z+Jy+^u3Z*kBK&iee0utM%?HQoilwEx2+D8M7#OIew7tBfwg$-oY_c|eQx>9~ zc%EgbsWY&r%}yBAw^@HKPpwOK_B!kJ=v#o1@|p$v%Sa2>a^`T9ruOnV;K8I}-d*hE zv#0|r7iby{uV~e5mX4u|i!%>GPdlKaM6@JPvFK7H=9eAX{@$Lbf6L{``fM570^;g2 z7cNdrNcAGB3Uft*X+ z^zUU+iF?1vnRov(Mterkr1AWs=fgN65mRH`l@C(kn#{f{_mzA`P$ZH*T#92q5mR;T z#)4fUjzu(}Y8&z>JKIZ(6nh)Fv7DiPWclmB;Q7aEE89i;l;MZ3Has+6G0y1dl|&Yn zlC<(R@S^O?_}8EAw&4iBwY+2TlgR|NSPVemr6hElR`T}Z+uXhnGPH!aut58rz1(RC!-8;L?#u?lD_lX$othh1k~UBY4$E` z>FQuCAlhkvKj3st6aaw2FV|>H-{9r<+4*if9zQ;uPY`3FD@=H$7*t8#?9CAwyprZB z3%nP-4%#0MiVNMXSoXZEe(i091-Lm|4d`;8=-sT6Q0pJ0DS<9D+|EuMr;A5an|F>m z23{0V+A2(~7!#Se4EC%ov->*e>P7__PYfxB6nk{5uu?Y~g2YEAx*1+mQ5_;*l~z;) ztW!Qs`7||;9*zcB`96Jxp%UGwGk54ujT%`Z2>UjLAA8Y~#rtN_g&`idTdG7(?hNot zsz`aQ^wIpwceSiCBrG;5+Eej`V^+-Ia(nhf5ERmqRx(nDCxc2bsW$1Y!Yq;W^4U4& z3w-b`@Vz#Lr~#F!&)>I2Q~6hy(3Lj7xhvaTzr3o4`~2nw$Ga&Q$ZpgNM1Ti zj{oDzwFG#CvS!J7{=wdmCAyEjOR*9i04hBx-urC9(<%R^ud?TQu=qJS7Zn9X-reHI zIa&!f8~*i4p=x)b1A7s&SWdEJ{iBA%#g28|Za>N#H(h_*WePLm(KDmSVvWS1=3e}m z9x%3%F^C;W$jifh$mPwpkZMUi7#D|=Qly)s@S|OxdF_&;-{(OY4Ui(3J-{X#`i9-{ zwmdbN@LSsu_%2M|*sEg9!T?1v6UQvv7ph!OjQqduN~2RpYVjrY^6l}X&nwyywzQ>z z<(W&0VF_(i6I5lR`o=M~BGkci%oO{LN?4+l2IQFaQ{NQVesSScsIfA5Q`=7W7bdFm z#yoxs+b)IhbQg1|&d}<94E<~OcR@BXM;8-C#!WF@Gprk044`vSVnM>!ah$ePnqzsI zjITJTlFb$OL{>rwds5^NQ;lwDmKgm5krv3$5`9h`X(Wtq_N2u4A|{)CH6;B zL&n332VNB6P$-*@*4w=7Kff|!-L(4`Vip5)3JBTlK&Gd!u2)+rnTAH36U%QSz`nQ&45$1 zTa?Qn7MGUDgP~S5U30p|CfLbvK4Gqr%$IG!#9dNPW#~*NbbECwsDUNq2dE-SY7HJvba+gPT1? zvG7;xtGY183z3+9s~STPaXZ#vO+o|`#nE3@X;e;8Jxu^6!q6&QGkYB;_xK8_2HnVE z&oIYwg5N}h^tc&?4fi2`gBV86mMyY59r_DVprTEM8GouzP2-aPDpis^(U=H;)kT0a zB3Y!@|5&-&Z~`{7qIkz|)d0y zdQpD}0qER8`?TiEtny~)WNAH^%)nqg(Ek_zcl{7c;2%6FK|}Dbzj4o-$MYSYVZ1c^ if`wYw&2<|u3mxJ*m`4B<&oudg0ei~ZGgVDE$Tq*YK!N(}OEzqW;x-ZDI8zG0uJqrO zJbv{2;?{!3@s>0Tt2|ww+Brb;=vxZ~6HW^*#|?w5FDsce0kH!RJbapY|0NK&6o49y!3%@9Sjn{(gC& zXCeQOg`5QoV|NLx_69&1O!riTDwdDb+mC-zVVx{mLTY&tL5g}$*o)ao%ah!V))*=35k}L0Ix{Rn&3m(?)5P6*CDy-yL3sTHv>aB6 z`2YR6Z7`XFwtHeHVyuZaGv6jB^CrXm&7qVaD|?~@oF212Rq#4EyznaHAhhx7yWY@? z40B6V)B6)Q7ISYj-%D;L(QQ6;I#cF5me0||N5SsZ~7PX&V<>fW-?9*Jioo(yJhQd2_vd>+BuLwx;JJMuX1pG zCBYXgA_l#sFZ27mV}QpHUUBfSen8m$jjhDVd@6C2D2We2FFG?d&s(D~({cQrlJoWF z(|WJZ@wByI0&Wdq53QdpgWQ2A4q&q^VnVu|gPct2cA7+|*-z!pJU4HrH=0~P;wIUX zaA!;*@lKETu{oC4G+tTBu}Am(l5o&eTgO?c^i9`HZhLBE-;K0nb}VbS-%VzNG!W-$ zw`1NfpA`FgN&abMl9ILD0q4iUp&!MCdx)?F&`SOIytDN1vA?H=%v@scoUZ?#5<=hr z`tcBaamCaRI@ld6rIA1d6JW@#NZCr`|b)4Pj$+B7Bo_WBZbn zPEWI?-O4OC>pjJDI&QC;Tnvh>0`iqkqG|lqOEYvzN_8v^fdu{5K13zoa?e*CeLu zG~fpwuxVvx@@X9U;9KskO6KR=tRG*mzJF>H{^E8C-8ys?&pBMcA3(>7$6$2xTt`?i zhEBrLYU2CVm3i|6QV}nbh;Ut+tap4yLmm76Iu`Od!Qa&6FMs&jAI{&^w49V$WJx2Wd|OZ5R)_J&i>9 zZ3_!6p!luSHSbuW17?>#>9llf4R7z!X>*ht(H65Mtqw+KvqC@^E%NjVfgX(W(r7L& zE1$r5UCbXA_fa7n-k&?r8=^tKk2<}h6JNUN4$oeoOl><}cJV)qY75%ti?!va8l&E< zotTKOfZNZWNT$B-HZ}4;Yep7+gxy^(Uo{<#WE9$FhU+y{N2*TGwc(b(@U)1MYG21) znOQ#2|9x16zTnpV2^wa{UGnuCy#8=Q?B`I#60-*&?2LOfQU$Bv5PmG=QGSAY9u7RM zu@Uwx;`=^^+%gq_Y zFN@j4&sJCkN8^>LSKJjd&DeO+tZRNp59REAwI@_QK)zpp)oGAHX0mxm#8`rE?h83p3k zlb7irbAmfdl{{>lx?2wU$;LVX~7;_PfeNgVeD6h$nb6kZs-mwM!7dto4z40Yh`_^J@(ji zXHYljA0%IikhnL~R@BxP@XkIrMEhsV-~L}>y1JW7oMrst!IFXWKq#}QmG`0=RaD-< z@gkzm={(iE72Q=LrmMASTwa2C37_HKR8<%mLs~8F)G?VMFO-5o84O}(l}J;<#-Cz9 zO12Y*CLm2I)<1vNFRTy;*H87vj~gH)O)(9z;Uo@tPVpsY^d<#{w)&}p7nal;Bv z2lee_aM{yX&%A>O&pf(1yc^WOzc4Rh+NzITsohnoMSE+PLSsVRH!dN(0A$Qs%lLQi zxDxe$=!}6;v(c+(-^cvg*a*U!7v3Y>UICu){BN|wJA%ug-E2Q74Km{aVe^9u#igf1 z(_ioZRc*1yl~!Ctd6n7em}C7~sosb?0Q%d}HqXFDV-kh?`?bUSp5nP{_$K3HO#|_mbM@GCr5Vq6Z`Sw{FN-olP~;kVKUO}}&!hcG;r!3xJCd_nm?2!i&Rsw7V0VFib@aw;FFGNWo^(i4-E@FXI5 zK7SO68(w!)f0yt>v3$~&JpGHnPFoe{8r{)lK>ni+X;MPjUnJm|4|q%|ise%MrUDt9 zw zB;b=Jp1vu{?sL9JalSadeyAsRSnqyUveo@Qjao56d0dfM)9t`;xTYk)+v~?OR)o(0xn+yat`Cw zzE&9)YO=fkxm>-5;>YEgli*7g&TPALS`EV1jmPS;cMrB|HYpmlXVS~P23udpUP;h7 zvVM>Yr?ng#O3_NB!l*{^+$A(N;E%x>n+c+at+)%qI?I&wXqq@4tfHStS^M zPEahba9%7_O<;`=VxGfT6e<nOxq%NF1 zZ5A7o#{p0RlyP|^sVp8c1pEum^$NtknqcbpS z+1rGDC6=X^ZEq8FVgw&7Von+s_P?RgP*aK<-LR~yEjrn|DB!T*95C^@tG{Pr#Zjqy zL&V045L*!y#x*e1F{-n&%XP8&?e2yMgyRSr8f+@35TFiGhNkz;Z(u726Sy~pTLwx5d*-mdhq>)i5{p*fYKwK>$du}OWYiB=b zeJO(M?QwI*LP2e2eCy$YcP_Mke`c?6)zb0iS=o{+q&M}fa@SWxxgVA_Ti)umlZQg! zMo3NphFu(lfE%kI=XXmhtS0~9i3(S8ET~2h*R%}X23W_pU)rtf2&Gk+&>Z0|-A3ff zJ%0B&p`p~78l7`tFn>-*Oi^5yxf66fOF-tDN1EC z`BZi|0~j=0>wWKpovEC5pxzhm^2D5$y|cH_qTi46%z+1IUgsrlr=AT?vkRQ-v1gCX zUVt8TbS(Y^xz*SZ5fKI5#2uWSf{E-c`R2yxm+rC;{548cn4-BVTh@|2u#If~f^uiW zl$k00$Id2`{LbBv8b|f)yi@GKeO%Iw-TM1z(Ps>G4qBGo4Nkmiq-N>zaK)lwgZ zPvVxx?#rC%4%$GsL^k_kC7;2T8be2cFV6=jW>GcS)5SWD9s1V1>s;RC$O?>A-Uo~O z^`p86H;OSqQoe4+s%<~^QzyBgoy7{uFkM16>-0uodwP$~nJ&}!tSE?huxk3ip^}ZC zd-!2e5YjtX86LSZp_v?q6S`C(^;vbZh5pNOGS)|Su9<~i;Zjgn@I>pkpUb*2M>lsz zP(!{i>zfZp(K$Iw3r;O(qj!E@2Z3O?=KYPi%IEGR^#E$B(NtBQ*G3rfdgudiH+-DN zJ~{g)2EZQ8BURlwL?jlPOA~s}wSm_6ub=Qgq8PbuV7(wNa#B8u_Ya zc(N}YJYyawhJ;|VuhbenCAjmMzkmo`)^{AYoD6Ii)}TSKTvu(-b@A|!N33I_s9=hQaV1iK48Q~6JT%;v zh?>)^Z`bZg-!uq%=yPvCoeZ@-H|Luh+G&8tit4*pOmAz{Bxlop4x;O#bj!Sx6< zL#F~5ttLQ?cP`Y>C@m$4x-kAi0V?2x)E`LvUJSUjd8hTpwd;ubvwjM-=>@&0a>TC} z577Ixx9ZE(MiEkfM91!q*d2x*g--_iR8(=&K%y*rdHEszJ<&GFN^#=q(k))7 zeisM_VWk(BqHXf7LR~eRI@OvEeRB21g^DTpa(Hbwm$e0F_Aa1mJRn|E=1labNtZuu zMaE^VzE%&STCl9dHk&BS$w4%`hen7PHQp;s>KD|Bkf|yZBo?x76$)Gzfb-w3VGAg=>!0J^mNe z*@ImT$BvE!$P)Df@8&zJllVl9b!(63BlgR$<~>*IOVC)k=yBQx2!(Nacl7)>6;CHr z`KH(HwI8B}w1DrghInA_lM}NTa9j}}8uB4E$TDf#D-dWX@gklV8)-6`8Dr)xAeUyM z$DkFc*z1Xen-uP#aSMo6JM$Wy$TDJ(^lhhpt_iTy@5oJXxXdKPR@2zH*~fitV>Grm zj`6dq7BXtO02SyozBFC8RdKL%wCmon*z7rs{^obEBrPv!D;J+#5eE8b)I8wy^8C7b>hZ3SOHc1S+< zIya--Sey(d*3W^pD^`^yU`9!1EmFnFYVBP!8^{O0utuS14$^|1-u4E3lXrww&k%`HZCe<;LojZ0~1|%Yp7w zsSu}&@tw+!khPlfg?f3&$XYsJ^wC$&t!E~KkH$u z6he_39gMVHVJ;kBIq`jwE4{5$RmQMfF>N~~+-w0nArINi@4_$8PTE$AeoxG}#-LuV zUYyJh+^@AZh^M=|-t&eCrnj}8&$sKC`0Tac1l9Rpr6dxmk)EOB>E}8gxWZq23i`NG zBlpUuDiEa)mx5f4o^%-gEPUo=X1T0fMhF;jU;H~KRX$%YYw(4amdVZK-M}m05!T5y zM~Cfg(5%srmrdLn4lb&xsSkFgTtP$qSNyanV0QmfjM-(N9qSnt<&$9-n5He25F-^I zX9&qFQ-!~XTYDyV)Os4VgFw+?hduiyeDimduygf1AX{o|0^bR4 zth=TsHQY*?k{s8F+vl%zA3mOr;9q^k_Lkj9)gEwpjHO#JMf@W%jtgle8AtX+N64po zeLbVRKCYf5(FZ;~;q}Ym=ezkI4v?&5s^H!*l25!{{ghsA_k|Q{SUtR;R3F#ow{mjf zzTyi7Q6r@dwJ34EKOS0&|JGAE*ro*^%4fFWx;$OgCjxE7zf^g}UJkMVtxAz<8xD@P z&v0%}mTNCx5?&e@WJV9e681Txr?M>xO+Tx~^_IO>h1Y( zy=m1%PWP^X`A8J|C=20#BO;#ltbw&4sNHf?I?9=lq@7NF0%qY)f2~_c!Q*FDIn!lk&t0h_$MedXYEpiwY_$@heY5%jMd3i zveV&smF|r2^t>7O8b7qkM9rppoobXkd+GJ79;Vlsg)FR?;%E8bEaq9unrq>=aWzeK zf)>FV&xXDS-cAjYs{e3qFk_dCAqXw{Ia1CM-Dt?oN`|FSGW|CDL-<;)v{g5lqh6$UVzKaq;Db#cll=e3|6Mz*JvKpC ytMvbSy$JYqTVDH@x(9$mX&U_K|KS0OwFt<77a9ugbuj;H7lrrAvK1iH(EkEca{1N( literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/badge-dark.png b/testdata/snapshots/darwin/badge-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d97c0e4ec198fc0e53f810f41e37d06ed5ce1b63 GIT binary patch literal 6189 zcmeI1S2P@6yT(TlC2E2pM)Xdi8;SB)qxUvyh|zl&M$ZTngb}?()aWG{U6|-C45EwP zTXcpw`M&d=`*V3N&i<|SuJ>ZEy`JZ}c-P)*$LeaoBqx1L3IG7e)l`-A0RX(Azr7vt zy}va&B5eZz0G_HT$r}b{@8ysL(ik;g=m@qMa<>IIurkiDMnwOkU`LY0qEx`sb=mYi ze<=_;!Qc>};#(ye(325-oDkWvLiY5XlCmCKU2FxbKrE{r>kGBMTofs%VWk>ygjxeDQoNvkh3S_iW zW7{T6639&95sv&0pb+|Grwa-fAW-R(5FXK=^^f%@<|MBBA3iHcsBu{G8ohnU*10SX z{_aV(C8W>I#ZD|t0Gn*UFM6Qc4ERs!(FP7t^{a4KWGRr0ktJzPD}GS^4@|UmV}(l7 zWUpumApd5DnaA!UJ*(^e8q!FCf3rtPQx|>11N3NoXV~5QE-H$kciQOZci`ho1$=AA z-w((`g6JPY!i`ArB7L~2bHk~!osw17S@GIXK(Ik*`tO9;W;}}|EP(3&d^d9i4HF+w6EeVS zh30=oxF)^NyhvN}g&F7_9~`7ZPw)U!3GSafbwTtZGl2ijQy|W+1DE7l$i-m+j1(1k zU3=zmzx6APJFy*8o64*$V%U-$9E9cVR&2QlW^jF0&J?vZj`M7&+?(FY+L zMiP?k7sieHFVvVxv&md8f)-Lg3Zzgz&=dxJ_BC_JOY$RE<08ZQ`V9GSr4*67rqVGV zD8R{P0&Kn2>8c)dJNkp72?33p_pZske@@NpC)EJLxq8s13H zweN0DQ9@X?O+ijdNqTm`2@GEbU}AZ5(&IiH)jw`6$&%a;u8uwBP^u)yylr-DF0W3D z0!9DC=!H_jZ3q$!9y+B<&;WkRI6M+HsQcrcIj}=2Cv7VUfu!bK9^HrAfYk`fB3DIA z>?+q@>a}j}{WT^xp!LFf@vb^iBi^0}?dkdBHB6LjHK@`^&&NTzayaKc;3&7gpTTFN z$`qrk=e>svaJ7FlDD=vu(VOU0 zU4v23i#v5dXI)V2qM`(BaU33(Tu&rN3wlHw7Dqojy&(M4{<^hDR^GT;|Kz0Bv1g7n zcLE;qZI=$l8U>~3vuL&82Y+bCzuK%K2T*Z-e2|4#CX#TDKLO_aH;g4Sl03y=g5xvIapJl_nL|y?DX4=Y5V8m(a4oSCeJ1{8)v``V4&W;C{O;<5It_ z#p&?@qwrN0(N9ADjhFW!2_B&B7Ua@6;g1?|jOsR(w-?!<@X(e~h%+;QrGR6gRLI91 zg$ar01xJ+Iay1D^Xq3nw#g`mYzqN4>G6nT^-Rn`~(1 zX^WD@KB7-(ax~M7V+qH#t;&2F7xxl-q)Esz3TL@-Q=D?Eur4-OIO(*vNknZF)Zu4+ zOVIBwImZKtN>-?MY#L{OXN~Tjy9lZO96ritS0cE0`azY85FnPrcf0efI#DbLdfM}H z>`3vj)u;P&qKpvOc5d-(SaRK?_0HWo&qQw=nH8FvBU4{h<@KvdrU~^lJ~45od0p_6 zX9FdZGB)qxCu{b$hP_~r=lV#=(w-}QHjNsg0mdVjkQGE^hesew9nEp{X#r2qCeCei z@!UT2_@#A1YUxL!*uyrr>#~)Nx*hFK$pRLDtd`kUo`z7NwJ6<2RU>X}KkQ1mrG$eE zZo`=SCYRJW<%ygSmmOZX?646Pg3~ivjWBT`D9x%ebY;?HNMaXVevl4&;$AyvDR#Tk z%Pe!dO4RjrR*-o^i2!#$S%3TSf zYao#AprFC%hhe1X;|S(XJXfyWT}ltx>yflbbX1kxVn^3ydth#K08H!)acTubSX@e* z9~_AGqWp?w3z>$jlx&jZq?G%0^0_SouWputAv_mzyliUY`;dxG4}>aB!PswliQpYW zGPFNP8o=u6=mNw2l)r8k!eWcQ$Z$(pW@hALr{e?ie=cWOTrDZi@_EswM$umZT zz1J?`GnCbnI`KE2w@3xX(5VRgmxBXe7DHd)iqi6O#QC)naY@V&f*VX$J$7TLk2vzZ6xcQn-0Dwv^!3+v~;M}hw5n#ml0S z1eNe{fzVz?zF_T#1AV%vxQNs-$HkTl7Qm~438lE?9pB98=;@rpus5F?yce2VzzM@xa-X-gSVTEj9F3%!7g(nRaH-xWG4gALYkip)S{qT z>*U@!A~xrSq271%+f@I&HaX1EnU6vKXit;1ck-V+JfPL!WS3ZzGtYW4xonpU+gCC# zSx>qV>9$>oY6B7#lY_u|d6}iSb$m%9$y4bIV;hC{_L!cl*J6@*vEBM_RjRXSU?HOv z`Wz77U-#26L0?{(;2I)Cf!d&Vnwo8X=+)xf5!hjC=>&ZKWVd4lynNEzI4DYr3dP#! zDNCX2Qk0UBsdsD5XW#^#*1y-j#iOJ>qrvNbOVA*##}Ydj`?(kce8Xt&AmjORc45$q zT5gK~V86dpdGm!QRPDT3#$2r|y(l9hxJ?{){p>vp&(}yQ#8mrnE1TGc z!^}0(CB;xg==yYn@dX74ObVE8y_$Ucz(Za8NeJOP<8rB>KNaZZz3l@T-*+9qcDJ_T z67yQaaK98Zmf-K1;8Q!rjSzzoAPt;bIp_8?&~ctajY?TEKwCS}!BIuvV|0&>^HUEh zX99KeC0QhrHYYj%u;XQ~80n5HgddP!Rfa0~=CZ)#gn#!wMX0_zBe@iaAO;Sv_-R~Wfl zME)ZS>vK=>BNcT9GPm9BaPyF9x`PGQlc|iD^uAiFskxpqMzO=%Z1W-e-3EPLa`zsnia=KrUx>8=Az}v7xmEZ)J z<%DBx5Pdm_e!_WQY>vs-B_`$u+L1MA({eqhh401R`v;=;4TsWUk3h%Un7(JZT(^xO z?LC&l6sJuXh8Y<69PMQkWk3dgzlw4?>UhmkCiCVMf|>wLaHJpYs)_E^>;2rgtdnMI zx}g|Q`?Ptjtu4}}g3k(79gDEI-4Io4IZ>0yXDcsFr+QIqQ`s5z>cR8d4@ueO_Vu%B$)+t5I$WPyvAT)23{$I-q{pI210b~U z=|H+PyWY{Uu}^dMr;H5Lh+Dbaw%hJ@sRz`e!J^!?(w%OwFjlgtP7$XrOy+a)-kl5` zw5Q{p?XRqjjmy4VmpbkgskLG+_c#y~a2oq^A9v+XgwL9uLq`oPYS-<~(^0OuwAC^R zX|PvOPdBGW*tBcWyk_WmUENw+tN{gt7vT=&9%Ab2)2jVv-I%l#z3XmE`c6^=61|(U z(@U0l;n4SVgYfx}iIr9-6N!?lN(KAYG^dcu!SiRKz5(x4@ByZ%kgJBq#-yin^lHr( zj699k(;@Z%(+NnqKsR z+^9XF32}Tt!rg7&gD@N}P5M@7x3FAR&g|;ytGS1PIgpo0q)x6q7Z5tW^P#NH0=GZC zGl^0lerD}H({_W9wAyqKsJp$3Sdx)xBP5LR@d4tT%)DVR%%mB%KD}#Uz8z*=q^%Je zxY}G-_vOnMmy2BK2O&Ewy~`Y(Ru|zL|9ChoHH9zX{Z^k`PmSv-4=owi3#;OD8fh4K zU4-94_-yT2kdVhdGxIB_jXei$O-sc)BCq75ZHkn27Pa4}eilyxh0h`@(g`8L*rx=8 z%=5=H%Njulq08s@4(RMAKKn^$8B}X zSuat9#Stg3!Y%MsBoPb6$a1(zsfXL`KL0ana8)Y5CrC71^Suzs@>{Is!OLI~3njB5 z*k>j@fSJyDkEzc2alfda?SRh=@zj2uhm!!^c@IXGmAYTCp(izAHz{Vhk&W%g_ze1a zOvvHwBobmU-W=a%(y=R>p~}(a<$6j)H&48nNU2|9m{a}dTre=P^*To9?yicFq1R^o z)c`cW8+~9iiS$L`u5(R7uO&N$;eSwzH!+*-&WGO`uVOTm9@5{3JD6RSlYykA{;<@} z?10|D0}-Sq*sEh@x$CQ&$k$}IJ6Ac((zuk`7PMkUK$noFLO&?ixb`Ppx$0^r>~1yf zf;d6AFh3Ic*_Hu0*+B`j;ZWm?d@3$U7Bx2_8e*6^NS zpaU!|Gd*BSbeRmr@$S<&RikwK9c)AC#Pm4Yfy|21ew+;D>z=JH&oHbVgS?VEKiK$M z%l97t=oWm{c$K*Yrc?Q0qia)`uL1SPKVjb0rZTDD)o-TaHGHhV=<>Q6uvu@v^ zfOl{Ahe(*?OpL3oWl;O$G3=Y-Qc*GsF!1B&<4iRLh9rS@l4Q0 z{Yq+LjfHOc`aQ)K@_j3*hMSq)v^>|a>$Nr_k%pULRLctw_si~%KQl3-E%E0e%^ASa zp8T}Y(a>p}6aHwVtJVi2^-6=Nij!lE!z687Q0>O5f+WA<^=jv0L#ate{(9uxGvK?v z%nZSevAVsB8@23;di+%T{Za-M~4^yxmh&IA8!fFbs6{-!2`yEBNT(zR# z3fQrlzp$%bbU3M(c0rlvjI56(LK~82zc7EpXxABsTJ6P$eN)$sF}lZSEz#~8GU>j$ zoA>o>>lnHGVv^hg2$ixH03z1?5=|`PS1TWjD~uECWzy8I9Q|57?qACdWw85~+;+mg zyBWWquzBY5I_>%yQdW!M7|YakTQ9Fr*%yMLd079IADv;rE5Bv!|9WSFnyq2jtj|b{ zR@Ei2($v&f<}Cm6{4Yy?_5Ocbcc%Ei%3`m%?@k!7$PxABo`|p2-){hbnzFW1mBOpY F{{qQA)pY;> literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/badge-light.png b/testdata/snapshots/darwin/badge-light.png new file mode 100644 index 0000000000000000000000000000000000000000..1b5c0296c32d5c2ed5c7a4d3746a45cd480196b3 GIT binary patch literal 6239 zcmeI1Wl$7eyTBJj=`IQBkPamj5a~`qSa9iPDUp(rmX?-W5di^V=?(?SrE^)jyOw5Q z_wxSV_ukL<D#Qfz1ONblSWQ(?9{|7_yq9h8u?L;4zn)qMU(m&VDYw@f)KX%*aADIm>I~hd?StJ@>9q>cSWeP_vWDp23}0&2)YJ zE}REcG#tGD-MYB`O##C7CK7EB>r!2FCMG0Y5nfOMSu? z>Q&;1%+|c;d(A%XqregWLryPZTLAw3=ojl;!3yR7KgYhz10V$x3oGlwxqc3;>_vKh zyFOUI6qvR0_)+~lnKGHA+UL7~)+psi#c}64k4a3)Y{>sZOZczVcZ+Gk!3AYML#Vy~ zinN|hdEM;3)Y5wh_!=zF2v%IdrV@Pp?_0UW&L3B$NXF=Lw0r!46w1*f|w*6HN8~Tr!=wi^Eg)I(sz-X z#Jh14Gv^xg&s>K}m)B9Lw9rX=^U!#_>VI=7X=BRbJSoQT!6dsod<`3mhbCqLU;fh? z$1(smMr2*+7+V5_&Xn@xKlXOwmRH4ZlNQb)8<6gP58d2-3ICrckt$5v)o)KamCdRq zxiY!Raqxr-2t;CG7vSYk=msDFTBd{3c5rV6*N`Br6OVIXlVPuDXitLr?5)|lHK`f= zm>R6|Md=^fj9Bjta%Nz_i%V9&<7Vygh@;zZo0l++9%GH zCIMLJ{wNVZdAhY9EU?5}VH3u*YsLcUq*g%!sUGfTC)_4>?z zd_k#D)AMc0c#tbS|C;zYqy5$oGlDK*fO{xa%wN|$Q3)S9u)FGuLZx0$&kBQd9x z4FfqnF4r_qVo@$9LqWJ1hIrS>DQveU3rIF3avdU67e{GOn2;p?j1xeRZjEbfYV zMHXp^olJl{$(<41Khb!RpiqCGlh2lF)a;eh&lY`}a@Zol&B3>mdik8ryP zek{oa(xjBPy~8F2jThpswK%xP&T>$+czG$CXX|oQebuU*Y`d=-E&5D=Mj-SDT+fJi zz@o%|Dt&`EA($-vU18q}r85k9uL70pYBO$>eqL{ce=eg|H{LaSn=)%B5{+|M`Q43! zqH&I&tE``hkId%Cu1}ngekEU{`OG4KmUvk7dF0aAU)mg&TmmazF{jMh#EuG=A@j}h zNl6c8eNc-0>S9V7WAXaRlW)%Yzx6Nmoz6r{8q1!2v=L>xJMf1YP!2sL7(d#WiD};8jobJl;xLpl{OGrC zjW)FW&4u0Z9G#D~Ua4E0ZPRV2Rk^lL@Tv>cQ-sLV$u{1SE_olFvf#D4ye+4Iixcsi zKt(B;qwD2KLKV>hMU+=LImgDN4tZDQj}+i{uGj-7r8Y%HVtm3+UKdJ8o-%Fz0-z=r zmLueft6b2zcQLZsnT@o>sN=Od8xo(GM|*_hAr-8Wze|^m)uk#~UTy%{T{$73_XBSA zepM7t?t+wP_QLs&35`tKS^7ia*1ZE<5)3Mf6$F`XrEs@byWg%hNy@qNXI$*mpK0B~ zlsK0p>;p?j<1zjMny~ph1UNE8kwKhWqZ|b+8M(^N6zd1dBXBI=X<_&w(Di zugoCfc6W#hRIvVjq!TyNMu$+R+yn#>3*6fJYq zBE~?^px$AQTfSs{TUNsP)NdxFYPP7tq}hIBX)jjY9*W7Ha(G$o&AvaT&B60L`$J`T z=R+UQ#&2P9^a~+k45aN1JInsr!$((i0E_9JT&9hST?w!*GWL{#ʭ`eZzLn&!qm;vYj0j5RN7@{T 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/heading-light.png b/testdata/snapshots/darwin/heading-light.png new file mode 100644 index 0000000000000000000000000000000000000000..03c5e0a6c22fd11196e6d025171a0b49d07c355c GIT binary patch literal 15798 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/icon-light.png b/testdata/snapshots/darwin/icon-light.png new file mode 100644 index 0000000000000000000000000000000000000000..65c3fec0f715fde2640beb849aa645fd4d0b3348 GIT binary patch literal 5115 zcmds5hcnzk|Nd&xNl5e>f`sD`o#;^yQKCi(qTJ~cy+oAgM356Dh#Ea{I?;0^N|e(s zB3zWy&f%iJH@`pPH}Aaj?0oi_-JRLl`F!TteKyg=NQa(=g9ZQqdOck&Qve`wCvp=i zQey0w4?YF}<~}_wb@Sl-?SddXGt1?##@>9feAKb*IaIreR5YfCcbl!68BDODOMD2= z!S*dECHG@&<@6-3vI^7Z429vrhg)JLXQO1%Ak^C}_D8#E= zto~Y7SwAKdTt44b=wW`oCF%f>lfIlFr{m%C`(Ht)-k-|5At57j`)_?5AxHx@%|B{H z1WOO{dr=h9^HC2=%;r!-M`MYkZ%*wcvsPRyLc=bZnLMSdDntI4-6OQ!Bgh~5lp|Gn z^~XtfvY%2LWq{+^tO9@czSn^~hwKx2f#|R3moR2oI)du0@g!PiX*JQfM0O(mk+9gG zeDN^r%N?l#&a8stw+{MW?veF3+>m`Cop0yX)+&9t(cpZg#xyWmVn;*ayFL7W64}Q@)bNn z#mOwO&c6qF;j*#Wm&RdsljY+40F;)1Z%wx(0ZMYoWe>Djyp7D#!)buvh6}Y2{bN4P z(V%(}hsd_!v0T;gwFckvSM_q8y-^R|l3cVn8yK7bfRYj?+lF!7llGiTFgNg9YS4AE zNgKR$rM0biux(#GnnWN0Hd%cbqQVZ&UoEZ-UcR)=IMbXf8&PZ|DcE)NP#NgcKREzJ zA(^QKL7-=1rN-eoZRs489fVtPHJH=!#ed_{*--^6;Obbx`jxzKZG+d8C_n^kd#g>0C@D-zf;Bhg_$02(z+A4Sc{V`ljrFQUPG_ zwxqGu;`D7fXGv04h#zMyhE~E;@Am-u^4XZP;|ELKCaG1w;}x?#dxl+q3RkWL7cLo@ z=79ApPQX_}HsyzG{K{odZ_-Bke!Ag26hprtMb}Azrh09a zWaCd_VlteblpTlJoQ!+CZI_e~9YViCH|*#XirP6-V(2`+Dq zw-eFgw4Zd%S2=&CIbQ00$-B0c=KKZATO}!_pjE0%9BS0E4*(2u|B-vsN3UG2oMvOI zmdvjpmq2tz7Jn96_)HuZ$9hJu@v_Li6Y^c6=u031bvy$^w+gIFi7(4GHCja-iIi*Z zD5~LyK;O^-$8MZvcrvJJlcY?-iqZb3dk<9b%stglukskr3WKImug4!xGOls^??Xl(VK z%`f95n+1T#lOxz#?dZdYV!@5|Yc7~`zg@;K%Cl5nA9CP4bQ^Wse)3>ubl8}SvBO=j ztD&h}OM%+P>|PD%7K_TaYrq8j)$9`mm}9kJo}fi@4`_bUbeRfZlbT8Na&v{bdB8@U z82^G5p6$zS{S@@Fj#$GgCa$%3g@rXe>`x@S(sV{;CY8UjRrSko7ugme=5Yv

&;{p+8@9Ze^5%Q{~X>)JSa9H@7L#FknHCueG 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 NdfG->bsCN_{{cZTf0qCN literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/image-dark.png b/testdata/snapshots/darwin/image-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..286963c4894e311f1315462bd2fd54a7e22c95a3 GIT binary patch literal 7050 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 Fe**}EN`n9Z literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/image-light.png b/testdata/snapshots/darwin/image-light.png new file mode 100644 index 0000000000000000000000000000000000000000..9b207d68a367f4f5b462842b9d58c180b428b8b1 GIT binary patch literal 6942 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/kbd-dark.png b/testdata/snapshots/darwin/kbd-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..119484d20c9c643d3f62d0dd237a520ea850b3f5 GIT binary patch literal 2646 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 AYybcN literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/link-dark.png b/testdata/snapshots/darwin/link-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9d8705f36be6417df8d5ece0ee0f8feeeb69c5b2 GIT binary patch literal 10794 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/link-light.png b/testdata/snapshots/darwin/link-light.png new file mode 100644 index 0000000000000000000000000000000000000000..81476194926cdbfb9ed9044705199a01758b3efe GIT binary patch literal 10421 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

f(+)x{ArO+jb!4SEsiiM&nmY)m|zXy#vL36WT zxb9+B(m+&QCa4YTPR+#Me=>YOF$JPl{jp_D8a}xw5_ec|UC#LO$>8C2(0tW4D#LZk z4cQ`Z@%G3oY9-oi`KD=gV4Ire>d&{BTWGX}lhrMMB)le8`cNO&Em7W(J7c5>vg-rm zt)kbCJW;Hj;9P>Rtc*Z?1R=y{lw$CsPlX-);EcoE3X@mvY>-saGR9oO^VUQ|31Q?d z&#%|Q1|WB2e)HX&?rO!p8fruOEL_^_cYUVN;{eBhY4&v&bU{_snN8I?dVRtF4LScmDg%G&^LGoPD)v_THG}FzW^9au5SPU literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/tag-dark.png b/testdata/snapshots/darwin/tag-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..71ce0bf8fb50125d760779812ec054304daa0a33 GIT binary patch literal 5625 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 GME?gsLlKVv literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/text-dark.png b/testdata/snapshots/darwin/text-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d1fb0d1f690790c5037c6bf155a1d425012291 GIT binary patch literal 21270 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≫&>r<8z=?>1ru~dp)qlJ7JkX>G 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/text-light.png b/testdata/snapshots/darwin/text-light.png new file mode 100644 index 0000000000000000000000000000000000000000..28658cae435373d1c4a78d501198ce689b30d170 GIT binary patch literal 20426 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 literal 0 HcmV?d00001 diff --git a/text.go b/text.go new file mode 100644 index 0000000..52cea10 --- /dev/null +++ b/text.go @@ -0,0 +1,85 @@ +package mujica + +import ( + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// TextOptions configure a Text. The zero value wraps every line. +type TextOptions struct { + // MaxLines cuts the text with an ellipsis after that many lines; zero never cuts. + MaxLines int + // Selectable lets the user select and copy the text. + Selectable bool +} + +// Text shows body text that wraps at the width it gets. +func Text(c *ui.Context, s string, opts TextOptions) *ui.Element { + if opts.MaxLines < 0 { + panic("mujica: Text MaxLines must not be negative") + } + e := ui.Text(c, s).LineHeight(theme.LineHeight) + switch { + case opts.MaxLines == 1: + e.SingleLine() + case opts.MaxLines > 1: + e.MaxLines(opts.MaxLines) + } + if opts.Selectable { + e.Selectable() + } + return e +} + +// EditableTextOptions configure the editable mode of Text. +type EditableTextOptions struct { + // Saving shows the caller's save in progress and blocks editing. + Saving bool + // Error is the caller's save failure, shown below the text. + Error string + // Label names the text for assistive technology. + Label string +} + +// EditableText is Text's editable mode. Double click or Enter edits; Enter +// or leaving confirms, Escape cancels. Changed reports a confirmed new value. +func EditableText(c *ui.Context, value *string, opts EditableTextOptions) *ui.Element { + k := Tokens(c) + u := Density(c).Unit() + line := k.ControlBorder + if opts.Error != "" { + line = k.Danger + } + var box *ui.Element + ui.Column(c).Gap(u).AlignItems(ui.Start).Children(func() { + ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Disabled(opts.Saving).Children(func() { + box = ui.EditableText(c, value) + pad := u / 2 + box.Padding(pad+1, pad*2+1).MinWidth(theme.MinHitSize*2).MinHeight(theme.MinHitSize).Radius(theme.ControlRadius). + BorderWidth(0, 0, 1, 0).BorderColor(line).BorderStyle(ui.BorderDashed) + internal.Ring(box, k.Focus, theme.ControlRadius) + if box.Hovered() { + box.Background(k.SurfaceHover) + } + internal.Fade(box, Motion(c, theme.StateDuration)) + if opts.Label != "" { + box.Label(opts.Label) + } + if opts.Saving { + Spinner(c, SpinnerOptions{Size: 12, Label: msg(c, "editable_text.saving")}) + } + }) + if opts.Error != "" { + box.Error(opts.Error) + ui.Text(c, opts.Error).TextColor(k.Danger).FontSize(theme.CaptionSize) + } + }) + if box.Changed() { + slog.Debug("mujica: editabletext changed", "len", len(*value)) + } + return box +} diff --git a/text_test.go b/text_test.go new file mode 100644 index 0000000..6b12e7d --- /dev/null +++ b/text_test.go @@ -0,0 +1,137 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestTextSelectAndCopy(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Text(c, "ID-4821", TextOptions{Selectable: true}) + }, 300, 100) + if err := tt.Click("ID-4821"); err != nil { + t.Fatal(err) + } + tt.Command("selectAll") + tt.Command("copy") + if got := tt.Clipboard(); got != "ID-4821" { + t.Fatalf("clipboard %q", got) + } +} + +func TestTextNotSelectableByDefault(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Text(c, "plain", TextOptions{}) + }, 300, 100) + _ = tt.Click("plain") + tt.Command("selectAll") + tt.Command("copy") + if got := tt.Clipboard(); got != "" { + t.Fatalf("clipboard %q, want empty", got) + } +} + +func TestTextMaxLinesCuts(t *testing.T) { + long := "The court gathers in the hall of mirrors while candles burn low and the music fades away." + var free, cut float32 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Width(120).Children(func() { + free = Text(c, long, TextOptions{}).Bounds().H + cut = Text(c, long, TextOptions{MaxLines: 1}).Bounds().H + }) + }, 300, 400) + tt.Frame() + if cut <= 0 || free < cut*3 { + t.Fatalf("free %v, one line %v", free, cut) + } +} + +func TestTextNegativeMaxLinesPanics(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic") + } + }() + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Text(c, "x", TextOptions{MaxLines: -1}) + }, 100, 100) + tt.Frame() +} + +func TestEditableTextConfirm(t *testing.T) { + name := "report.pdf" + changed := 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if EditableText(c, &name, EditableTextOptions{Label: "File name"}).Changed() { + changed++ + } + }, 300, 120) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + tt.Type("summary") + tt.Key(0, ui.KeyEnter) + if name != "summary.pdf" || changed != 1 { + t.Fatalf("name %q, changed %d", name, changed) + } +} + +func TestEditableTextCancel(t *testing.T) { + name := "report.pdf" + changed := 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if EditableText(c, &name, EditableTextOptions{}).Changed() { + changed++ + } + }, 300, 120) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + tt.Type("draft") + tt.Key(0, ui.KeyEscape) + if name != "report.pdf" || changed != 0 { + t.Fatalf("name %q, changed %d", name, changed) + } + if !tt.HasText("report.pdf") { + t.Fatal("text not shown after cancel") + } +} + +func TestEditableTextSavingBlocksAndErrorShows(t *testing.T) { + name := "notes" + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + EditableText(c, &name, EditableTextOptions{Saving: true, Error: "disk full"}) + }, 300, 120) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + tt.Type("x") + tt.Key(0, ui.KeyEnter) + if name != "notes" { + t.Fatalf("edited while saving: %q", name) + } + if !tt.HasText("disk full") { + t.Fatal("save error not shown") + } + if _, ok := tt.Find("Saving"); !ok { + t.Fatal("saving spinner not labeled") + } +} + +func TestTextSnapshot(t *testing.T) { + name := "Coronation.md" + snapshot(t, "text", 420, 280, func(c *ui.Context) { + ui.Column(c).Padding(20).Gap(12).Width(380).Children(func() { + Text(c, "Velvet curtains fall over the long gallery, and every candle leans toward the open door.", TextOptions{}) + Text(c, "A single line that is far too long to fit and so ends in an ellipsis.", TextOptions{MaxLines: 1}) + Text(c, "Selectable: ID-4821", TextOptions{Selectable: true}) + EditableText(c, &name, EditableTextOptions{Error: "The name is already taken."}) + EditableText(c, &name, EditableTextOptions{Saving: true}) + }) + }) +}