Merge branch 'main' into worktree-agent-ab9ba373bc47c2352
This commit is contained in:
173 files changed
+9698
-13
No files matched your search
@@ -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(`<svg xmlns="http://www.w3.org/2000/svg" width="240" height="160" viewBox="0 0 240 160">
|
||||
<rect width="240" height="160" fill="#2B2126"/><rect x="20" y="20" width="200" height="120" fill="none" stroke="#BDA172" stroke-width="2"/>
|
||||
<path d="M120 40 L150 80 L120 120 L90 80 Z" fill="#7D2034"/><circle cx="120" cy="80" r="8" fill="#F2EBDD"/></svg>`))
|
||||
}()
|
||||
|
||||
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")
|
||||
})`
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,358 @@
|
||||
package main
|
||||
|
||||
var inputPages = []page{}
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
mujica "mujicaui"
|
||||
"mujicaui/icons"
|
||||
)
|
||||
|
||||
const inputCategory = "Inputs"
|
||||
|
||||
// inputVariants lists the button variants for variant pickers.
|
||||
var inputVariants = []mujica.RadioItem[mujica.ButtonVariant]{
|
||||
{Value: mujica.Primary, Label: "Primary"}, {Value: mujica.Secondary, Label: "Secondary"},
|
||||
{Value: mujica.Outline, Label: "Outline"}, {Value: mujica.Ghost, Label: "Ghost"}, {Value: mujica.Danger, Label: "Danger"},
|
||||
}
|
||||
|
||||
// inputSection titles one part of a demo.
|
||||
func inputSection(c *ui.Context, title string, fn func()) {
|
||||
ui.Column(c).Gap(8).Children(func() {
|
||||
ui.Text(c, title).FontWeight(600).TextColor(mujica.Tokens(c).TextMuted)
|
||||
fn()
|
||||
})
|
||||
}
|
||||
|
||||
// inputFlags builds one checkbox per named flag, logging each toggle.
|
||||
func inputFlags(c *ui.Context, log func(string), flags map[string]*bool, order ...string) {
|
||||
ui.Row(c).Gap(16).Wrap().Children(func() {
|
||||
for _, name := range order {
|
||||
if mujica.Checkbox(c, flags[name], name, mujica.CheckboxOptions{}).Changed() {
|
||||
log(fmt.Sprintf("option %s = %v", name, *flags[name]))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type inputButtonDemo struct {
|
||||
variant mujica.ButtonVariant
|
||||
icon, loading, disabled bool
|
||||
}
|
||||
|
||||
type inputTextDemo struct {
|
||||
value, err string
|
||||
clearable, readOnly, disabled, sl, fail bool
|
||||
}
|
||||
|
||||
type inputAreaDemo struct {
|
||||
value string
|
||||
max float64
|
||||
readOnly, disabled bool
|
||||
}
|
||||
|
||||
type inputNumberDemo struct {
|
||||
value, step float64
|
||||
empty, stepper, disabled, fail bool
|
||||
}
|
||||
|
||||
type inputSearchDemo struct {
|
||||
query, find string
|
||||
current int
|
||||
disabled bool
|
||||
}
|
||||
|
||||
type inputCheckDemo struct {
|
||||
single, mixed, disabled bool
|
||||
items [4]bool
|
||||
}
|
||||
|
||||
type inputGroupDemo struct {
|
||||
vertical, disabled bool
|
||||
}
|
||||
|
||||
type inputRadioDemo struct {
|
||||
value string
|
||||
horizontal, disabledAll bool
|
||||
}
|
||||
|
||||
var inputPages = []page{
|
||||
{ID: "021", Name: "Button", ZhName: "按钮", Category: inputCategory, Keys: "Tab 聚焦;Enter 或 Space 触发。",
|
||||
Code: `if mujica.Button(c, "Save", mujica.ButtonOptions{Icon: icons.Must("check")}).Clicked() {
|
||||
fmt.Println("saved")
|
||||
}`,
|
||||
Demo: func(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "021", func() inputButtonDemo { return inputButtonDemo{} })
|
||||
inputSection(c, "Parameters", func() {
|
||||
mujica.RadioGroup(c, &s.variant, inputVariants, mujica.RadioGroupOptions{Horizontal: true, Label: "Variant"})
|
||||
inputFlags(c, log, map[string]*bool{"Icon": &s.icon, "Loading": &s.loading, "Disabled": &s.disabled}, "Icon", "Loading", "Disabled")
|
||||
})
|
||||
inputSection(c, "Live", func() {
|
||||
opts := mujica.ButtonOptions{Variant: s.variant, Loading: s.loading, Disabled: s.disabled}
|
||||
if s.icon {
|
||||
opts.Icon = icons.Must("check")
|
||||
}
|
||||
ui.Row(c).Children(func() {
|
||||
if mujica.Button(c, "Save changes", opts).Clicked() {
|
||||
log("Button clicked")
|
||||
}
|
||||
})
|
||||
})
|
||||
inputSection(c, "Variants", func() {
|
||||
ui.Row(c).Gap(12).Wrap().Children(func() {
|
||||
for _, v := range inputVariants {
|
||||
if mujica.Button(c, v.Label, mujica.ButtonOptions{Variant: v.Value}).Clicked() {
|
||||
log(v.Label + " clicked")
|
||||
}
|
||||
}
|
||||
mujica.Button(c, "Loading", mujica.ButtonOptions{Loading: true})
|
||||
mujica.Button(c, "Disabled", mujica.ButtonOptions{Disabled: true})
|
||||
})
|
||||
})
|
||||
}},
|
||||
{ID: "022", Name: "IconButton", ZhName: "图标按钮", Category: inputCategory, Keys: "Tab 聚焦;Enter 或 Space 触发;悬停显示提示。",
|
||||
Code: `if mujica.IconButton(c, icons.Must("settings"), "Settings", mujica.IconButtonOptions{Variant: mujica.Ghost}).Clicked() {
|
||||
fmt.Println("open settings")
|
||||
}`,
|
||||
Demo: func(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "022", func() inputButtonDemo { return inputButtonDemo{variant: mujica.Ghost} })
|
||||
inputSection(c, "Parameters", func() {
|
||||
mujica.RadioGroup(c, &s.variant, inputVariants, mujica.RadioGroupOptions{Horizontal: true, Label: "Variant"})
|
||||
inputFlags(c, log, map[string]*bool{"Loading": &s.loading, "Disabled": &s.disabled}, "Loading", "Disabled")
|
||||
})
|
||||
inputSection(c, "Live", func() {
|
||||
ui.Row(c).Gap(8).Children(func() {
|
||||
for _, n := range []string{"bold", "italic", "underline", "copy", "trash-2"} {
|
||||
opts := mujica.IconButtonOptions{Variant: s.variant, Loading: s.loading, Disabled: s.disabled}
|
||||
if mujica.IconButton(c, icons.Must(n), n, opts).Clicked() {
|
||||
log("IconButton " + n + " clicked")
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}},
|
||||
{ID: "023", Name: "ButtonGroup", ZhName: "按钮组", Category: inputCategory, Keys: "Tab 进入组;方向键在组内移动并跳过禁用项;Enter 或 Space 触发。",
|
||||
Code: `items := []mujica.ButtonGroupItem{{Label: "Day"}, {Label: "Week"}, {Label: "Month"}}
|
||||
if i := mujica.ButtonGroup(c, items, mujica.ButtonGroupOptions{Label: "Range"}).ClickedItem(); i >= 0 {
|
||||
fmt.Println("chose", items[i].Label)
|
||||
}`,
|
||||
Demo: func(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "023", func() inputGroupDemo { return inputGroupDemo{} })
|
||||
inputSection(c, "Parameters", func() {
|
||||
inputFlags(c, log, map[string]*bool{"Vertical": &s.vertical, "Disable group": &s.disabled}, "Vertical", "Disable group")
|
||||
})
|
||||
inputSection(c, "Live", func() {
|
||||
items := []mujica.ButtonGroupItem{{Label: "Day"}, {Label: "Week", Disabled: true}, {Label: "Month"}, {Icon: icons.Must("calendar"), Name: "Pick a date"}}
|
||||
r := mujica.ButtonGroup(c, items, mujica.ButtonGroupOptions{Vertical: s.vertical, Disabled: s.disabled, Label: "Range"})
|
||||
if i := r.ClickedItem(); i >= 0 {
|
||||
log(fmt.Sprintf("ButtonGroup item %d clicked", i))
|
||||
}
|
||||
})
|
||||
}},
|
||||
{ID: "024", Name: "TextInput", ZhName: "文本输入框", Category: inputCategory, Keys: "Tab 聚焦;Enter 提交;Escape 清空(可清空时);Cmd/Ctrl+Z 撤销、+C/X/V 剪贴板;支持输入法组合。",
|
||||
Code: `r := mujica.TextInput(c, &name, mujica.TextInputOptions{Label: "Name", Placeholder: "Your name", Clearable: true})
|
||||
if r.Submitted() {
|
||||
fmt.Println("hello", name)
|
||||
}`,
|
||||
Demo: func(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "024", func() inputTextDemo { return inputTextDemo{clearable: true, sl: true} })
|
||||
inputSection(c, "Parameters", func() {
|
||||
inputFlags(c, log, map[string]*bool{"Clearable": &s.clearable, "Slots": &s.sl, "ReadOnly": &s.readOnly, "Disabled": &s.disabled}, "Clearable", "Slots", "ReadOnly", "Disabled")
|
||||
mujica.TextInput(c, &s.err, mujica.TextInputOptions{Label: "Error message", Placeholder: "Error message (empty for none)"})
|
||||
})
|
||||
inputSection(c, "Live", func() {
|
||||
opts := mujica.TextInputOptions{Label: "Name", Placeholder: "Your name", Clearable: s.clearable, ReadOnly: s.readOnly, Disabled: s.disabled, Error: s.err}
|
||||
if s.sl {
|
||||
opts.Leading = func() { ui.Icon(c, icons.Must("user")).Size(16, 16).TextColor(mujica.Tokens(c).TextMuted) }
|
||||
opts.Trailing = func() { ui.Text(c, fmt.Sprintf("%d", len([]rune(s.value)))).TextColor(mujica.Tokens(c).TextMuted) }
|
||||
}
|
||||
r := mujica.TextInput(c, &s.value, opts)
|
||||
if r.Changed() {
|
||||
log(fmt.Sprintf("TextInput changed: %q", s.value))
|
||||
}
|
||||
if r.Submitted() {
|
||||
log(fmt.Sprintf("TextInput submitted: %q", s.value))
|
||||
}
|
||||
})
|
||||
inputSection(c, "States", func() {
|
||||
ui.Row(c).Gap(12).Children(func() {
|
||||
e, ro, off := "1789-13-01", "Read only", "Disabled"
|
||||
mujica.TextInput(c, &e, mujica.TextInputOptions{Label: "Error state", Error: "No such date"}).Grow(1)
|
||||
mujica.TextInput(c, &ro, mujica.TextInputOptions{Label: "Read-only state", ReadOnly: true}).Grow(1)
|
||||
mujica.TextInput(c, &off, mujica.TextInputOptions{Label: "Disabled state", Disabled: true}).Grow(1)
|
||||
})
|
||||
})
|
||||
}},
|
||||
{ID: "025", Name: "TextArea", ZhName: "多行文本框", Category: inputCategory, Keys: "Enter 换行;Cmd/Ctrl+Enter 提交(可配置);滚轮与方向键滚动;撤销、剪贴板与输入法同 TextInput。",
|
||||
Code: `r := mujica.TextArea(c, ¬es, mujica.TextAreaOptions{Label: "Notes", MaxHeight: 160, MaxLength: 200})
|
||||
if r.Submitted() {
|
||||
fmt.Println("send", notes)
|
||||
}`,
|
||||
Demo: func(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "025", func() inputAreaDemo { return inputAreaDemo{max: 120} })
|
||||
inputSection(c, "Parameters", func() {
|
||||
inputFlags(c, log, map[string]*bool{"ReadOnly": &s.readOnly, "Disabled": &s.disabled}, "ReadOnly", "Disabled")
|
||||
if mujica.NumberInput(c, &s.max, mujica.NumberInputOptions{Min: 0, Max: 1000, Step: 10, Label: "MaxLength"}).Changed() {
|
||||
log(fmt.Sprintf("option MaxLength = %v", s.max))
|
||||
}
|
||||
})
|
||||
inputSection(c, "Live", func() {
|
||||
r := mujica.TextArea(c, &s.value, mujica.TextAreaOptions{Label: "Notes", Placeholder: "Write a dedication…", MinHeight: 80, MaxHeight: 160,
|
||||
MaxLength: int(s.max), ReadOnly: s.readOnly, Disabled: s.disabled})
|
||||
if r.Changed() {
|
||||
log(fmt.Sprintf("TextArea changed: %d characters", len([]rune(s.value))))
|
||||
}
|
||||
if r.Submitted() {
|
||||
log("TextArea submitted")
|
||||
}
|
||||
})
|
||||
}},
|
||||
{ID: "026", Name: "NumberInput", ZhName: "数字输入框", Category: inputCategory, Keys: "Up/Down 按步长增减;步进模式下 Up/Down/Left/Right、Home、End。",
|
||||
Code: `if mujica.NumberInput(c, &copies, mujica.NumberInputOptions{Min: 1, Max: 99, Label: "Copies"}).Changed() {
|
||||
fmt.Println("copies", copies)
|
||||
}`,
|
||||
Demo: func(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "026", func() inputNumberDemo { return inputNumberDemo{value: 3, step: 1} })
|
||||
inputSection(c, "Parameters", func() {
|
||||
inputFlags(c, log, map[string]*bool{"AllowEmpty": &s.empty, "StepperOnly": &s.stepper, "Disabled": &s.disabled, "Error": &s.fail}, "AllowEmpty", "StepperOnly", "Disabled", "Error")
|
||||
steps := []mujica.RadioItem[float64]{{Value: 1, Label: "Step 1"}, {Value: 0.5, Label: "Step 0.5"}, {Value: 0.01, Label: "Step 0.01"}}
|
||||
if mujica.RadioGroup(c, &s.step, steps, mujica.RadioGroupOptions{Horizontal: true, Label: "Step"}).Changed() {
|
||||
log(fmt.Sprintf("option Step = %v", s.step))
|
||||
}
|
||||
})
|
||||
if s.stepper {
|
||||
s.empty = false
|
||||
}
|
||||
if !s.empty && math.IsNaN(s.value) {
|
||||
s.value = 0
|
||||
}
|
||||
inputSection(c, "Live", func() {
|
||||
opts := mujica.NumberInputOptions{Min: 0, Max: 10, Step: s.step, AllowEmpty: s.empty, StepperOnly: s.stepper, Disabled: s.disabled, Label: "Quantity", Placeholder: "Empty"}
|
||||
if s.fail {
|
||||
opts.Error = "Not in stock"
|
||||
}
|
||||
ui.Box(c).Width(240).Children(func() {
|
||||
if mujica.NumberInput(c, &s.value, opts).Changed() {
|
||||
log(fmt.Sprintf("NumberInput changed: %v", s.value))
|
||||
}
|
||||
})
|
||||
})
|
||||
inputSection(c, "States", func() {
|
||||
ui.Row(c).Gap(12).AlignItems(ui.Start).Children(func() {
|
||||
a, b, e := 4.0, 2.0, 7.5
|
||||
mujica.NumberInput(c, &a, mujica.NumberInputOptions{Min: 1, Max: 8, StepperOnly: true, Label: "Stepper state"})
|
||||
mujica.NumberInput(c, &b, mujica.NumberInputOptions{Label: "Disabled state", Disabled: true}).Width(160)
|
||||
mujica.NumberInput(c, &e, mujica.NumberInputOptions{Step: 0.5, Label: "Error state", Error: "Must be whole"}).Width(160)
|
||||
})
|
||||
})
|
||||
}},
|
||||
{ID: "027", Name: "PasswordInput", ZhName: "密码输入框", Category: inputCategory, Keys: "Tab 依次聚焦输入框与显隐按钮;Space 或 Enter 切换显隐;Enter 在输入框中提交;粘贴可用,遮蔽时不可复制。",
|
||||
Code: `if mujica.PasswordInput(c, &password, mujica.PasswordInputOptions{Label: "Password"}).Submitted() {
|
||||
fmt.Println("signing in")
|
||||
}`,
|
||||
Demo: func(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "027", func() inputTextDemo { return inputTextDemo{} })
|
||||
inputSection(c, "Parameters", func() {
|
||||
inputFlags(c, log, map[string]*bool{"Disabled": &s.disabled, "Error": &s.fail}, "Disabled", "Error")
|
||||
})
|
||||
inputSection(c, "Live", func() {
|
||||
opts := mujica.PasswordInputOptions{Label: "Password", Placeholder: "Password", Disabled: s.disabled}
|
||||
if s.fail {
|
||||
opts.Error = "At least 8 characters"
|
||||
}
|
||||
r := mujica.PasswordInput(c, &s.value, opts)
|
||||
if r.Changed() {
|
||||
// The contents never reach the log.
|
||||
log(fmt.Sprintf("PasswordInput changed: %d characters", len([]rune(s.value))))
|
||||
}
|
||||
if r.Submitted() {
|
||||
log("PasswordInput submitted")
|
||||
}
|
||||
})
|
||||
}},
|
||||
{ID: "028", Name: "SearchInput", ZhName: "搜索框", Category: inputCategory, Keys: "Enter 提交(搜索模式)或下一项(查找栏);Shift+Enter 上一项;Cmd+G / F3 下一项,Shift 加上为上一项;Escape 清空。",
|
||||
Code: `matches := 0
|
||||
if query != "" {
|
||||
matches = strings.Count(text, query)
|
||||
}
|
||||
r := mujica.SearchInput(c, &query, mujica.SearchInputOptions{Find: true, Matches: matches, Current: ¤t})
|
||||
if r.Moved() {
|
||||
fmt.Println("show match", current)
|
||||
}`,
|
||||
Demo: func(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "028", func() inputSearchDemo { return inputSearchDemo{} })
|
||||
const text = "The rose of the court, the rose of the chapel, and the rose of the garden all bloom in May."
|
||||
inputSection(c, "Parameters", func() {
|
||||
inputFlags(c, log, map[string]*bool{"Disabled": &s.disabled}, "Disabled")
|
||||
})
|
||||
inputSection(c, "Search mode", func() {
|
||||
r := mujica.SearchInput(c, &s.query, mujica.SearchInputOptions{Disabled: s.disabled})
|
||||
if r.Changed() {
|
||||
log(fmt.Sprintf("SearchInput changed: %q", s.query))
|
||||
}
|
||||
if r.Submitted() {
|
||||
log(fmt.Sprintf("SearchInput submitted: %q", s.query))
|
||||
}
|
||||
})
|
||||
inputSection(c, "Find bar mode", func() {
|
||||
n := 0
|
||||
if s.find != "" {
|
||||
n = strings.Count(text, s.find)
|
||||
}
|
||||
r := mujica.SearchInput(c, &s.find, mujica.SearchInputOptions{Find: true, Matches: n, Current: &s.current, Disabled: s.disabled})
|
||||
if r.Changed() {
|
||||
log(fmt.Sprintf("find query: %q, %d matches", s.find, n))
|
||||
}
|
||||
if r.Moved() {
|
||||
log(fmt.Sprintf("find moved to match %d", s.current+1))
|
||||
}
|
||||
ui.Text(c, text).TextColor(mujica.Tokens(c).TextMuted)
|
||||
})
|
||||
}},
|
||||
{ID: "029", Name: "Checkbox", ZhName: "复选框", Category: inputCategory, Keys: "Tab 聚焦;Space 切换;点击标签同样切换;全选框在组内勾选全部或清空。",
|
||||
Code: `mujica.CheckboxGroup(c, "Notifications", []mujica.CheckboxItem{
|
||||
{Label: "Mail", Value: &mail},
|
||||
{Label: "Calendar", Value: &calendar},
|
||||
}, mujica.CheckboxGroupOptions{})`,
|
||||
Demo: func(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "029", func() inputCheckDemo { return inputCheckDemo{items: [4]bool{true, false, false, true}} })
|
||||
inputSection(c, "Parameters", func() {
|
||||
inputFlags(c, log, map[string]*bool{"Indeterminate": &s.mixed, "Disabled": &s.disabled}, "Indeterminate", "Disabled")
|
||||
})
|
||||
inputSection(c, "Live", func() {
|
||||
if mujica.Checkbox(c, &s.single, "Accept the terms", mujica.CheckboxOptions{Indeterminate: s.mixed, Disabled: s.disabled}).Changed() {
|
||||
s.mixed = false
|
||||
log(fmt.Sprintf("Checkbox changed: %v", s.single))
|
||||
}
|
||||
})
|
||||
inputSection(c, "Group with select all", func() {
|
||||
items := []mujica.CheckboxItem{
|
||||
{Label: "Mail", Value: &s.items[0]}, {Label: "Calendar", Value: &s.items[1]},
|
||||
{Label: "Messages", Value: &s.items[2]}, {Label: "Security alerts (required)", Value: &s.items[3], Disabled: true},
|
||||
}
|
||||
if mujica.CheckboxGroup(c, "Notifications", items, mujica.CheckboxGroupOptions{Disabled: s.disabled}).Changed() {
|
||||
log(fmt.Sprintf("CheckboxGroup changed: %v", s.items))
|
||||
}
|
||||
})
|
||||
}},
|
||||
{ID: "030", Name: "RadioGroup", ZhName: "单选组", Category: inputCategory, Keys: "Tab 进入组并落在已选项;方向键选择并跳过禁用项;Home、End 选择首末项。",
|
||||
Code: `items := []mujica.RadioItem[string]{{Value: "s", Label: "Small"}, {Value: "m", Label: "Medium"}, {Value: "l", Label: "Large"}}
|
||||
if mujica.RadioGroup(c, &size, items, mujica.RadioGroupOptions{Label: "Size"}).Changed() {
|
||||
fmt.Println("size", size)
|
||||
}`,
|
||||
Demo: func(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "030", func() inputRadioDemo { return inputRadioDemo{value: "m"} })
|
||||
inputSection(c, "Parameters", func() {
|
||||
inputFlags(c, log, map[string]*bool{"Horizontal": &s.horizontal, "Disabled": &s.disabledAll}, "Horizontal", "Disabled")
|
||||
})
|
||||
inputSection(c, "Live", func() {
|
||||
items := []mujica.RadioItem[string]{{Value: "s", Label: "Small"}, {Value: "m", Label: "Medium"}, {Value: "l", Label: "Large"}, {Value: "xl", Label: "Sold out", Disabled: true}}
|
||||
if mujica.RadioGroup(c, &s.value, items, mujica.RadioGroupOptions{Horizontal: s.horizontal, Disabled: s.disabledAll, Label: "Size"}).Changed() {
|
||||
log("RadioGroup chose " + s.value)
|
||||
}
|
||||
})
|
||||
}},
|
||||
}
|
||||
@@ -1,3 +1,499 @@
|
||||
package main
|
||||
|
||||
var layoutPages = []page{}
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
mujica "mujicaui"
|
||||
"mujicaui/icons"
|
||||
)
|
||||
|
||||
var layoutPages = []page{
|
||||
{ID: "001", Name: "AppShell", ZhName: "应用外壳", Category: "Layout", Demo: layoutAppShellDemo, Code: layoutAppShellCode,
|
||||
Keys: "Cmd/Ctrl+B or the toggle button folds the sidebar. Tab moves between the toggle, the sidebar and the content; arrows, Page Up/Down, Home and End scroll the focused region."},
|
||||
{ID: "002", Name: "TitleBar", ZhName: "标题栏", Category: "Layout", Demo: layoutTitleBarDemo, Code: layoutTitleBarCode,
|
||||
Keys: "Tools in the slots take Tab and Enter/Space as usual. Dragging the empty bar moves the window; double-click zooms it."},
|
||||
{ID: "003", Name: "StatusBar", ZhName: "状态栏", Category: "Layout", Demo: layoutStatusBarDemo, Code: layoutStatusBarCode,
|
||||
Keys: "Tab reaches action items; Enter or Space activates them. Plain status text is not a tab stop."},
|
||||
{ID: "004", Name: "Container", ZhName: "容器", Category: "Layout", Demo: layoutContainerDemo, Code: layoutContainerCode,
|
||||
Keys: "None: Container never takes the focus."},
|
||||
{ID: "005", Name: "Stack", ZhName: "堆叠", Category: "Layout", Demo: layoutStackDemo, Code: layoutStackCode,
|
||||
Keys: "None: Stack never takes the focus; Tab follows the children's order."},
|
||||
{ID: "006", Name: "Grid", ZhName: "网格", Category: "Layout", Demo: layoutGridDemo, Code: layoutGridCode,
|
||||
Keys: "None: Grid never takes the focus; Tab follows the build order."},
|
||||
{ID: "007", Name: "ScrollArea", ZhName: "滚动区域", Category: "Layout", Demo: layoutScrollAreaDemo, Code: layoutScrollAreaCode,
|
||||
Keys: "When the content overflows, Tab focuses the area; arrows scroll by a line, Page Up/Down and Space by a page, Home and End to the ends."},
|
||||
{ID: "008", Name: "SplitPane", ZhName: "分栏", Category: "Layout", Demo: layoutSplitPaneDemo, Code: layoutSplitPaneCode,
|
||||
Keys: "Tab focuses the divider. Left/Right (Up/Down when vertical) move it by 4 units, Home and End to the limits, Enter folds the collapsible pane; an arrow past its minimum folds it, the opposite arrow opens it."},
|
||||
{ID: "009", Name: "AspectRatio", ZhName: "宽高比", Category: "Layout", Demo: layoutAspectRatioDemo, Code: layoutAspectRatioCode,
|
||||
Keys: "None: AspectRatio never takes the focus."},
|
||||
{ID: "010", Name: "Divider", ZhName: "分隔线", Category: "Layout", Demo: layoutDividerDemo, Code: layoutDividerCode,
|
||||
Keys: "None: Divider and its ornament never take the focus."},
|
||||
}
|
||||
|
||||
// layoutCheck is a check box that logs its changes.
|
||||
func layoutCheck(c *ui.Context, v *bool, label string, log func(string)) {
|
||||
was := *v
|
||||
ui.Checkbox(c, v, label)
|
||||
if *v != was {
|
||||
log(fmt.Sprintf("%s: %v", label, *v))
|
||||
}
|
||||
}
|
||||
|
||||
// layoutChoice is a labeled drop-down that logs its changes.
|
||||
func layoutChoice(c *ui.Context, v *string, label string, options []string, log func(string)) {
|
||||
was := *v
|
||||
ui.Row(c).Gap(8).Children(func() {
|
||||
ui.Text(c, label)
|
||||
ui.Select(c, v, options)
|
||||
})
|
||||
if *v != was {
|
||||
log(fmt.Sprintf("%s: %s", label, *v))
|
||||
}
|
||||
}
|
||||
|
||||
// layoutSlider is a labeled slider that logs whole-number changes.
|
||||
func layoutSlider(c *ui.Context, v *float64, label string, lo, hi float64, log func(string)) {
|
||||
was := math.Round(*v)
|
||||
ui.Row(c).Gap(8).Children(func() {
|
||||
ui.Text(c, fmt.Sprintf("%s %.0f", label, *v))
|
||||
ui.Slider(c, v, lo, hi).Width(140)
|
||||
})
|
||||
if math.Round(*v) != was {
|
||||
log(fmt.Sprintf("%s: %.0f", label, *v))
|
||||
}
|
||||
}
|
||||
|
||||
func layoutControls(c *ui.Context, fn func()) { ui.Row(c).Wrap().Gap(12).Children(fn) }
|
||||
|
||||
func layoutFrame(c *ui.Context, h float32, fn func()) *ui.Element {
|
||||
k := mujica.Tokens(c)
|
||||
return ui.Column(c).Height(h).Border(1, k.Border).Radius(4).Clip().Children(fn)
|
||||
}
|
||||
|
||||
var layoutSpaces = []string{"Default", "None", "XS", "S", "M", "L", "XL", "XXL"}
|
||||
|
||||
func layoutSpace(s string) mujica.Space { return mujica.Space(slices.Index(layoutSpaces, s)) }
|
||||
|
||||
const layoutAppShellCode = `var collapsed bool
|
||||
mujica.AppShell(c, &collapsed, mujica.AppShellOptions{
|
||||
TitleBar: func() { mujica.TitleBar(c, "Court Records", mujica.TitleBarOptions{}) },
|
||||
Sidebar: func() { ui.Text(c, "Chronicles") },
|
||||
Content: func() { ui.Text(c, "The Grand Hall") },
|
||||
})`
|
||||
|
||||
func layoutAppShellDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
collapsed, title, status bool
|
||||
width float64
|
||||
scroll ui.ScrollState
|
||||
}
|
||||
s := ui.Local(c.Root(), "001", func() state { return state{title: true, status: true, width: 220} })
|
||||
layoutControls(c, func() {
|
||||
layoutCheck(c, &s.title, "Title bar", log)
|
||||
layoutCheck(c, &s.status, "Status bar", log)
|
||||
layoutCheck(c, &s.collapsed, "Collapsed", log)
|
||||
layoutSlider(c, &s.width, "Sidebar width", 160, 320, log)
|
||||
})
|
||||
opts := mujica.AppShellOptions{
|
||||
SidebarWidth: float32(s.width),
|
||||
ContentScroll: &s.scroll,
|
||||
Sidebar: func() {
|
||||
for _, n := range []string{"Chronicles", "Letters", "Decrees", "Portraits", "Ledgers"} {
|
||||
ui.Text(c, n).Padding(6, 12)
|
||||
}
|
||||
},
|
||||
Content: func() {
|
||||
mujica.Container(c, mujica.ContainerOptions{}, func() {
|
||||
for i := range 30 {
|
||||
ui.Text(c, fmt.Sprintf("Entry %d of the court record", i+1)).Padding(4, 0)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
if s.title {
|
||||
opts.TitleBar = func() { mujica.TitleBar(c, "Court Records", mujica.TitleBarOptions{}) }
|
||||
}
|
||||
if s.status {
|
||||
opts.StatusBar = func() {
|
||||
mujica.StatusBar(c, mujica.StatusBarOptions{Left: []mujica.StatusItem{{ID: "pos", Text: fmt.Sprintf("Scrolled %.0f", s.scroll.Y)}}})
|
||||
}
|
||||
}
|
||||
ui.Column(c).Margin(16, 0, 0, 0).Children(func() {
|
||||
layoutFrame(c, 360, func() {
|
||||
if mujica.AppShell(c, &s.collapsed, opts).Toggled {
|
||||
log(fmt.Sprintf("sidebar collapsed: %v", s.collapsed))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const layoutTitleBarCode = `mujica.TitleBar(c, "Court Records", mujica.TitleBarOptions{
|
||||
Trailing: func() { mujica.Button(c, "Share", mujica.ButtonOptions{Variant: mujica.Ghost}) },
|
||||
})`
|
||||
|
||||
func layoutTitleBarDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
leading, trailing bool
|
||||
title string
|
||||
}
|
||||
s := ui.Local(c.Root(), "002", func() state { return state{trailing: true, title: "Court Records"} })
|
||||
layoutControls(c, func() {
|
||||
layoutCheck(c, &s.leading, "Leading tool", log)
|
||||
layoutCheck(c, &s.trailing, "Trailing tool", log)
|
||||
layoutChoice(c, &s.title, "Title", []string{"Court Records", "宫廷档案", "A very long title of the royal chronicle of the court"}, log)
|
||||
})
|
||||
var opts mujica.TitleBarOptions
|
||||
if s.leading {
|
||||
opts.Leading = func() {
|
||||
if mujica.Button(c, "", mujica.ButtonOptions{Variant: mujica.Ghost, Icon: icons.Must("panel-left"), Label: "Sidebar"}).Clicked() {
|
||||
log("leading tool clicked")
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.trailing {
|
||||
opts.Trailing = func() {
|
||||
if mujica.Button(c, "Share", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() {
|
||||
log("Share clicked")
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.Column(c).Margin(16, 0, 0, 0).Width(520).Children(func() {
|
||||
layoutFrame(c, 90, func() { mujica.TitleBar(c, s.title, opts) })
|
||||
})
|
||||
bar := c.TitleBar()
|
||||
ui.Text(c, fmt.Sprintf("Window controls reserve: left %.0f, right %.0f, height %.0f DIP", bar.Left, bar.Right, bar.Height)).
|
||||
TextColor(mujica.Tokens(c).TextMuted).Margin(8, 0, 0, 0)
|
||||
}
|
||||
|
||||
const layoutStatusBarCode = `bar := mujica.StatusBar(c, mujica.StatusBarOptions{
|
||||
Left: []mujica.StatusItem{{ID: "sync", Text: "Syncing", ShowProgress: true, Progress: 0.4}},
|
||||
Right: []mujica.StatusItem{{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true}},
|
||||
})
|
||||
if bar.Clicked == "bell" {
|
||||
opened++
|
||||
}`
|
||||
|
||||
func layoutStatusBarDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
width, progress float64
|
||||
showProgress bool
|
||||
tone string
|
||||
}
|
||||
s := ui.Local(c.Root(), "003", func() state { return state{width: 640, progress: 40, showProgress: true, tone: "Warning"} })
|
||||
layoutControls(c, func() {
|
||||
layoutSlider(c, &s.width, "Width", 240, 720, log)
|
||||
layoutCheck(c, &s.showProgress, "Progress", log)
|
||||
layoutSlider(c, &s.progress, "Percent", 0, 100, log)
|
||||
layoutChoice(c, &s.tone, "Tone", []string{"Neutral", "Success", "Warning", "Danger", "Info"}, log)
|
||||
})
|
||||
tones := map[string]mujica.StatusItemTone{"Neutral": mujica.StatusItemNeutral, "Success": mujica.StatusItemSuccess,
|
||||
"Warning": mujica.StatusItemWarning, "Danger": mujica.StatusItemDanger, "Info": mujica.StatusItemInfo}
|
||||
opts := mujica.StatusBarOptions{
|
||||
Left: []mujica.StatusItem{
|
||||
{ID: "branch", Text: "main", Icon: icons.Must("folder")},
|
||||
{ID: "sync", Text: "Syncing", ShowProgress: s.showProgress, Progress: float32(s.progress / 100)},
|
||||
{ID: "lint", Text: "2 warnings", Tone: tones[s.tone], Secondary: true, Action: true},
|
||||
},
|
||||
Right: []mujica.StatusItem{
|
||||
{ID: "enc", Text: "UTF-8", Secondary: true},
|
||||
{ID: "pos", Text: "Ln 12, Col 4", Secondary: true},
|
||||
{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true},
|
||||
},
|
||||
}
|
||||
ui.Column(c).Margin(16, 0, 0, 0).Width(float32(s.width)).Children(func() {
|
||||
if id := mujica.StatusBar(c, opts).Clicked; id != "" {
|
||||
log("clicked " + id)
|
||||
}
|
||||
})
|
||||
ui.Text(c, "Narrow the width: secondary items fold into a count, last first.").
|
||||
TextColor(mujica.Tokens(c).TextMuted).Margin(8, 0, 0, 0)
|
||||
}
|
||||
|
||||
const layoutContainerCode = `mujica.Container(c, mujica.ContainerOptions{MaxWidth: 320, Center: true}, func() {
|
||||
ui.Text(c, "A centered column of prose.")
|
||||
})`
|
||||
|
||||
func layoutContainerDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
maxWidth float64
|
||||
padding string
|
||||
center, fill, clip, wide bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "004", func() state { return state{maxWidth: 360, padding: "Default", center: true} })
|
||||
layoutControls(c, func() {
|
||||
layoutSlider(c, &s.maxWidth, "Max width", 0, 600, log)
|
||||
layoutChoice(c, &s.padding, "Padding", layoutSpaces, log)
|
||||
layoutCheck(c, &s.center, "Center", log)
|
||||
layoutCheck(c, &s.fill, "Fill", log)
|
||||
layoutCheck(c, &s.clip, "Clip", log)
|
||||
layoutCheck(c, &s.wide, "Overflowing child", log)
|
||||
})
|
||||
k := mujica.Tokens(c)
|
||||
ui.Column(c).Margin(16, 0, 0, 0).Children(func() {
|
||||
layoutFrame(c, 200, func() {
|
||||
mujica.Container(c, mujica.ContainerOptions{MaxWidth: float32(s.maxWidth), Padding: layoutSpace(s.padding),
|
||||
Center: s.center, Fill: s.fill, Clip: s.clip}, func() {
|
||||
ui.Text(c, "Of the Court").FontSize(18)
|
||||
ui.Text(c, "Prose keeps a readable measure inside a container.").TextColor(k.TextMuted)
|
||||
if s.wide {
|
||||
ui.Box(c).Size(900, 24).Background(k.Selection).Shrink(0)
|
||||
}
|
||||
}).Background(k.Surface).Border(1, k.Border)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const layoutStackCode = `mujica.Stack(c, mujica.StackOptions{Horizontal: true, Gap: mujica.SpaceM}, func() {
|
||||
mujica.Button(c, "Save", mujica.ButtonOptions{})
|
||||
mujica.StackItem(!showDraft, func() { mujica.Button(c, "Draft", mujica.ButtonOptions{Variant: mujica.Secondary}) })
|
||||
mujica.Button(c, "Cancel", mujica.ButtonOptions{Variant: mujica.Ghost})
|
||||
})`
|
||||
|
||||
func layoutStackDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
horizontal, wrap, hide bool
|
||||
gap, align, justify string
|
||||
}
|
||||
s := ui.Local(c.Root(), "005", func() state {
|
||||
return state{horizontal: true, gap: "Default", align: "Auto", justify: "Start"}
|
||||
})
|
||||
aligns := []string{"Auto", "Start", "Center", "End", "Stretch"}
|
||||
justifies := map[string]ui.Align{"Start": ui.Start, "Center": ui.Center, "End": ui.End, "SpaceBetween": ui.SpaceBetween}
|
||||
layoutControls(c, func() {
|
||||
layoutCheck(c, &s.horizontal, "Horizontal", log)
|
||||
s.wrap = s.wrap && s.horizontal
|
||||
layoutCheck(c, &s.wrap, "Wrap", log)
|
||||
layoutCheck(c, &s.hide, "Hide Draft", log)
|
||||
layoutChoice(c, &s.gap, "Gap", layoutSpaces, log)
|
||||
layoutChoice(c, &s.align, "Align", aligns, log)
|
||||
layoutChoice(c, &s.justify, "Justify", []string{"Start", "Center", "End", "SpaceBetween"}, log)
|
||||
})
|
||||
align := mujica.StackAlign(slices.Index(aligns, s.align))
|
||||
ui.Column(c).Margin(16, 0, 0, 0).Width(420).Children(func() {
|
||||
layoutFrame(c, 200, func() {
|
||||
mujica.Stack(c, mujica.StackOptions{Horizontal: s.horizontal, Wrap: s.wrap, Gap: layoutSpace(s.gap),
|
||||
Align: align, Justify: justifies[s.justify]}, func() {
|
||||
for _, n := range []string{"Save", "Draft", "Publish", "Archive", "Cancel"} {
|
||||
mujica.StackItem(s.hide && n == "Draft", func() {
|
||||
if mujica.Button(c, n, mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
log(n + " clicked")
|
||||
}
|
||||
})
|
||||
}
|
||||
}).Grow(1).Padding(12)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const layoutGridCode = `mujica.Grid(c, mujica.GridOptions{Columns: []ui.Track{ui.Fixed(96), ui.Fr(1)}}, func() {
|
||||
mujica.GridCell(c, mujica.GridCellOptions{ColumnSpan: -1}, func() { ui.Text(c, "Header") })
|
||||
ui.Text(c, "Name")
|
||||
ui.Text(c, "Ada")
|
||||
})`
|
||||
|
||||
func layoutGridDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
first, gap, rowGap string
|
||||
header, tall bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "006", func() state {
|
||||
return state{first: "Fixed 120", gap: "Default", rowGap: "Default", header: true, tall: true}
|
||||
})
|
||||
tracks := map[string]ui.Track{"Fixed 120": ui.Fixed(120), "Fit content": ui.FitContent(), "Fr 1": ui.Fr(1), "Fr 2": ui.Fr(2)}
|
||||
layoutControls(c, func() {
|
||||
layoutChoice(c, &s.first, "First column", []string{"Fixed 120", "Fit content", "Fr 1", "Fr 2"}, log)
|
||||
layoutChoice(c, &s.gap, "Gap", layoutSpaces, log)
|
||||
layoutChoice(c, &s.rowGap, "Row gap", layoutSpaces, log)
|
||||
layoutCheck(c, &s.header, "Header spans all", log)
|
||||
layoutCheck(c, &s.tall, "First cell spans 2 rows", log)
|
||||
})
|
||||
k := mujica.Tokens(c)
|
||||
cell := func(text string) {
|
||||
ui.Box(c).Center().Padding(8).Background(k.Surface).Border(1, k.Border).Radius(4).
|
||||
Children(func() { ui.Text(c, text).TextColor(k.TextMuted) })
|
||||
}
|
||||
ui.Column(c).Margin(16, 0, 0, 0).Width(520).Children(func() {
|
||||
mujica.Grid(c, mujica.GridOptions{Columns: []ui.Track{tracks[s.first], ui.Fr(1), ui.Fr(1)},
|
||||
Gap: layoutSpace(s.gap), RowGap: layoutSpace(s.rowGap)}, func() {
|
||||
if s.header {
|
||||
mujica.GridCell(c, mujica.GridCellOptions{ColumnSpan: -1}, func() { cell("Header, ColumnSpan -1") })
|
||||
}
|
||||
span := 1
|
||||
if s.tall {
|
||||
span = 2
|
||||
}
|
||||
mujica.GridCell(c, mujica.GridCellOptions{RowSpan: span}, func() { cell(s.first) })
|
||||
for i := range 4 {
|
||||
cell(fmt.Sprintf("Cell %d", i+1))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const layoutScrollAreaCode = `var offset ui.ScrollState
|
||||
mujica.ScrollArea(c, &offset, mujica.ScrollAreaOptions{Label: "Log"}, func() {
|
||||
for range 50 {
|
||||
ui.Text(c, "A line of the log")
|
||||
}
|
||||
}).Height(200)`
|
||||
|
||||
func layoutScrollAreaDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
direction string
|
||||
offset ui.ScrollState
|
||||
lastX, lastY float32
|
||||
}
|
||||
s := ui.Local(c.Root(), "007", func() state { return state{direction: "Vertical"} })
|
||||
dirs := map[string]mujica.ScrollDirection{"Vertical": mujica.ScrollVertical, "Horizontal": mujica.ScrollHorizontal, "Both": mujica.ScrollBoth}
|
||||
layoutControls(c, func() {
|
||||
layoutChoice(c, &s.direction, "Direction", []string{"Vertical", "Horizontal", "Both"}, log)
|
||||
if mujica.Button(c, "Top", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
s.offset.X, s.offset.Y = 0, 0
|
||||
log("scroll to start")
|
||||
}
|
||||
if mujica.Button(c, "Middle", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
s.offset.X, s.offset.Y = s.offset.MaxX/2, s.offset.MaxY/2
|
||||
log("scroll to middle")
|
||||
}
|
||||
if mujica.Button(c, "End", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
s.offset.X, s.offset.Y = math.MaxFloat32, math.MaxFloat32
|
||||
log("scroll to end")
|
||||
}
|
||||
})
|
||||
k := mujica.Tokens(c)
|
||||
ui.Column(c).Margin(16, 0, 0, 0).Width(420).Children(func() {
|
||||
mujica.ScrollArea(c, &s.offset, mujica.ScrollAreaOptions{Direction: dirs[s.direction], Label: "Ledger"}, func() {
|
||||
for i := range 40 {
|
||||
ui.Text(c, fmt.Sprintf("%02d Ledger line of the treasury, folio %d, carried over from the last quarter", i+1, i+7)).
|
||||
SingleLine().Padding(4, 12)
|
||||
}
|
||||
}).Height(220).Background(k.Surface).Border(1, k.Border).Radius(4)
|
||||
})
|
||||
if s.offset.Y != s.lastY || s.offset.X != s.lastX {
|
||||
s.lastX, s.lastY = s.offset.X, s.offset.Y
|
||||
log(fmt.Sprintf("offset %.0f, %.0f", s.offset.X, s.offset.Y))
|
||||
}
|
||||
ui.Text(c, fmt.Sprintf("Offset %.0f, %.0f of %.0f, %.0f", s.offset.X, s.offset.Y, s.offset.MaxX, s.offset.MaxY)).
|
||||
TextColor(k.TextMuted).Margin(8, 0, 0, 0)
|
||||
}
|
||||
|
||||
const layoutSplitPaneCode = `split := mujica.SplitPaneState{Size: 160}
|
||||
mujica.SplitPane(c, &split, mujica.SplitPaneOptions{MinFirst: 120, Collapsible: mujica.SplitPaneFirst},
|
||||
func() { ui.Text(c, "Files") },
|
||||
func() { ui.Text(c, "Editor") },
|
||||
).Fill()`
|
||||
|
||||
func layoutSplitPaneDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
split mujica.SplitPaneState
|
||||
vertical bool
|
||||
collapsible string
|
||||
minFirst float64
|
||||
}
|
||||
s := ui.Local(c.Root(), "008", func() state {
|
||||
return state{split: mujica.SplitPaneState{Size: 180}, collapsible: "First", minFirst: 120}
|
||||
})
|
||||
sides := map[string]mujica.SplitPaneSide{"Neither": mujica.SplitPaneNeither, "First": mujica.SplitPaneFirst, "Second": mujica.SplitPaneSecond}
|
||||
layoutControls(c, func() {
|
||||
layoutCheck(c, &s.vertical, "Vertical", log)
|
||||
layoutChoice(c, &s.collapsible, "Collapsible", []string{"Neither", "First", "Second"}, log)
|
||||
layoutSlider(c, &s.minFirst, "Min first", 40, 200, log)
|
||||
})
|
||||
s.split.Collapsed = s.split.Collapsed && s.collapsible != "Neither"
|
||||
k := mujica.Tokens(c)
|
||||
pane := func(title, body string, bg ui.Color) func() {
|
||||
return func() {
|
||||
ui.Column(c).Fill().Padding(12).Gap(6).Background(bg).Children(func() {
|
||||
ui.Text(c, title).FontSize(16)
|
||||
ui.Text(c, body).TextColor(k.TextMuted)
|
||||
})
|
||||
}
|
||||
}
|
||||
ui.Column(c).Margin(16, 0, 0, 0).Width(560).Children(func() {
|
||||
layoutFrame(c, 280, func() {
|
||||
r := mujica.SplitPane(c, &s.split, mujica.SplitPaneOptions{Vertical: s.vertical, MinFirst: float32(s.minFirst),
|
||||
Collapsible: sides[s.collapsible]},
|
||||
pane("Files", "Drag the divider, or focus it and use the arrows.", k.Surface),
|
||||
pane("Editor", "Enter on the divider folds the collapsible pane.", k.Background))
|
||||
r.Fill()
|
||||
if r.Resized {
|
||||
log(fmt.Sprintf("resized to %.0f", s.split.Size))
|
||||
}
|
||||
if r.Toggled {
|
||||
log(fmt.Sprintf("collapsed: %v", s.split.Collapsed))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const layoutAspectRatioCode = `ui.Box(c).Size(320, 240).Children(func() {
|
||||
mujica.AspectRatio(c, mujica.AspectRatioOptions{Ratio: 16.0 / 9}, func() {
|
||||
ui.Text(c, "16 : 9")
|
||||
})
|
||||
})`
|
||||
|
||||
func layoutAspectRatioDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
ratio string
|
||||
width, height float64
|
||||
}
|
||||
s := ui.Local(c.Root(), "009", func() state { return state{ratio: "16 : 9", width: 360, height: 220} })
|
||||
ratios := map[string]float32{"1 : 1": 1, "4 : 3": 4.0 / 3, "16 : 9": 16.0 / 9, "3 : 4": 3.0 / 4, "21 : 9": 21.0 / 9}
|
||||
layoutControls(c, func() {
|
||||
layoutChoice(c, &s.ratio, "Ratio", []string{"1 : 1", "4 : 3", "16 : 9", "3 : 4", "21 : 9"}, log)
|
||||
layoutSlider(c, &s.width, "Parent width", 120, 560, log)
|
||||
layoutSlider(c, &s.height, "Parent height", 80, 320, log)
|
||||
})
|
||||
k := mujica.Tokens(c)
|
||||
ui.Box(c).Margin(16, 0, 0, 0).Size(float32(s.width), float32(s.height)).Border(1, k.Border).Children(func() {
|
||||
mujica.AspectRatio(c, mujica.AspectRatioOptions{Ratio: ratios[s.ratio]}, func() {
|
||||
ui.Box(c).Fill().Center().Background(k.Surface).Border(1, k.Ornament).Children(func() {
|
||||
ui.Text(c, s.ratio).TextColor(k.TextMuted)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const layoutDividerCode = `ui.Column(c).Padding(16).Gap(12).Children(func() {
|
||||
mujica.Divider(c, mujica.DividerOptions{Ornament: true})
|
||||
mujica.Divider(c, mujica.DividerOptions{Label: "Chapter II"})
|
||||
})`
|
||||
|
||||
func layoutDividerDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
vertical, ornament bool
|
||||
label string
|
||||
}
|
||||
s := ui.Local(c.Root(), "010", func() state { return state{ornament: true, label: "None"} })
|
||||
layoutControls(c, func() {
|
||||
layoutCheck(c, &s.vertical, "Vertical", log)
|
||||
layoutCheck(c, &s.ornament, "Ornament", log)
|
||||
if !s.vertical {
|
||||
layoutChoice(c, &s.label, "Label", []string{"None", "Chapter II", "第二章"}, log)
|
||||
}
|
||||
})
|
||||
label := s.label
|
||||
if label == "None" || s.vertical {
|
||||
label = ""
|
||||
}
|
||||
opts := mujica.DividerOptions{Vertical: s.vertical, Ornament: s.ornament, Label: label}
|
||||
ui.Column(c).Margin(16, 0, 0, 0).Width(480).Children(func() {
|
||||
if s.vertical {
|
||||
ui.Row(c).Height(80).Gap(16).Children(func() {
|
||||
ui.Text(c, "Overture")
|
||||
mujica.Divider(c, opts)
|
||||
ui.Text(c, "Aria")
|
||||
})
|
||||
return
|
||||
}
|
||||
ui.Column(c).Gap(16).Children(func() {
|
||||
ui.Text(c, "Overture")
|
||||
mujica.Divider(c, opts)
|
||||
ui.Text(c, "Aria")
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user