Merge branch 'main' into worktree-agent-ac0c9e7144c57cf07

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 12:26:17 -04:00
commit e538f88d5d
444 files changed
+30007 -38

No files matched your search

+397 -1
View File
@@ -1,3 +1,399 @@
package main
var datetimePages = []page{}
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"mujicaui/theme"
)
const dtCategory = "Date & Time"
var datetimePages = []page{
{ID: "051", Name: "Calendar", ZhName: "日历", Category: dtCategory, Demo: dtCalendarDemo,
Code: dtCalendarCode, Keys: "Tab enters the days; arrows move by day and week; PageUp/PageDown by month, with Shift by year; Home/End go to the first and last day; Enter or Space chooses."},
{ID: "052", Name: "DatePicker", ZhName: "日期选择器", Category: dtCategory, Demo: dtDatePickerDemo,
Code: dtDatePickerCode, Keys: "Type a date and press Enter, or leave the field, to commit; Down opens the calendar; in it arrows, PageUp/PageDown, Enter choose and Escape closes."},
{ID: "053", Name: "DateRangePicker", ZhName: "日期范围选择器", Category: dtCategory, Demo: dtDateRangeDemo,
Code: dtDateRangeCode, Keys: "Enter, Space or Down opens; Enter picks the start, then the end; Tab reaches Clear, Cancel and Confirm; Escape discards the draft."},
{ID: "054", Name: "TimePicker", ZhName: "时间选择器", Category: dtCategory, Demo: dtTimePickerDemo,
Code: dtTimePickerCode, Keys: "Tab enters the segments; Left/Right move between them; Up/Down step; digits type; A/P set the period; Alt+Down or the clock button opens the lists."},
{ID: "055", Name: "TimeRangePicker", ZhName: "时间范围选择器", Category: dtCategory, Demo: dtTimeRangeDemo,
Code: dtTimeRangeCode, Keys: "Each end works as a TimePicker; Tab moves from the start to the end."},
{ID: "056", Name: "DateTimePicker", ZhName: "日期时间选择器", Category: dtCategory, Demo: dtDateTimeDemo,
Code: dtDateTimeCode, Keys: "Enter, Space or Down opens; the calendar and time segments work as in DatePicker and TimePicker; Tab reaches Cancel and Confirm; Escape cancels."},
{ID: "057", Name: "MonthPicker", ZhName: "月份选择器", Category: dtCategory, Demo: dtMonthDemo,
Code: dtMonthCode, Keys: "Enter, Space or Down opens; arrows move by month and row; PageUp/PageDown by year; Enter chooses; Escape closes."},
{ID: "058", Name: "YearPicker", ZhName: "年份选择器", Category: dtCategory, Demo: dtYearDemo,
Code: dtYearCode, Keys: "Enter, Space or Down opens; arrows move by year and row; PageUp/PageDown by decade; Enter chooses; Escape closes."},
}
// dtSection lays out a captioned block of a demo.
func dtSection(c *ui.Context, title string, fn func()) {
k := mujica.Tokens(c)
ui.Column(c).Gap(8).Children(func() {
ui.Text(c, title).Font(theme.SerifFont).FontSize(theme.H3Size).TextColor(k.Text)
fn()
})
}
// dtStates lays labeled variants side by side.
func dtStates(c *ui.Context, fn func(state func(name string, body func()))) {
k := mujica.Tokens(c)
ui.Row(c).Gap(24).Wrap().AlignItems(ui.Start).Children(func() {
fn(func(name string, body func()) {
ui.Column(c).Gap(6).AlignItems(ui.Start).Children(func() {
ui.Text(c, name).FontSize(theme.CaptionSize).TextColor(k.TextMuted)
body()
})
})
})
}
func dtControls(c *ui.Context, fn func()) {
ui.Row(c).Gap(16).Wrap().AlignItems(ui.Center).Children(fn)
}
var dtWeekStarts = []string{"Locale", "Sunday", "Monday", "Saturday"}
func dtWeekStart(s string) mujica.WeekStart {
for i, n := range dtWeekStarts {
if n == s {
return mujica.WeekStart(i)
}
}
panic("gallery: unknown week start " + s)
}
func dtCalendarDemo(c *ui.Context, log func(string)) {
type state struct {
day mujica.Date
weekStart string
weekends, bound, frozen bool
}
s := ui.Local(c.Root(), "051", func() state { return state{day: mujica.DateOf(time.Now()), weekStart: "Locale"} })
dtSection(c, "Calendar", func() {
dtControls(c, func() {
ui.Select(c, &s.weekStart, dtWeekStarts)
ui.Checkbox(c, &s.weekends, "Disable weekends")
ui.Checkbox(c, &s.bound, "Only this month")
ui.Checkbox(c, &s.frozen, "Disabled")
})
opts := mujica.CalendarOptions{WeekStart: dtWeekStart(s.weekStart), Disabled: s.frozen, Label: "Calendar demo"}
if s.weekends {
opts.DisableDate = func(d mujica.Date) bool { return d.Weekday() == time.Saturday || d.Weekday() == time.Sunday }
}
if s.bound {
now := mujica.DateOf(time.Now())
opts.Min = mujica.Date{Year: now.Year, Month: now.Month, Day: 1}
opts.Max = opts.Min.AddMonths(1).AddDays(-1)
}
if mujica.Calendar(c, &s.day, opts).Changed() {
log("Calendar changed: " + s.day.String())
}
})
}
func dtDatePickerDemo(c *ui.Context, log func(string)) {
type state struct {
a, b, d mujica.Date
clearable, slash bool
}
s := ui.Local(c.Root(), "052", func() state {
t := mujica.DateOf(time.Now())
return state{a: t, d: t, clearable: true}
})
dtSection(c, "DatePicker", func() {
dtControls(c, func() {
ui.Checkbox(c, &s.clearable, "Clearable")
ui.Checkbox(c, &s.slash, "Layout 02/01/2006")
})
layout := ""
if s.slash {
layout = "02/01/2006"
}
now := mujica.DateOf(time.Now())
dtStates(c, func(state func(string, func())) {
state("Default", func() {
if mujica.DatePicker(c, &s.a, mujica.DatePickerOptions{Label: "Due date", Layout: layout, Clearable: s.clearable}).Changed() {
log("DatePicker changed: " + s.a.String())
}
})
state("Next 30 days only", func() {
opts := mujica.DatePickerOptions{Label: "Delivery", Layout: layout, Clearable: s.clearable, Min: now, Max: now.AddDays(30)}
if mujica.DatePicker(c, &s.b, opts).Changed() {
log("DatePicker (bounded) changed: " + s.b.String())
}
})
state("Disabled", func() {
mujica.DatePicker(c, &s.d, mujica.DatePickerOptions{Label: "Locked", Layout: layout, Disabled: true})
})
})
})
}
func dtDateRangeDemo(c *ui.Context, log func(string)) {
type state struct {
a mujica.DateRange
noTuesdays bool
frozen bool
fromThisWeek bool
}
s := ui.Local(c.Root(), "053", func() state { return state{} })
dtSection(c, "DateRangePicker", func() {
dtControls(c, func() {
ui.Checkbox(c, &s.noTuesdays, "Tuesdays unavailable")
ui.Checkbox(c, &s.fromThisWeek, "Start from today")
ui.Checkbox(c, &s.frozen, "Disabled")
})
opts := mujica.DateRangePickerOptions{Label: "Stay", Disabled: s.frozen}
if s.noTuesdays {
opts.DisableDate = func(d mujica.Date) bool { return d.Weekday() == time.Tuesday }
}
if s.fromThisWeek {
opts.Min = mujica.DateOf(time.Now())
}
r := mujica.DateRangePicker(c, &s.a, opts)
if r.Changed() {
log(fmt.Sprintf("DateRangePicker changed: %v – %v (%d days)", s.a.Start, s.a.End, s.a.Days()))
}
if r.Cancelled() {
log("DateRangePicker cancelled")
}
})
}
func dtTimePickerDemo(c *ui.Context, log func(string)) {
type state struct {
a, b mujica.TimeOfDay
seconds, hour12 bool
step string
}
s := ui.Local(c.Root(), "054", func() state {
return state{a: mujica.TimeOfDay{Hour: 9, Minute: 30}, b: mujica.TimeOfDay{Hour: 10}, step: "1"}
})
dtSection(c, "TimePicker", func() {
dtControls(c, func() {
ui.Checkbox(c, &s.seconds, "Seconds")
ui.Checkbox(c, &s.hour12, "12-hour")
ui.Text(c, "Minute step")
ui.Select(c, &s.step, []string{"1", "5", "15", "30"})
})
step := 0
if _, err := fmt.Sscan(s.step, &step); err != nil {
panic(err)
}
base := mujica.TimePickerOptions{Seconds: s.seconds, Hour12: s.hour12, MinuteStep: step}
dtStates(c, func(state func(string, func())) {
state("Any time", func() {
o := base
o.Label = "Alarm"
if mujica.TimePicker(c, &s.a, o).Changed() {
log("TimePicker changed: " + s.a.String())
}
})
state("Office hours 09:00–18:00", func() {
o := base
o.Label, o.Min, o.Max = "Meeting", mujica.TimeOfDay{Hour: 9}, mujica.TimeOfDay{Hour: 18}
if mujica.TimePicker(c, &s.b, o).Changed() {
log("TimePicker (bounded) changed: " + s.b.String())
}
})
state("Disabled", func() {
o := base
o.Label, o.Disabled = "Locked", true
mujica.TimePicker(c, &s.a, o)
})
})
})
}
func dtTimeRangeDemo(c *ui.Context, log func(string)) {
type state struct {
v mujica.TimeRange
overnight bool
}
s := ui.Local(c.Root(), "055", func() state {
return state{v: mujica.TimeRange{Start: mujica.TimeOfDay{Hour: 9}, End: mujica.TimeOfDay{Hour: 17}}}
})
dtSection(c, "TimeRangePicker", func() {
dtControls(c, func() {
if ui.Checkbox(c, &s.overnight, "Allow overnight").Changed() && !s.overnight && s.v.Overnight() {
s.v = mujica.TimeRange{Start: s.v.End, End: s.v.Start}
log("TimeRangePicker swapped ends: overnight turned off")
}
})
opts := mujica.TimeRangePickerOptions{Label: "Opening hours", AllowOvernight: s.overnight, MinuteStep: 15}
if mujica.TimeRangePicker(c, &s.v, opts).Changed() {
log(fmt.Sprintf("TimeRangePicker changed: %v – %v overnight=%v", s.v.Start, s.v.End, s.v.Overnight()))
}
})
}
var dtZones = []string{"Local", "UTC", "Asia/Shanghai", "America/New_York", "Europe/London"}
func dtDateTimeDemo(c *ui.Context, log func(string)) {
type state struct {
v time.Time
zone string
hour12 bool
}
s := ui.Local(c.Root(), "056", func() state { return state{zone: "Local"} })
dtSection(c, "DateTimePicker", func() {
dtControls(c, func() {
ui.Text(c, "Time zone")
ui.Select(c, &s.zone, dtZones)
ui.Checkbox(c, &s.hour12, "12-hour")
})
loc, err := time.LoadLocation(s.zone)
if err != nil {
panic(err)
}
r := mujica.DateTimePicker(c, &s.v, mujica.DateTimePickerOptions{Label: "Meeting", Location: loc, Hour12: s.hour12})
if r.Changed() {
log("DateTimePicker changed: " + s.v.Format(time.RFC3339))
}
if r.Cancelled() {
log("DateTimePicker cancelled")
}
k := mujica.Tokens(c)
ui.Text(c, "Try 2026-03-08 02:30 in America/New_York: the picker reports the DST gap.").
FontSize(theme.CaptionSize).TextColor(k.TextMuted)
})
}
func dtMonthDemo(c *ui.Context, log func(string)) {
type state struct {
v mujica.YearMonth
clearable, past bool
}
s := ui.Local(c.Root(), "057", func() state { return state{clearable: true} })
dtSection(c, "MonthPicker", func() {
dtControls(c, func() {
ui.Checkbox(c, &s.clearable, "Clearable")
ui.Checkbox(c, &s.past, "Past months only")
})
opts := mujica.MonthPickerOptions{Label: "Billing month", Clearable: s.clearable,
DisableMonth: func(m mujica.YearMonth) bool { return m.Month == time.February }}
if s.past {
now := time.Now()
opts.Max = mujica.YearMonth{Year: now.Year(), Month: now.Month()}
}
dtStates(c, func(state func(string, func())) {
state("February unavailable", func() {
if mujica.MonthPicker(c, &s.v, opts).Changed() {
log("MonthPicker changed: " + s.v.String())
}
})
state("Disabled", func() {
mujica.MonthPicker(c, &s.v, mujica.MonthPickerOptions{Label: "Locked", Disabled: true})
})
})
})
}
func dtYearDemo(c *ui.Context, log func(string)) {
type state struct {
v int
clearable bool
}
s := ui.Local(c.Root(), "058", func() state { return state{v: time.Now().Year(), clearable: true} })
dtSection(c, "YearPicker", func() {
dtControls(c, func() { ui.Checkbox(c, &s.clearable, "Clearable") })
opts := mujica.YearPickerOptions{Label: "Vintage", Min: 1990, Max: time.Now().Year(), Clearable: s.clearable,
DisableYear: func(y int) bool { return y == 2020 }}
dtStates(c, func(state func(string, func())) {
state("1990 to now, 2020 unavailable", func() {
if mujica.YearPicker(c, &s.v, opts).Changed() {
log(fmt.Sprintf("YearPicker changed: %d", s.v))
}
})
state("Disabled", func() {
mujica.YearPicker(c, &s.v, mujica.YearPickerOptions{Label: "Locked", Disabled: true})
})
})
})
}
// Code snippets mirror example_datetime_test.go.
const (
dtCalendarCode = `day := mujica.Date{Year: 2026, Month: time.October, Day: 5}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.Calendar(c, &day, mujica.CalendarOptions{WeekStart: mujica.WeekStartMonday}).Changed() {
fmt.Println("chosen", day)
}
}
ui.Render(view, 400, 300, 1)`
dtDatePickerCode = `due := mujica.Date{Year: 2026, Month: time.October, Day: 5}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
opts := mujica.DatePickerOptions{Label: "Due date", Clearable: true,
Min: mujica.Date{Year: 2026, Month: time.January, Day: 1}}
if mujica.DatePicker(c, &due, opts).Changed() {
fmt.Println("due", due)
}
}
ui.Render(view, 400, 300, 1)`
dtDateRangeCode = `var stay mujica.DateRange
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.DateRangePicker(c, &stay, mujica.DateRangePickerOptions{Label: "Stay"})
if r.Changed() {
fmt.Println("stay", stay.Start, stay.End, stay.Days())
}
}
ui.Render(view, 400, 300, 1)`
dtTimePickerCode = `alarm := mujica.TimeOfDay{Hour: 7, Minute: 30}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
opts := mujica.TimePickerOptions{Label: "Alarm", Hour12: true, MinuteStep: 5}
if mujica.TimePicker(c, &alarm, opts).Changed() {
fmt.Println("alarm", alarm)
}
}
ui.Render(view, 400, 300, 1)`
dtTimeRangeCode = `shift := mujica.TimeRange{Start: mujica.TimeOfDay{Hour: 22}, End: mujica.TimeOfDay{Hour: 6}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
opts := mujica.TimeRangePickerOptions{Label: "Night shift", AllowOvernight: true}
if mujica.TimeRangePicker(c, &shift, opts).Changed() {
fmt.Println("shift", shift.Start, shift.End, shift.Overnight())
}
}
ui.Render(view, 400, 300, 1)`
dtDateTimeCode = `tokyo, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
panic(err)
}
var meeting time.Time
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
opts := mujica.DateTimePickerOptions{Label: "Meeting", Location: tokyo, MinuteStep: 15}
if mujica.DateTimePicker(c, &meeting, opts).Changed() {
fmt.Println("meeting", meeting)
}
}
ui.Render(view, 400, 300, 1)`
dtMonthCode = `var month mujica.YearMonth
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
opts := mujica.MonthPickerOptions{Label: "Billing month", Clearable: true}
if mujica.MonthPicker(c, &month, opts).Changed() {
fmt.Println("month", month)
}
}
ui.Render(view, 400, 300, 1)`
dtYearCode = `year := 2026
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
opts := mujica.YearPickerOptions{Label: "Vintage", Min: 1990, Max: 2030}
if mujica.YearPicker(c, &year, opts).Changed() {
fmt.Println("year", year)
}
}
ui.Render(view, 400, 300, 1)`
)
+493 -1
View File
@@ -1,3 +1,495 @@
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
}
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")
})`
+32
View File
@@ -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)
})
}
+111 -1
View File
@@ -1,3 +1,113 @@
package main
var feedbackPages = []page{}
var feedbackPages = []page{
{
ID: "091", Name: "Alert", ZhName: "提示条 警告 通知横幅", Category: "Feedback",
Demo: alertDemo,
Code: `open := true
r := mujica.Alert(c, &open, "The archive could not be read.", mujica.AlertOptions{
Severity: mujica.SeverityError,
Title: "Import failed",
Action: "Retry",
})
if r.Action {
// retry the import
}`,
Keys: "Tab moves to the action and the dismiss button; Enter or Space presses them.",
},
{
ID: "092", Name: "Toast", ZhName: "轻提示 消息队列 吐司", Category: "Feedback",
Demo: toastDemo,
Code: `if mujica.Button(c, "Delete note", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() {
mujica.ShowToast(c, mujica.ToastMessage{ID: "note-1", Message: "Note deleted", Action: "Undo"})
}
// Last in the view:
if r := mujica.Toaster(c); r.Action == "note-1" {
// restore the note
}`,
Keys: "Toasts never take the focus. Tab reaches their buttons after the page; Escape closes the focused toast. Hover or focus pauses its timer.",
},
{
ID: "093", Name: "Progress", ZhName: "进度条 环形进度 计量条", Category: "Feedback",
Demo: progressDemo,
Code: `mujica.Progress(c, 0.62, mujica.ProgressOptions{Label: "Uploading", ShowValue: true})
mujica.Progress(c, 0, mujica.ProgressOptions{Shape: mujica.ProgressRing, Indeterminate: true, Label: "Connecting"})
mujica.Meter(c, 93, mujica.MeterOptions{
Max: 100, Label: "Disk", ShowValue: true,
Levels: &ui.MeterLevels{Warning: 70, Critical: 90},
})`,
Keys: "Not focusable; assistive technology reads its name and value.",
},
{
ID: "094", Name: "Spinner", ZhName: "加载标记 加载中 转圈", Category: "Feedback",
Demo: spinnerDemo,
Code: `mujica.Spinner(c, mujica.SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: true})`,
Keys: "Not focusable; read as an indeterminate progress indicator named by Label.",
},
{
ID: "095", Name: "Skeleton", ZhName: "骨架屏 占位", Category: "Feedback",
Demo: skeletonDemo,
Code: `ui.Row(c).Gap(12).Children(func() {
mujica.Skeleton(c, mujica.SkeletonOptions{Shape: mujica.SkeletonCircle})
ui.Column(c).Grow(1).Children(func() {
mujica.Skeleton(c, mujica.SkeletonOptions{Lines: 2})
})
})`,
Keys: "Decorative; not focusable and hidden from assistive technology.",
},
{
ID: "096", Name: "EmptyState", ZhName: "空状态 无数据 无搜索结果", Category: "Feedback",
Demo: emptyStateDemo,
Code: `r := mujica.EmptyState(c, mujica.EmptyStateOptions{
Kind: mujica.EmptyNoResults,
Query: query,
Action: "Clear search",
})
if r.Action {
query = ""
}`,
Keys: "Tab reaches the action button; Enter or Space presses it.",
},
{
ID: "097", Name: "Result", ZhName: "结果页 成功 失败 无权限", Category: "Feedback",
Demo: resultDemo,
Code: `r := mujica.Result(c, mujica.ResultOptions{
Kind: mujica.ResultForbidden,
Description: "You need the Editor role to change billing.",
Actions: []string{"Request access", "Go back"},
})
switch r.Action {
case 0: // send the request
case 1: // navigate back
}`,
Keys: "Tab moves through the actions, primary first; Enter or Space presses one.",
},
{
ID: "098", Name: "StatusIndicator", ZhName: "状态标记 在线 离线", Category: "Feedback",
Demo: statusDemo,
Code: `mujica.StatusIndicator(c, mujica.StatusSuccess, mujica.StatusIndicatorOptions{Label: "API online"})
mujica.StatusIndicator(c, mujica.StatusOffline, mujica.StatusIndicatorOptions{})`,
Keys: "Not focusable; read as a status with its word.",
},
{
ID: "099", Name: "NotificationCenter", ZhName: "通知中心 消息列表", Category: "Feedback",
Demo: notificationDemo,
Code: `r := mujica.NotificationCenter(c, &items, mujica.NotificationCenterOptions{})
if r.Action == "a" {
// open the build log
}`,
Keys: "The list is one Tab stop. Arrows move among notifications and their buttons; Enter marks the focused one read; Delete or Backspace removes it.",
},
{
ID: "100", Name: "LoadingOverlay", ZhName: "加载遮罩 区域加载", Category: "Feedback",
Demo: loadingOverlayDemo,
Code: `r := mujica.LoadingOverlay(c, loading, mujica.LoadingOverlayOptions{Cancel: "Cancel"}, func() {
ui.Text(c, "Quarterly report")
mujica.Button(c, "Export", mujica.ButtonOptions{})
})
if r.Canceled {
loading = false
}`,
Keys: "While loading, the covered region takes no input; focus inside moves to Cancel, and Escape there cancels. The rest of the window stays usable.",
},
}
+401
View File
@@ -0,0 +1,401 @@
package main
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"mujicaui/theme"
)
// feedbackSection titles a part of a demo.
func feedbackSection(c *ui.Context, title string, fn func()) {
ui.Column(c).Gap(8).FillWidth().Children(func() {
ui.Text(c, title).FontSize(theme.CaptionSize).TextColor(mujica.Tokens(c).TextMuted)
fn()
})
}
var feedbackSeverities = []mujica.Severity{mujica.SeverityInfo, mujica.SeveritySuccess, mujica.SeverityWarning, mujica.SeverityError}
func alertDemo(c *ui.Context, log func(string)) {
type state struct {
sev int
title, action bool
closable bool
open bool
}
s := ui.Local(c.Root(), "091", func() state { return state{sev: 3, title: true, action: true, closable: true, open: true} })
ui.Column(c).Gap(16).FillWidth().Children(func() {
feedbackSection(c, "Parameters", func() {
ui.Segmented(c, &s.sev, "Info", "Success", "Warning", "Error")
ui.Row(c).Gap(16).Children(func() {
ui.Checkbox(c, &s.title, "Title")
ui.Checkbox(c, &s.action, "Action")
ui.Checkbox(c, &s.closable, "Closable")
})
})
feedbackSection(c, "Live", func() {
opts := mujica.AlertOptions{Severity: feedbackSeverities[s.sev]}
if s.title {
opts.Title = "Import failed"
}
if s.action {
opts.Action = "Retry"
}
var open *bool
if s.closable {
open = &s.open
}
r := mujica.Alert(c, open, "The archive could not be read.", opts)
if r.Action {
log("alert: action Retry")
}
if r.Closed {
log("alert: closed")
}
if s.closable && !s.open && mujica.Button(c, "Show again", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.open = true
log("alert: reopened")
}
})
feedbackSection(c, "Severities", func() {
for _, sev := range feedbackSeverities {
mujica.Alert(c, nil, "Severity "+sev.String()+" uses its own icon, bar and name.", mujica.AlertOptions{Severity: sev}).Element.Key(sev)
}
})
})
}
func toastDemo(c *ui.Context, log func(string)) {
type state struct {
sev int
action bool
seconds float64
n int
note string
}
s := ui.Local(c.Root(), "092", func() state { return state{seconds: 5} })
ui.Column(c).Gap(16).FillWidth().Children(func() {
feedbackSection(c, "Parameters", func() {
ui.Segmented(c, &s.sev, "Info", "Success", "Warning", "Error")
ui.Checkbox(c, &s.action, "Undo action")
ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() {
ui.Text(c, fmt.Sprintf("Duration %.0f s (errors stay until closed)", s.seconds))
ui.Slider(c, &s.seconds, 1, 10).Width(200)
})
})
feedbackSection(c, "Actions", func() {
ui.Row(c).Gap(12).Wrap().Children(func() {
if mujica.Button(c, "Show toast", mujica.ButtonOptions{}).Clicked() {
s.n++
m := mujica.ToastMessage{ID: fmt.Sprint(s.n), Severity: feedbackSeverities[s.sev], Message: fmt.Sprintf("Message %d", s.n)}
if s.action {
m.Action = "Undo"
}
if m.Severity != mujica.SeverityError {
m.Duration = time.Duration(s.seconds * float64(time.Second))
}
mujica.ShowToast(c, m)
log("toast: show " + m.ID)
}
if mujica.Button(c, "Show five at once", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
for range 5 {
s.n++
mujica.ShowToast(c, mujica.ToastMessage{ID: fmt.Sprint(s.n), Message: fmt.Sprintf("Queued %d", s.n)})
}
log("toast: five queued, three visible")
}
if mujica.Button(c, "Repeat same ID", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.n++
mujica.ShowToast(c, mujica.ToastMessage{ID: "sync", Message: fmt.Sprintf("Synced %d times", s.n)})
log("toast: dedupe id sync")
}
})
ui.Text(c, "Type here while toasts appear; the focus stays:")
ui.TextInput(c, &s.note).Width(320)
})
})
if r := mujica.Toaster(c); r.Action != "" {
log("toast: action " + r.Action)
} else if r.Closed != "" {
log("toast: closed " + r.Closed)
}
}
func progressDemo(c *ui.Context, log func(string)) {
type state struct {
value, meter float64
indet, showValue bool
ring int
}
s := ui.Local(c.Root(), "093", func() state { return state{value: 0.62, meter: 72, showValue: true} })
ui.Column(c).Gap(16).FillWidth().Children(func() {
feedbackSection(c, "Parameters", func() {
ui.Segmented(c, &s.ring, "Linear", "Ring")
ui.Row(c).Gap(16).Children(func() {
if ui.Checkbox(c, &s.indet, "Indeterminate").Changed() {
log(fmt.Sprintf("progress: indeterminate %v", s.indet))
}
ui.Checkbox(c, &s.showValue, "Show value")
})
ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() {
ui.Text(c, "Value")
if ui.Slider(c, &s.value, 0, 1).Width(240).Changed() {
log(fmt.Sprintf("progress: value %.2f", s.value))
}
})
})
feedbackSection(c, "Progress", func() {
mujica.Progress(c, s.value, mujica.ProgressOptions{
Shape: mujica.ProgressShape(s.ring), Indeterminate: s.indet, Label: "Uploading", ShowValue: s.showValue,
})
})
feedbackSection(c, "Meter with thresholds 70 and 90", func() {
ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() {
ui.Text(c, "Level")
if ui.Slider(c, &s.meter, 0, 100).Width(240).Changed() {
log(fmt.Sprintf("meter: value %.0f", s.meter))
}
})
v := float64(int(s.meter))
mujica.Meter(c, v, mujica.MeterOptions{Max: 100, Label: "Disk", ShowValue: true, Levels: &ui.MeterLevels{Warning: 70, Critical: 90}})
mujica.Meter(c, 100-v, mujica.MeterOptions{Max: 100, Label: "Battery (low is bad)", ShowValue: true, Levels: &ui.MeterLevels{Warning: 30, Critical: 10}})
})
})
}
func spinnerDemo(c *ui.Context, log func(string)) {
type state struct {
size float64
showLabel, active bool
}
s := ui.Local(c.Root(), "094", func() state { return state{size: 20, showLabel: true, active: true} })
ui.Column(c).Gap(16).FillWidth().Children(func() {
feedbackSection(c, "Parameters", func() {
ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() {
ui.Text(c, fmt.Sprintf("Size %.0f", s.size))
ui.Slider(c, &s.size, 12, 48).Width(200)
})
ui.Row(c).Gap(16).Children(func() {
ui.Checkbox(c, &s.showLabel, "Show label")
if ui.Checkbox(c, &s.active, "Loading (unchecked stops the animation)").Changed() {
log(fmt.Sprintf("spinner: loading %v", s.active))
}
})
})
feedbackSection(c, "Live", func() {
if s.active {
mujica.Spinner(c, mujica.SpinnerOptions{Size: float32(s.size), Label: "Syncing", ShowLabel: s.showLabel})
} else {
ui.Text(c, "Done")
}
})
feedbackSection(c, "In a button", func() {
mujica.Button(c, "Saving", mujica.ButtonOptions{Loading: true})
})
})
}
func skeletonDemo(c *ui.Context, log func(string)) {
type state struct {
lines float64
loading bool
}
s := ui.Local(c.Root(), "095", func() state { return state{lines: 3, loading: true} })
ui.Column(c).Gap(16).FillWidth().Children(func() {
feedbackSection(c, "Parameters", func() {
ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() {
ui.Text(c, fmt.Sprintf("Lines %.0f", s.lines))
ui.Slider(c, &s.lines, 1, 6).Width(200)
})
if ui.Checkbox(c, &s.loading, "Loading").Changed() {
log(fmt.Sprintf("skeleton: loading %v", s.loading))
}
})
feedbackSection(c, "Card (same size loading and loaded)", func() {
ui.Row(c).Gap(12).Padding(16).Width(420).Background(mujica.Tokens(c).Surface).Radius(theme.CardRadius).Children(func() {
if s.loading {
mujica.Skeleton(c, mujica.SkeletonOptions{Shape: mujica.SkeletonCircle})
ui.Column(c).Grow(1).Children(func() {
mujica.Skeleton(c, mujica.SkeletonOptions{Lines: int(s.lines)})
})
return
}
ui.Box(c).Size(40, 40).Radius(20).Background(mujica.Tokens(c).Selection)
ui.Column(c).Grow(1).Children(func() {
for i := range int(s.lines) {
ui.Text(c, fmt.Sprintf("Loaded line %d", i+1)).LineHeight(theme.LineHeight)
}
})
})
})
feedbackSection(c, "Rect", func() {
mujica.Skeleton(c, mujica.SkeletonOptions{Shape: mujica.SkeletonRect, Width: 420, Height: 96})
})
})
}
func emptyStateDemo(c *ui.Context, log func(string)) {
type state struct {
kind int
query string
icon, action bool
}
s := ui.Local(c.Root(), "096", func() state { return state{kind: 1, query: "quarterly", icon: true, action: true} })
ui.Column(c).Gap(16).FillWidth().Children(func() {
feedbackSection(c, "Parameters", func() {
ui.Segmented(c, &s.kind, "No data", "No results")
ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() {
ui.Checkbox(c, &s.icon, "Icon")
ui.Checkbox(c, &s.action, "Action")
if s.kind == 1 {
ui.TextInput(c, &s.query).Width(200)
}
})
})
feedbackSection(c, "Live", func() {
opts := mujica.EmptyStateOptions{Kind: mujica.EmptyKind(s.kind), NoIcon: !s.icon}
if s.kind == 1 {
opts.Query = s.query
}
if s.action {
opts.Action = []string{"New project", "Clear search"}[s.kind]
}
if mujica.EmptyState(c, opts).Action {
log("empty state: action " + opts.Action)
s.query = ""
}
})
})
}
func resultDemo(c *ui.Context, log func(string)) {
type state struct{ kind int }
s := ui.Local(c.Root(), "097", func() state { return state{kind: 2} })
descs := []string{"Your order has been placed.", "The payment was declined by the bank.", "You need the Editor role to change billing."}
actions := [][]string{{"View order", "Keep shopping"}, {"Try again", "Back to cart"}, {"Request access", "Go back"}}
ui.Column(c).Gap(16).FillWidth().Children(func() {
feedbackSection(c, "Parameters", func() {
ui.Segmented(c, &s.kind, "Success", "Failure", "Forbidden")
})
r := mujica.Result(c, mujica.ResultOptions{Kind: mujica.ResultKind(s.kind), Description: descs[s.kind], Actions: actions[s.kind]})
if r.Action >= 0 {
log("result: action " + actions[s.kind][r.Action])
}
})
}
func statusDemo(c *ui.Context, log func(string)) {
type state struct {
status int
custom bool
}
s := ui.Local(c.Root(), "098", func() state { return state{} })
ui.Column(c).Gap(16).FillWidth().Children(func() {
feedbackSection(c, "Parameters", func() {
if ui.Segmented(c, &s.status, "Success", "Warning", "Error", "Offline", "Busy").Changed() {
log("status: " + mujica.Status(s.status).String())
}
ui.Checkbox(c, &s.custom, "Custom label")
})
feedbackSection(c, "Live", func() {
opts := mujica.StatusIndicatorOptions{}
if s.custom {
opts.Label = "Database replica"
}
mujica.StatusIndicator(c, mujica.Status(s.status), opts)
})
feedbackSection(c, "All states (shape and word, not color alone)", func() {
ui.Row(c).Gap(20).Wrap().Children(func() {
for st := mujica.StatusSuccess; st <= mujica.StatusBusy; st++ {
mujica.StatusIndicator(c, st, mujica.StatusIndicatorOptions{}).Key(st)
}
})
})
})
}
func notificationDemo(c *ui.Context, log func(string)) {
type state struct {
items []mujica.Notification
n int
}
s := ui.Local(c.Root(), "099", func() state {
return state{items: []mujica.Notification{
{ID: "a", Title: "Build failed", Body: "main · 3 tests failing", Time: "2 min ago", Action: "View log"},
{ID: "b", Title: "Invite accepted", Body: "Mira joined the workspace", Time: "1 h ago"},
{ID: "c", Title: "Backup complete", Time: "Yesterday", Read: true},
}}
})
ui.Column(c).Gap(16).FillWidth().Children(func() {
feedbackSection(c, "Caller-owned list", func() {
if mujica.Button(c, "Add notification", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.n++
id := fmt.Sprintf("new-%d", s.n)
s.items = append([]mujica.Notification{{ID: id, Title: fmt.Sprintf("Comment %d on the design doc", s.n), Time: "now"}}, s.items...)
log("notifications: added " + id)
}
})
ui.Column(c).Width(520).Height(420).Children(func() {
r := mujica.NotificationCenter(c, &s.items, mujica.NotificationCenterOptions{})
for _, ev := range [][2]string{{"action", r.Action}, {"toggled", r.Toggled}, {"opened", r.Opened}, {"removed", r.Removed}} {
if ev[1] != "" {
log("notifications: " + ev[0] + " " + ev[1])
}
}
if r.MarkedAllRead {
log("notifications: marked all read")
}
if r.ClearedAll {
log("notifications: cleared all")
}
})
})
}
func loadingOverlayDemo(c *ui.Context, log func(string)) {
type state struct {
loading, cancel bool
saves int
}
s := ui.Local(c.Root(), "100", func() state { return state{loading: true, cancel: true} })
ui.Column(c).Gap(16).FillWidth().Children(func() {
feedbackSection(c, "Parameters", func() {
ui.Row(c).Gap(16).Children(func() {
if ui.Checkbox(c, &s.loading, "Loading").Changed() {
log(fmt.Sprintf("overlay: loading %v", s.loading))
}
ui.Checkbox(c, &s.cancel, "Cancel action")
})
})
feedbackSection(c, "Covered region", func() {
opts := mujica.LoadingOverlayOptions{Label: "Generating report…"}
if s.cancel {
opts.Cancel = "Cancel"
}
r := mujica.LoadingOverlay(c, s.loading, opts, func() {
ui.Column(c).Gap(8).Padding(16).FillWidth().AlignItems(ui.Start).Background(mujica.Tokens(c).Surface).Radius(theme.CardRadius).Children(func() {
ui.Text(c, "Quarterly report").Font(theme.SerifFont).FontSize(theme.H3Size)
ui.Text(c, fmt.Sprintf("Exported %d times.", s.saves))
if mujica.Button(c, "Export", mujica.ButtonOptions{}).Clicked() {
s.saves++
log("overlay: export clicked")
}
})
})
r.Element.Width(420)
if r.Canceled {
s.loading = false
log("overlay: canceled")
}
})
feedbackSection(c, "Outside the region (always usable)", func() {
if mujica.Button(c, "Outside button", mujica.ButtonOptions{Variant: mujica.Secondary}).AlignSelf(ui.Start).Clicked() {
log("overlay: outside clicked")
}
})
})
}
+414 -1
View File
@@ -1,3 +1,416 @@
package main
var formPages = []page{}
import (
"fmt"
"strings"
"time"
"github.com/egoist/mygo"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"mujicaui/icons"
)
// formStates shows a component's states side by side.
func formStates(c *ui.Context, fn func()) {
ui.Column(c).Gap(8).Children(func() {
ui.Text(c, "States").FontWeight(600)
ui.Row(c).Gap(24).Wrap().AlignItems(ui.Start).Children(fn)
})
}
// formToggles shows parameter check boxes in one row.
func formToggles(c *ui.Context, names []string, values ...*bool) {
ui.Row(c).Gap(16).Wrap().Children(func() {
for i, n := range names {
ui.Checkbox(c, values[i], n)
}
})
}
type formDemo struct {
st mujica.FormState
name, email string
tags []string
horizontal, slowServer bool
}
type fieldDemo struct {
value string
required, horizontal bool
withError bool
}
type groupDemo struct {
value string
disabled, readOnly, invalid bool
}
type pinDemo struct {
code string
length4, disabled bool
invalid bool
}
type tagsDemo struct {
tags []string
limit, disabled bool
}
type ratingDemo struct {
value float64
half, readOnly, disabled bool
}
type colorDemo struct {
color ui.Color
disabled bool
}
type pickerDemo struct {
paths []string
directory, multiple, filter bool
disabled bool
}
type dropDemo struct {
files []mujica.DroppedFile
images bool
disabled bool
}
type transferDemo struct {
chosen []string
disabled bool
}
var transferDemoItems = []mujica.TransferItem{
{ID: "sakiko", Label: "Sakiko Togawa"}, {ID: "uika", Label: "Uika Misumi"}, {ID: "mutsumi", Label: "Mutsumi Wakaba"},
{ID: "umiri", Label: "Umiri Yahata"}, {ID: "nyamu", Label: "Nyamu Yutenji"}, {ID: "tomori", Label: "Tomori Takamatsu", Disabled: true},
{ID: "anon", Label: "Anon Chihaya"}, {ID: "rana", Label: "Rana Kaname"}, {ID: "soyo", Label: "Soyo Nagasaki"},
}
type formAsync struct {
version uint64
err string
}
var formPages = []page{
{ID: "041", Name: "Form", ZhName: "表单", Category: "Forms", Code: formCode["Form"],
Keys: "Tab moves between fields; leaving a field validates it. Enter in a field submits. A failed submit focuses the first error.",
Demo: func(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "041", func() formDemo {
return formDemo{name: "Sakiko", tags: []string{"keyboard"}}
})
async := *ui.Local(c.Root(), "041.async", func() chan formAsync { return make(chan formAsync, 8) })
saved := *ui.Local(c.Root(), "041.save", func() chan string { return make(chan string, 1) })
for drained := false; !drained; {
select {
case r := <-async:
log(fmt.Sprintf("async name check v%d: %q, kept %v", r.version, r.err, d.st.Resolve("name", r.version, r.err)))
case e := <-saved:
d.st.EndSubmit(e)
log(fmt.Sprintf("submit ended, error %q", e))
default:
drained = true
}
}
formToggles(c, []string{"Labels beside fields", "Server rejects the submit"}, &d.horizontal, &d.slowServer)
res := mujica.Form(c, &d.st, mujica.FormOptions{Horizontal: d.horizontal}, func(f *mujica.FormScope) {
mujica.FormItem(f, "name", &d.name, mujica.FormItemOptions[string]{Label: "Stage name", Required: true, Initial: "Sakiko",
Description: "Checked against the archive; \"Oblivionis\" is taken.",
Async: func(v string, version uint64) {
log(fmt.Sprintf("async check started v%d", version))
go func() {
time.Sleep(700 * time.Millisecond)
msg := ""
if strings.EqualFold(v, "Oblivionis") {
msg = "That name is taken."
}
async <- formAsync{version, msg}
c.Invalidate()
}()
}}, func() *ui.Element {
return mujica.InputGroup(c, &d.name, mujica.InputGroupOptions{Invalid: d.st.Error("name") != ""}).Input
})
mujica.FormItem(f, "email", &d.email, mujica.FormItemOptions[string]{Label: "Email", Required: true,
Validate: func(v string) string {
if !strings.Contains(v, "@") {
return "Enter an email address."
}
return ""
}}, func() *ui.Element {
g := mujica.InputGroup(c, &d.email, mujica.InputGroupOptions{Invalid: d.st.Error("email") != ""})
if g.Submitted() {
d.st.Submit()
}
return g.Input
})
mujica.FormItem(f, "tags", &d.tags, mujica.FormItemOptions[[]string]{Label: "Roles", Required: true, Initial: []string{"keyboard"}}, func() *ui.Element {
return mujica.TagsInput(c, &d.tags, mujica.TagsInputOptions{Max: 4, Invalid: d.st.Error("tags") != ""}).Input
})
})
if res.Submitted() {
log(fmt.Sprintf("submitted %q %q %q", d.name, d.email, d.tags))
fail := ""
if d.slowServer {
fail = "The archive refused the entry. Your values are kept."
}
go func() {
time.Sleep(900 * time.Millisecond)
saved <- fail
c.Invalidate()
}()
}
if res.Reset() {
log("reset to initial values")
}
}},
{ID: "042", Name: "FormField", ZhName: "表单字段", Category: "Forms", Code: formCode["FormField"],
Keys: "Clicking the label focuses the control; Tab reaches the control directly.",
Demo: func(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "042", func() fieldDemo { return fieldDemo{} })
formToggles(c, []string{"Required", "Label beside", "Show error"}, &d.required, &d.horizontal, &d.withError)
e := ""
if d.withError {
e = "This name is already in the guest list."
}
mujica.FormField(c, "Guest name", mujica.FormFieldOptions{Required: d.required, Horizontal: d.horizontal,
Description: "As written on the invitation.", Error: e}, func() {
if mujica.InputGroup(c, &d.value, mujica.InputGroupOptions{Invalid: d.withError}).Changed() {
log(fmt.Sprintf("value %q", d.value))
}
})
}},
{ID: "043", Name: "InputGroup", ZhName: "输入组合", Category: "Forms", Code: formCode["InputGroup"],
Keys: "Each button and the input are separate Tab stops. Enter in the input submits.",
Demo: func(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "043", func() groupDemo { return groupDemo{value: "mujica"} })
formToggles(c, []string{"Disabled", "Read-only", "Invalid"}, &d.disabled, &d.readOnly, &d.invalid)
opts := mujica.InputGroupOptions{Prefix: "https://", Suffix: ".org", Label: "Site", Disabled: d.disabled, ReadOnly: d.readOnly, Invalid: d.invalid,
Buttons: []mujica.InputGroupButton{{Label: "Clear", Icon: icons.Must("x"), IconOnly: true}, {Label: "Copy", Icon: icons.Must("copy")}}}
g := mujica.InputGroup(c, &d.value, opts)
switch g.Clicked {
case 0:
d.value = ""
log("clear")
case 1:
c.WriteClipboard("https://" + d.value + ".org")
log("copied")
}
if g.Changed() {
log(fmt.Sprintf("value %q", d.value))
}
if g.Submitted() {
log("submitted")
}
formStates(c, func() {
for _, o := range []mujica.InputGroupOptions{{Label: "Disabled", Disabled: true}, {Label: "Read-only", ReadOnly: true}, {Label: "Invalid", Invalid: true}} {
v := o.Label
mujica.InputGroup(c, &v, o)
}
})
}},
{ID: "044", Name: "PinInput", ZhName: "验证码输入", Category: "Forms", Code: formCode["PinInput"],
Keys: "Type digits to fill cells, Backspace clears the last, Cmd/Ctrl+V pastes the whole code.",
Demo: func(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "044", func() pinDemo { return pinDemo{} })
formToggles(c, []string{"4 digits", "Disabled", "Invalid"}, &d.length4, &d.disabled, &d.invalid)
n := 6
if d.length4 {
n = 4
}
if len(d.code) > n {
d.code = d.code[:n]
}
p := mujica.PinInput(c, &d.code, mujica.PinInputOptions{Length: n, Disabled: d.disabled, Invalid: d.invalid})
if p.Changed() {
log(fmt.Sprintf("code %q", d.code))
}
if p.Completed() {
log(fmt.Sprintf("completed %q", d.code))
}
formStates(c, func() {
a, b := "12", "0420"
mujica.PinInput(c, &a, mujica.PinInputOptions{Length: 4, Disabled: true, Label: "Disabled code"})
mujica.PinInput(c, &b, mujica.PinInputOptions{Length: 4, Invalid: true, Label: "Invalid code"})
})
}},
{ID: "045", Name: "TagsInput", ZhName: "标签输入", Category: "Forms", Code: formCode["TagsInput"],
Keys: "Enter or a comma adds a tag; Backspace in the empty input removes the last; Tab reaches each remove button.",
Demo: func(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "045", func() tagsDemo { return tagsDemo{tags: []string{"rose", "thorn"}} })
formToggles(c, []string{"At most 5", "Disabled"}, &d.limit, &d.disabled)
opts := mujica.TagsInputOptions{Placeholder: "Add a flower", Label: "Flowers", Disabled: d.disabled,
Validate: func(s string) string {
if len([]rune(s)) > 16 {
return "Keep tags under 16 characters."
}
return ""
}}
if d.limit {
opts.Max = 5
}
if mujica.TagsInput(c, &d.tags, opts).Changed() {
log(fmt.Sprintf("tags %q", d.tags))
}
}},
{ID: "046", Name: "Rating", ZhName: "评分", Category: "Forms", Code: formCode["Rating"],
Keys: "Arrows change by one step, Home clears, End fills.",
Demo: func(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "046", func() ratingDemo { return ratingDemo{value: 3, half: true} })
formToggles(c, []string{"Half stars", "Read-only", "Disabled"}, &d.half, &d.readOnly, &d.disabled)
step := 1.0
if d.half {
step = 0.5
}
if !d.half {
d.value = float64(int(d.value + 0.5))
}
if mujica.Rating(c, &d.value, mujica.RatingOptions{Step: step, ReadOnly: d.readOnly, Disabled: d.disabled, ShowValue: true}).Changed() {
log(fmt.Sprintf("rating %v", d.value))
}
formStates(c, func() {
a, b := 4.5, 2.0
mujica.Rating(c, &a, mujica.RatingOptions{Step: 0.5, ReadOnly: true, Label: "Read-only rating"})
mujica.Rating(c, &b, mujica.RatingOptions{Disabled: true, Label: "Disabled rating"})
})
}},
{ID: "047", Name: "ColorPicker", ZhName: "颜色选择器", Category: "Forms", Code: formCode["ColorPicker"],
Keys: "Enter or Space opens; arrows move in the square and sliders; Escape closes and returns focus.",
Demo: func(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "047", func() colorDemo { return colorDemo{color: ui.Hex("#7D2034")} })
formToggles(c, []string{"Disabled"}, &d.disabled)
if mujica.ColorPicker(c, &d.color, mujica.ColorPickerOptions{Label: "Banner color", Disabled: d.disabled}).Changed() {
log(fmt.Sprintf("color %v", d.color))
}
}},
{ID: "048", Name: "FilePicker", ZhName: "文件选择", Category: "Forms", Code: formCode["FilePicker"],
Keys: "Enter or Space opens the system dialog; the dialog's own keys apply.",
Demo: func(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "048", func() pickerDemo { return pickerDemo{multiple: true} })
formToggles(c, []string{"Folders", "Multiple", "PDF and MIDI only", "Disabled"}, &d.directory, &d.multiple, &d.filter, &d.disabled)
opts := mujica.FilePickerOptions{Directory: d.directory, Multiple: d.multiple, Disabled: d.disabled, Title: "Choose scores"}
if d.filter {
opts.Filters = []mygo.FileFilter{{Name: "Scores", Extensions: []string{"pdf", "mid"}}}
}
p := mujica.FilePicker(c, &d.paths, opts)
if p.Changed() {
log(fmt.Sprintf("picked %d: %q", len(d.paths), d.paths))
}
if p.Canceled() {
log("canceled (not an error)")
}
}},
{ID: "049", Name: "FileDropZone", ZhName: "文件拖放区", Category: "Forms", Code: formCode["FileDropZone"],
Keys: "Tab reaches Browse… and each remove button; dropping needs a pointer, Browse is the keyboard path.",
Demo: func(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "049", func() dropDemo { return dropDemo{} })
formToggles(c, []string{"Images only", "Disabled"}, &d.images, &d.disabled)
opts := mujica.FileDropZoneOptions{MaxFiles: 4, MaxSize: 10 << 20, Disabled: d.disabled}
if d.images {
opts.Extensions = []string{"png", "jpg", "jpeg", "gif"}
}
if mujica.FileDropZone(c, &d.files, opts).Changed() {
log(fmt.Sprintf("files %d", len(d.files)))
}
}},
{ID: "050", Name: "Transfer", ZhName: "穿梭框", Category: "Forms", Code: formCode["Transfer"],
Keys: "Up and Down move within a list, Space checks; Tab reaches the move buttons.",
Demo: func(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "050", func() transferDemo { return transferDemo{chosen: []string{"sakiko"}} })
formToggles(c, []string{"Disabled"}, &d.disabled)
if mujica.Transfer(c, &d.chosen, transferDemoItems, mujica.TransferOptions{SourceTitle: "Auditions", TargetTitle: "Band", Disabled: d.disabled}).Changed() {
log(fmt.Sprintf("band %q", d.chosen))
}
}},
}
// formCode holds each page's copyable snippet, the same as its doc and Example.
var formCode = map[string]string{
"Form": `var state mujica.FormState
name := ""
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
res := mujica.Form(c, &state, mujica.FormOptions{}, func(f *mujica.FormScope) {
mujica.FormItem(f, "name", &name, mujica.FormItemOptions[string]{Label: "Name", Required: true}, func() *ui.Element {
return mujica.InputGroup(c, &name, mujica.InputGroupOptions{Invalid: state.Error("name") != ""}).Input
})
})
if res.Submitted() {
state.EndSubmit("") // after saving name
}
}
ui.Render(view, 400, 300, 1)`,
"FormField": `email := ""
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.FormField(c, "Email", mujica.FormFieldOptions{Required: true, Description: "For receipts."}, func() {
mujica.InputGroup(c, &email, mujica.InputGroupOptions{})
})
}
ui.Render(view, 400, 300, 1)`,
"InputGroup": `site := ""
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
g := mujica.InputGroup(c, &site, mujica.InputGroupOptions{
Prefix: "https://", Label: "Site",
Buttons: []mujica.InputGroupButton{{Label: "Open"}},
})
if g.Clicked == 0 {
c.OpenURL("https://" + site)
}
}
ui.Render(view, 400, 300, 1)`,
"PinInput": `code := ""
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.PinInput(c, &code, mujica.PinInputOptions{Length: 6}).Completed() {
fmt.Println("verify", code)
}
}
ui.Render(view, 400, 300, 1)`,
"TagsInput": `tags := []string{"gothic"}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.TagsInput(c, &tags, mujica.TagsInputOptions{Max: 5, Placeholder: "Add a tag", Label: "Tags"})
}
ui.Render(view, 400, 300, 1)`,
"Rating": `stars := 3.5
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Rating(c, &stars, mujica.RatingOptions{Step: 0.5, ShowValue: true})
}
ui.Render(view, 400, 300, 1)`,
"ColorPicker": `tint := ui.Hex("#7D2034")
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ColorPicker(c, &tint, mujica.ColorPickerOptions{Label: "Tint"})
}
ui.Render(view, 400, 300, 1)`,
"FilePicker": `var paths []string
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.FilePicker(c, &paths, mujica.FilePickerOptions{
Multiple: true,
Filters: []mygo.FileFilter{{Name: "Scores", Extensions: []string{"pdf", "mid"}}},
})
}
ui.Render(view, 400, 300, 1)`,
"FileDropZone": `var files []mujica.DroppedFile
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.FileDropZone(c, &files, mujica.FileDropZoneOptions{
Extensions: []string{"png", "jpg"}, MaxFiles: 3, MaxSize: 5 << 20,
})
}
ui.Render(view, 400, 300, 1)`,
"Transfer": `chosen := []string{"uika"}
items := []mujica.TransferItem{{ID: "uika", Label: "Uika"}, {ID: "mutsumi", Label: "Mutsumi"}, {ID: "nyamu", Label: "Nyamu", Disabled: true}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Transfer(c, &chosen, items, mujica.TransferOptions{})
}
ui.Render(view, 400, 300, 1)`,
}
+356 -1
View File
@@ -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, &notes, 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: &current})
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)
}
})
}},
}
+497 -1
View File
@@ -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")
})
})
}
+460 -1
View File
@@ -1,3 +1,462 @@
package main
var navigationPages = []page{}
import (
"fmt"
"math"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"mujicaui/icons"
)
var navigationPages = []page{
{ID: "059", Name: "Tabs", ZhName: "标签页", Category: "Navigation", Demo: navigationTabsDemo, Code: navigationTabsCode,
Keys: "Tab enters the tab list at the chosen tab. Left/Right (Up/Down when vertical) choose the next enabled tab, skipping disabled ones and wrapping; Home and End choose the first and last."},
{ID: "060", Name: "SegmentedControl", ZhName: "分段控件", Category: "Navigation", Demo: navigationSegmentedDemo, Code: navigationSegmentedCode,
Keys: "One Tab stop. Arrow keys choose the previous or next segment and wrap; Home and End choose the ends."},
{ID: "061", Name: "Breadcrumb", ZhName: "面包屑", Category: "Navigation", Demo: navigationBreadcrumbDemo, Code: navigationBreadcrumbCode,
Keys: "Tab moves through the links and the fold button; Enter or Space activates. In the fold menu, Tab moves between items and Escape closes it, returning focus to the button."},
{ID: "062", Name: "Pagination", ZhName: "分页", Category: "Navigation", Demo: navigationPaginationDemo, Code: navigationPaginationCode,
Keys: "The page buttons are one Tab stop: Left/Right move, Home/End jump to the ends, Enter or Space goes to the page. The size select opens with Up/Down; the jump field goes on Enter."},
{ID: "063", Name: "Steps", ZhName: "步骤条", Category: "Navigation", Demo: navigationStepsDemo, Code: navigationStepsCode,
Keys: "Clickable steps are one Tab stop; arrow keys move between them and Enter or Space goes to the step."},
{ID: "064", Name: "Sidebar", ZhName: "侧边栏", Category: "Navigation", Demo: navigationSidebarDemo, Code: navigationSidebarCode,
Keys: "One Tab stop. Up/Down choose the previous or next item, Home/End the ends, typing the first letters of a label jumps to it. Section and group titles open and close with a click."},
{ID: "065", Name: "NavigationMenu", ZhName: "导航菜单", Category: "Navigation", Demo: navigationMenuDemo, Code: navigationMenuCode,
Keys: "Left/Right move between top links; Down or Enter opens a group with focus on its first link, Up/Down move inside, Escape closes. Back/Forward also answer Cmd+[ / Cmd+] (Alt+Left/Right off macOS)."},
{ID: "066", Name: "Menubar", ZhName: "菜单栏", Category: "Navigation", Demo: navigationMenubarDemo, Code: navigationMenubarCode,
Keys: "Tab reaches the bar; Left/Right move between menus, Down opens one. Up/Down/Home/End move through items, Right opens a submenu (or the next menu), Left closes it, Enter/Space chooses, Escape closes one level."},
{ID: "067", Name: "Toolbar", ZhName: "工具栏", Category: "Navigation", Demo: navigationToolbarDemo, Code: navigationToolbarCode,
Keys: "One Tab stop. Left/Right move between actions, Home/End to the ends, Enter or Space runs one. Actions that do not fit move to the » menu at the end."},
{ID: "068", Name: "CommandPalette", ZhName: "命令面板", Category: "Navigation", Demo: navigationPaletteDemo, Code: navigationPaletteCode,
Keys: "Cmd+K (Ctrl+K off macOS) opens it with focus in the search. Type to filter, Up/Down choose (wrapping), Enter runs and closes, Escape closes without running."},
}
// navigationCheck is a check box that logs its changes.
func navigationCheck(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))
}
}
// navigationSlider is a labeled whole-number slider that logs its changes.
func navigationSlider(c *ui.Context, v *float64, label string, lo, hi float64, log func(string)) {
was := math.Round(*v)
ui.Row(c).Gap(8).AlignItems(ui.Center).Children(func() {
ui.Text(c, fmt.Sprintf("%s %.0f", label, math.Round(*v)))
ui.StepSlider(c, v, lo, hi, 1).Width(160)
})
if math.Round(*v) != was {
log(fmt.Sprintf("%s: %.0f", label, math.Round(*v)))
}
}
func navigationControls(c *ui.Context, fn func()) { ui.Row(c).Wrap().Gap(16).Children(fn) }
func navigationDemo(c *ui.Context, controls, body func()) {
ui.Column(c).Gap(20).AlignItems(ui.Stretch).Children(func() {
navigationControls(c, controls)
body()
})
}
const navigationTabsCode = `mujica.Tabs(c, &tab, []mujica.Tab{
{Label: "General", Panel: func() { ui.Text(c, "General settings") }},
{Label: "Fonts", Icon: icons.Must("italic"), Panel: func() { ui.Text(c, "Fonts") }},
{Label: "Archive", Disabled: true},
}, mujica.TabsOptions{Label: "Settings"})`
func navigationTabsDemo(c *ui.Context, log func(string)) {
type state struct {
tab int
vertical, archive, many bool
}
s := ui.Local(c.Root(), "059", func() state { return state{archive: true} })
navigationDemo(c, func() {
navigationCheck(c, &s.vertical, "Vertical", log)
navigationCheck(c, &s.archive, "Disable Archive", log)
navigationCheck(c, &s.many, "Many tabs (overflow)", log)
}, func() {
names := []string{"General", "Fonts", "Archive", "Advanced"}
if s.many {
names = append(names, "Seals", "Heraldry", "Ledgers", "Envoys", "Banquets", "Chapel")
}
var tabs []mujica.Tab
for i, n := range names {
tabs = append(tabs, mujica.Tab{Label: n, Disabled: n == "Archive" && s.archive, Panel: func() {
ui.Text(c, fmt.Sprintf("The %s panel (tab %d).", n, i+1))
}})
}
tabs[1].Icon = icons.Must("italic")
s.tab = min(s.tab, len(tabs)-1)
if tabs[s.tab].Disabled {
s.tab = 0
}
ui.Column(c).Width(420).Children(func() {
if mujica.Tabs(c, &s.tab, tabs, mujica.TabsOptions{Vertical: s.vertical, Label: "Settings"}).Changed() {
log("Changed: tab " + names[s.tab])
}
})
})
}
const navigationSegmentedCode = `mujica.SegmentedControl(c, &view, []mujica.Segment{
{Label: "List", Icon: icons.Must("list"), HideLabel: true},
{Label: "Grid", Icon: icons.Must("layout-grid"), HideLabel: true},
}, mujica.SegmentedControlOptions{Label: "View"})`
func navigationSegmentedDemo(c *ui.Context, log func(string)) {
type state struct {
text, icon int
equal, iconOnly, disab bool
}
s := ui.Local(c.Root(), "060", func() state { return state{} })
navigationDemo(c, func() {
navigationCheck(c, &s.equal, "Equal widths", log)
navigationCheck(c, &s.iconOnly, "Icons only", log)
navigationCheck(c, &s.disab, "Disabled", log)
}, func() {
opts := mujica.SegmentedControlOptions{Equal: s.equal, Disabled: s.disab, Label: "Range"}
ui.Row(c).Gap(24).AlignItems(ui.Center).Children(func() {
if mujica.SegmentedControl(c, &s.text, []mujica.Segment{{Label: "Day"}, {Label: "Week"}, {Label: "Fortnight"}}, opts).Changed() {
log(fmt.Sprintf("Changed: range %d", s.text))
}
opts.Label = "View"
if mujica.SegmentedControl(c, &s.icon, []mujica.Segment{
{Label: "List", Icon: icons.Must("list"), HideLabel: s.iconOnly},
{Label: "Grid", Icon: icons.Must("layout-grid"), HideLabel: s.iconOnly},
}, opts).Changed() {
log(fmt.Sprintf("Changed: view %d", s.icon))
}
})
})
}
const navigationBreadcrumbCode = `if i, ok := mujica.Breadcrumb(c, path, mujica.BreadcrumbOptions{}).Chosen(); ok {
path = path[:i+1]
}`
var navigationBreadcrumbNames = []string{"Home", "Library", "Manuscripts", "Seventeenth century", "Correspondence of the court", "Letters to the Duchess", "Draft of the winter decree"}
func navigationBreadcrumbDemo(c *ui.Context, log func(string)) {
type state struct{ depth, max float64 }
s := ui.Local(c.Root(), "061", func() state { return state{depth: 6, max: 4} })
navigationDemo(c, func() {
navigationSlider(c, &s.depth, "Depth", 1, 7, log)
navigationSlider(c, &s.max, "MaxItems", 3, 7, log)
}, func() {
var path []mujica.BreadcrumbItem
for i, n := range navigationBreadcrumbNames[:int(s.depth)] {
it := mujica.BreadcrumbItem{Label: n}
if i == 0 {
it.Icon = icons.Must("house")
}
path = append(path, it)
}
if i, ok := mujica.Breadcrumb(c, path, mujica.BreadcrumbOptions{MaxItems: int(s.max)}).Chosen(); ok {
log("Chosen: " + path[i].Label)
s.depth = float64(i + 1)
}
})
}
const navigationPaginationCode = `if mujica.Pagination(c, &page, &size, mujica.PaginationOptions{Total: 420, PageSizes: []int{10, 20, 50}, Jump: true}).Changed() {
load(page, size)
}`
func navigationPaginationDemo(c *ui.Context, log func(string)) {
type state struct {
page, size int
total float64
sizes, jump, disab bool
}
s := ui.Local(c.Root(), "062", func() state { return state{page: 1, size: 20, total: 420, sizes: true, jump: true} })
navigationDemo(c, func() {
navigationSlider(c, &s.total, "Total", 0, 1000, log)
navigationCheck(c, &s.sizes, "Page sizes", log)
navigationCheck(c, &s.jump, "Jump", log)
navigationCheck(c, &s.disab, "Disabled", log)
}, func() {
opts := mujica.PaginationOptions{Total: int(s.total), Jump: s.jump, Disabled: s.disab}
if s.sizes {
opts.PageSizes = []int{10, 20, 50}
} else {
s.size = 20
}
if mujica.Pagination(c, &s.page, &s.size, opts).Changed() {
log(fmt.Sprintf("Changed: page %d, size %d", s.page, s.size))
}
})
}
const navigationStepsCode = `mujica.Steps(c, &current, []mujica.Step{
{Title: "Account", Clickable: true},
{Title: "Profile", Description: "Name and seal"},
{Title: "Confirm"},
}, mujica.StepsOptions{Label: "Sign-up"})`
func navigationStepsDemo(c *ui.Context, log func(string)) {
type state struct {
current int
vertical, failed, clickOK bool
}
s := ui.Local(c.Root(), "063", func() state { return state{current: 1, clickOK: true} })
steps := []mujica.Step{
{Title: "Account", Description: "Name and seal"},
{Title: "Profile", Description: "Titles and house"},
{Title: "Payment", Description: "Treasury"},
{Title: "Confirm"},
}
navigationDemo(c, func() {
navigationCheck(c, &s.vertical, "Vertical", log)
navigationCheck(c, &s.failed, "Payment failed", log)
navigationCheck(c, &s.clickOK, "Done steps clickable", log)
if mujica.Button(c, "Back", mujica.ButtonOptions{Variant: mujica.Secondary, Disabled: s.current == 0}).Clicked() {
s.current--
log(fmt.Sprintf("Back: step %d", s.current+1))
}
if mujica.Button(c, "Next", mujica.ButtonOptions{Disabled: s.current == len(steps)}).Clicked() {
s.current++
log(fmt.Sprintf("Next: step %d", s.current+1))
}
}, func() {
for i := range steps {
steps[i].Clickable = s.clickOK && i < s.current
}
steps[2].Error = s.failed
if mujica.Steps(c, &s.current, steps, mujica.StepsOptions{Vertical: s.vertical, Label: "Sign-up"}).Changed() {
log("Changed: step " + steps[s.current].Title)
}
})
}
const navigationSidebarCode = `mujica.Sidebar(c, &selected, []mujica.SidebarSection{
{Title: "Mail", Items: []mujica.SidebarItem{
{ID: "inbox", Label: "Inbox", Icon: icons.Must("inbox"), Count: 3},
{Label: "Folders", Items: []mujica.SidebarItem{{ID: "work", Label: "Work", Icon: icons.Must("folder")}}},
}},
}, mujica.SidebarOptions{})`
func navigationSidebarDemo(c *ui.Context, log func(string)) {
type state struct {
selected string
compact bool
unread float64
}
s := ui.Local(c.Root(), "064", func() state { return state{selected: "inbox", unread: 12} })
navigationDemo(c, func() {
navigationCheck(c, &s.compact, "Compact", log)
navigationSlider(c, &s.unread, "Unread", 0, 1200, log)
}, func() {
sections := []mujica.SidebarSection{
{Title: "Library", Items: []mujica.SidebarItem{
{ID: "inbox", Label: "Inbox", Icon: icons.Must("inbox"), Count: int(s.unread)},
{ID: "starred", Label: "Starred", Icon: icons.Must("star")},
{Label: "Projects", Items: []mujica.SidebarItem{
{ID: "opera", Label: "Opera", Icon: icons.Must("folder")},
{ID: "ballet", Label: "Ballet", Icon: icons.Must("folder")},
}},
}},
{Title: "System", Items: []mujica.SidebarItem{
{ID: "settings", Label: "Settings", Icon: icons.Must("settings")},
{ID: "trash", Label: "Trash", Icon: icons.Must("trash-2")},
}},
}
ui.Row(c).Height(360).Border(1, mujica.Tokens(c).Border).Radius(4).Clip().Children(func() {
if mujica.Sidebar(c, &s.selected, sections, mujica.SidebarOptions{Compact: s.compact}).Changed() {
log("Changed: " + s.selected)
}
ui.Text(c, "Showing "+s.selected).Padding(24)
})
})
}
const navigationMenuCode = `mujica.NavigationMenu(c, router, []mujica.NavigationLink{
{Label: "Home", Path: "/"},
{Label: "Library", Items: []mujica.NavigationLink{
{Label: "Poems", Path: "/library/poems", Description: "Verse of the court"},
}},
}, mujica.NavigationMenuOptions{History: true})
router.View(c, func(r *ui.Route) { ui.Text(c, r.Path()) })`
func navigationMenuDemo(c *ui.Context, log func(string)) {
type state struct {
router *ui.Router
history bool
last string
}
s := ui.Local(c.Root(), "065", func() state { return state{router: ui.NewRouter("/"), history: true, last: "/"} })
navigationDemo(c, func() {
navigationCheck(c, &s.history, "Back and forward", log)
}, func() {
mujica.NavigationMenu(c, s.router, []mujica.NavigationLink{
{Label: "Home", Path: "/", Icon: icons.Must("house")},
{Label: "Library", Items: []mujica.NavigationLink{
{Label: "Poems", Path: "/library/poems", Description: "Verse of the court"},
{Label: "Letters", Path: "/library/letters", Description: "Correspondence"},
{Label: "Archive", Items: []mujica.NavigationLink{
{Label: "Older letters", Path: "/archive/letters"},
}},
}},
{Label: "About", Path: "/about"},
}, mujica.NavigationMenuOptions{History: s.history})
if p := s.router.Path(); p != s.last {
log(fmt.Sprintf("Path: %s → %s", s.last, p))
s.last = p
}
ui.Column(c).Height(120).Children(func() {
s.router.View(c, func(r *ui.Route) {
r.Title(r.Path())
ui.Text(c, "Page "+r.Path()).Padding(16)
})
})
})
}
const navigationMenubarCode = `res := mujica.Menubar(c, []mujica.MenubarMenu{
{Label: "File", Items: []mujica.MenubarItem{
{ID: "new", Label: "New", Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyN}},
{Label: "Export", Items: []mujica.MenubarItem{{ID: "pdf", Label: "PDF"}}},
}},
{Label: "View", Items: []mujica.MenubarItem{
{ID: "wrap", Label: "Word wrap", Kind: mujica.MenubarCheck, Checked: wrap},
}},
}, mujica.MenubarOptions{})
if id, ok := res.Chosen(); ok && id == "wrap" {
wrap = !wrap
}`
func navigationMenubarDemo(c *ui.Context, log func(string)) {
type state struct {
wrap, ruler, locked bool
zoom string
}
s := ui.Local(c.Root(), "066", func() state { return state{wrap: true, zoom: "fit"} })
navigationDemo(c, func() {
navigationCheck(c, &s.locked, "Lock editing (disables Edit items)", log)
}, func() {
menus := []mujica.MenubarMenu{
{Label: "File", Items: []mujica.MenubarItem{
{ID: "new", Label: "New", Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyN}},
{ID: "open", Label: "Open…", Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyO}},
{Label: "Export", Items: []mujica.MenubarItem{
{ID: "pdf", Label: "PDF"},
{ID: "png", Label: "Image"},
{Label: "Archive", Items: []mujica.MenubarItem{{ID: "zip", Label: "Zip"}, {ID: "tar", Label: "Tarball"}}},
}},
{Kind: mujica.MenubarSeparator},
{ID: "quit", Label: "Quit", Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyQ}},
}},
{Label: "Edit", Items: []mujica.MenubarItem{
{ID: "undo", Label: "Undo", Disabled: s.locked, Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyZ}},
{ID: "redo", Label: "Redo", Disabled: s.locked, Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyZ}},
}},
{Label: "View", Items: []mujica.MenubarItem{
{ID: "wrap", Label: "Word wrap", Kind: mujica.MenubarCheck, Checked: s.wrap},
{ID: "ruler", Label: "Ruler", Kind: mujica.MenubarCheck, Checked: s.ruler},
{Kind: mujica.MenubarSeparator},
{ID: "fit", Label: "Fit to window", Kind: mujica.MenubarRadio, Checked: s.zoom == "fit"},
{ID: "actual", Label: "Actual size", Kind: mujica.MenubarRadio, Checked: s.zoom == "actual"},
}},
}
ui.Column(c).Height(260).Border(1, mujica.Tokens(c).Border).Radius(4).Clip().Children(func() {
if id, ok := mujica.Menubar(c, menus, mujica.MenubarOptions{}).Chosen(); ok {
switch id {
case "wrap":
s.wrap = !s.wrap
case "ruler":
s.ruler = !s.ruler
case "fit", "actual":
s.zoom = id
}
log("Chosen: " + id)
}
ui.Text(c, fmt.Sprintf("Word wrap %v · Ruler %v · Zoom %s", s.wrap, s.ruler, s.zoom)).Padding(16)
})
})
}
const navigationToolbarCode = `mujica.Toolbar(c, mujica.ToolbarOptions{Label: "Format"}, func() {
mujica.ToolbarGroup(c, func() {
mujica.ToolbarButton(c, "Bold", mujica.ToolbarButtonOptions{Icon: icons.Must("bold")})
mujica.ToolbarButton(c, "Italic", mujica.ToolbarButtonOptions{Icon: icons.Must("italic")})
})
mujica.ToolbarSeparator(c)
if mujica.ToolbarButton(c, "Copy", mujica.ToolbarButtonOptions{Icon: icons.Must("copy"), ShowLabel: true}).Clicked() {
copySelection()
}
})`
func navigationToolbarDemo(c *ui.Context, log func(string)) {
type state struct {
width float64
labels bool
noDeletes bool
}
s := ui.Local(c.Root(), "067", func() state { return state{width: 520, noDeletes: true} })
navigationDemo(c, func() {
navigationSlider(c, &s.width, "Width", 120, 640, log)
navigationCheck(c, &s.labels, "Show labels", log)
navigationCheck(c, &s.noDeletes, "Disable Delete", log)
}, func() {
ui.Column(c).Width(float32(s.width)).Children(func() {
mujica.Toolbar(c, mujica.ToolbarOptions{Label: "Format"}, func() {
button := func(label, icon string, disabled bool) {
if mujica.ToolbarButton(c, label, mujica.ToolbarButtonOptions{Icon: icons.Must(icon), ShowLabel: s.labels, Disabled: disabled}).Clicked() {
log("Clicked: " + label)
}
}
mujica.ToolbarGroup(c, func() {
button("Bold", "bold", false)
button("Italic", "italic", false)
button("Underline", "underline", false)
})
mujica.ToolbarSeparator(c)
button("Copy", "copy", false)
button("Image", "image", false)
button("Delete", "trash-2", s.noDeletes)
mujica.ToolbarSeparator(c)
button("Settings", "settings", false)
})
})
})
}
const navigationPaletteCode = `res := mujica.CommandPalette(c, &open, []mujica.Command{
{ID: "new", Label: "New document", Group: "File"},
{ID: "theme", Label: "Toggle dark mode", Group: "View", Keywords: []string{"night"}},
}, mujica.CommandPaletteOptions{})
if id, ok := res.Chosen(); ok {
run(id)
}`
func navigationPaletteDemo(c *ui.Context, log func(string)) {
type state struct{ open, wasOpen bool }
s := ui.Local(c.Root(), "068", func() state { return state{} })
navigationDemo(c, func() {
if mujica.Button(c, "Open command palette", mujica.ButtonOptions{Icon: icons.Must("command")}).Clicked() {
s.open = true
}
ui.Text(c, "or press Cmd+K / Ctrl+K").TextColor(mujica.Tokens(c).TextMuted)
}, func() {
res := mujica.CommandPalette(c, &s.open, []mujica.Command{
{ID: "new", Label: "New document", Group: "File", Icon: icons.Must("file")},
{ID: "open", Label: "Open recent", Group: "File", Icon: icons.Must("folder")},
{ID: "save", Label: "Save all", Group: "File", Icon: icons.Must("file-text")},
{ID: "theme", Label: "Toggle dark mode", Group: "View", Keywords: []string{"theme", "night"}, Icon: icons.Must("moon")},
{ID: "zoom-in", Label: "Zoom in", Group: "View", Icon: icons.Must("zoom-in")},
{ID: "zoom-out", Label: "Zoom out", Group: "View", Icon: icons.Must("zoom-out")},
{ID: "settings", Label: "Open settings", Group: "Application", Keywords: []string{"preferences"}, Icon: icons.Must("settings")},
}, mujica.CommandPaletteOptions{})
if s.open != s.wasOpen {
log(fmt.Sprintf("Open: %v", s.open))
s.wasOpen = s.open
}
if id, ok := res.Chosen(); ok {
log("Run: " + id)
}
})
}
+463 -1
View File
@@ -1,3 +1,465 @@
package main
var overlayPages = []page{}
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
const overlayCategory = "Overlays"
var overlayPages = []page{
{ID: "081", Name: "Dialog", ZhName: "对话框", Category: overlayCategory, Demo: dialogDemo, Code: dialogCode,
Keys: "Tab / Shift+Tab 在对话框内循环;Escape 关闭(可关闭此规则);关闭后焦点回到触发按钮。"},
{ID: "082", Name: "AlertDialog", ZhName: "警示对话框", Category: overlayCategory, Demo: alertDialogDemo, Code: alertDialogCode,
Keys: "打开时焦点在取消;Enter 只触发聚焦的按钮;Escape 等同取消;点击遮罩无效。"},
{ID: "083", Name: "Drawer", ZhName: "抽屉", Category: overlayCategory, Demo: drawerDemo, Code: drawerCode,
Keys: "模态:焦点限制在抽屉内并在关闭后恢复;Escape 关闭。非模态:窗口仍可操作,Escape 关闭。"},
{ID: "084", Name: "Popover", ZhName: "弹出框", Category: overlayCategory, Demo: popoverDemo, Code: popoverCode,
Keys: "Tab 从锚点进入弹层;Escape 或外部点击关闭并把焦点还给锚点。"},
{ID: "085", Name: "Tooltip", ZhName: "文字提示", Category: overlayCategory, Demo: tooltipDemo, Code: tooltipCode,
Keys: "键盘聚焦或悬停超过延迟后显示;Escape 隐藏;移开或失焦隐藏。"},
{ID: "086", Name: "HoverCard", ZhName: "悬停卡片", Category: overlayCategory, Demo: hoverCardDemo, Code: hoverCardCode,
Keys: "键盘聚焦锚点后显示,Tab 进入卡片;Escape 关闭。"},
{ID: "087", Name: "DropdownMenu", ZhName: "下拉菜单", Category: overlayCategory, Demo: dropdownMenuDemo, Code: dropdownMenuCode,
Keys: "Enter / Space / ↓ 打开;↑↓ Home End 移动;→ 打开子菜单,← 关闭;Enter 选择;Escape 或 Tab 关闭。"},
{ID: "088", Name: "ContextMenu", ZhName: "右键菜单", Category: overlayCategory, Demo: contextMenuDemo, Code: contextMenuCode,
Keys: "右键在指针处打开;焦点在区域内时按 Menu 键或 Shift+F10 在区域下方打开;其余按键同下拉菜单。"},
{ID: "089", Name: "Popconfirm", ZhName: "气泡确认", Category: overlayCategory, Demo: popconfirmDemo, Code: popconfirmCode,
Keys: "打开后焦点在确认;Enter 确认;Escape 或外部点击取消。"},
{ID: "090", Name: "Overlay", ZhName: "覆盖层", Category: overlayCategory, Demo: overlayDemo, Code: overlayCode,
Keys: "Escape 关闭;模态时焦点限制在覆盖层内,点击遮罩关闭。"},
}
// overlayControls lays out a demo's parameter row.
func overlayControls(c *ui.Context, fn func()) {
ui.Row(c).Gap(16).Wrap().AlignItems(ui.Center).Padding(0, 0, 12, 0).Children(fn)
}
func overlayCheck(c *ui.Context, log func(string), v *bool, label string) {
if ui.Checkbox(c, v, label).Changed() {
log(fmt.Sprintf("%s = %v", label, *v))
}
}
const dialogCode = `open := true
name := "Ophelia"
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.Button(c, "Rename…", mujica.ButtonOptions{}).Clicked() {
open = true
}
mujica.Dialog(c, &open, mujica.DialogOptions{
Title: "Rename the chamber",
Description: "The new name shows in every hall.",
Decorated: true,
Actions: func() {
if mujica.Button(c, "Cancel", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
open = false
}
if mujica.Button(c, "Rename", mujica.ButtonOptions{}).Clicked() {
open = false
}
},
}, func() {
ui.TextInput(c, &name)
})
}
ui.Render(view, 400, 300, 1)`
type dialogDemoState struct {
open, decorated, keepBackdrop, keepEscape, hideClose bool
name string
}
func dialogDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "081", func() dialogDemoState { return dialogDemoState{decorated: true, name: "Ophelia"} })
overlayControls(c, func() {
overlayCheck(c, log, &s.decorated, "Decorated")
overlayCheck(c, log, &s.keepBackdrop, "KeepOnBackdrop")
overlayCheck(c, log, &s.keepEscape, "KeepOnEscape")
overlayCheck(c, log, &s.hideClose, "HideClose")
})
if mujica.Button(c, "Rename…", mujica.ButtonOptions{}).Clicked() {
s.open = true
log("open")
}
was := s.open
mujica.Dialog(c, &s.open, mujica.DialogOptions{
Title: "Rename the chamber", Description: "The new name shows in every hall.", Decorated: s.decorated,
KeepOnBackdrop: s.keepBackdrop, KeepOnEscape: s.keepEscape, HideClose: s.hideClose,
Actions: func() {
if mujica.Button(c, "Cancel", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.open = false
}
if mujica.Button(c, "Rename", mujica.ButtonOptions{}).Clicked() {
log("renamed to " + s.name)
s.open = false
}
},
}, func() { ui.TextInput(c, &s.name) })
if was && !s.open {
log("closed")
}
}
const alertDialogCode = `open := true
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.Button(c, "Delete archive", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() {
open = true
}
r := mujica.AlertDialog(c, &open, mujica.AlertDialogOptions{
Title: "Delete the archive?",
Description: "Twelve letters will be destroyed.",
ConfirmText: "archive",
})
if r.Confirmed {
fmt.Println("deleted")
}
}
ui.Render(view, 400, 300, 1)`
type alertDialogDemoState struct{ open, typed bool }
func alertDialogDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "082", func() alertDialogDemoState { return alertDialogDemoState{} })
overlayControls(c, func() { overlayCheck(c, log, &s.typed, "ConfirmText") })
if mujica.Button(c, "Delete archive", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() {
s.open = true
log("open")
}
opts := mujica.AlertDialogOptions{Title: "Delete the archive?", Description: "Twelve letters will be destroyed. This cannot be undone."}
if s.typed {
opts.ConfirmText = "archive"
}
r := mujica.AlertDialog(c, &s.open, opts)
if r.Confirmed {
log("confirmed")
}
if r.Cancelled {
log("cancelled")
}
}
const drawerCode = `open := true
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.Button(c, "Filters", mujica.ButtonOptions{}).Clicked() {
open = true
}
mujica.Drawer(c, &open, mujica.DrawerOptions{Title: "Filters", Side: mujica.DrawerRight, Size: 320}, func() {
ui.Text(c, "Guests are placed by rank.")
})
}
ui.Render(view, 400, 300, 1)`
type drawerDemoState struct {
open, nonModal bool
side string
size float64
}
func drawerDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "083", func() drawerDemoState { return drawerDemoState{side: "Right", size: 320} })
sides := map[string]mujica.DrawerSide{"Right": mujica.DrawerRight, "Left": mujica.DrawerLeft, "Top": mujica.DrawerTop, "Bottom": mujica.DrawerBottom}
overlayControls(c, func() {
if ui.Select(c, &s.side, []string{"Right", "Left", "Top", "Bottom"}).Changed() {
log("side = " + s.side)
}
overlayCheck(c, log, &s.nonModal, "NonModal")
ui.Text(c, fmt.Sprintf("Size %.0f", s.size))
if ui.StepSlider(c, &s.size, 120, 900, 20).Width(160).Changed() {
log(fmt.Sprintf("size = %.0f", s.size))
}
})
if mujica.Button(c, "Open drawer", mujica.ButtonOptions{}).Clicked() {
s.open = true
log("open")
}
was := s.open
mujica.Drawer(c, &s.open, mujica.DrawerOptions{Title: "Seating chart", Side: sides[s.side], Size: float32(s.size), NonModal: s.nonModal,
Actions: func() {
if mujica.Button(c, "Apply", mujica.ButtonOptions{}).Clicked() {
log("applied")
s.open = false
}
}}, func() {
ui.Text(c, "Guests are placed by rank, then by arrival.").TextColor(mujica.Tokens(c).TextMuted)
})
if was && !s.open {
log("closed")
}
}
const popoverCode = `open := true
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
b := mujica.Button(c, "Guests", mujica.ButtonOptions{Variant: mujica.Secondary})
if b.Clicked() {
open = !open
}
mujica.Popover(c, b, &open, mujica.PopoverOptions{Title: "Guest list"}, func() {
ui.Text(c, "Twelve guests confirmed.")
})
}
ui.Render(view, 400, 300, 1)`
type popoverDemoState struct {
open, titled bool
rsvp bool
}
func popoverDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "084", func() popoverDemoState { return popoverDemoState{titled: true} })
overlayControls(c, func() { overlayCheck(c, log, &s.titled, "Title") })
b := mujica.Button(c, "Guests", mujica.ButtonOptions{Variant: mujica.Secondary})
if b.Clicked() {
s.open = !s.open
log(fmt.Sprintf("open = %v", s.open))
}
opts := mujica.PopoverOptions{Label: "Guest list"}
if s.titled {
opts.Title = "Guest list"
}
was := s.open
mujica.Popover(c, b, &s.open, opts, func() {
ui.Text(c, "Twelve guests confirmed; three await a reply.")
if ui.Checkbox(c, &s.rsvp, "Remind the others").Changed() {
log(fmt.Sprintf("remind = %v", s.rsvp))
}
})
if was && !s.open {
log("closed")
}
}
const tooltipCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
b := mujica.Button(c, "Seal", mujica.ButtonOptions{})
mujica.Tooltip(c, b, "Seal the letter with wax", mujica.TooltipOptions{})
}
ui.Render(view, 400, 300, 1)`
func tooltipDemo(c *ui.Context, log func(string)) {
delay := ui.Local(c.Root(), "085", func() float64 { return 500 })
overlayControls(c, func() {
ui.Text(c, fmt.Sprintf("Delay %.0f ms", *delay))
if ui.StepSlider(c, delay, 100, 1500, 100).Width(160).Changed() {
log(fmt.Sprintf("delay = %.0f ms", *delay))
}
})
opts := mujica.TooltipOptions{Delay: time.Duration(*delay) * time.Millisecond}
ui.Row(c).Gap(12).Children(func() {
b := mujica.Button(c, "Seal", mujica.ButtonOptions{})
if b.Clicked() {
log("seal clicked")
}
mujica.Tooltip(c, b, "Seal the letter with wax", opts)
d := mujica.Button(c, "Burn", mujica.ButtonOptions{Variant: mujica.Danger})
if d.Clicked() {
log("burn clicked")
}
mujica.Tooltip(c, d, "Burn every copy of the letter; nothing is kept", opts)
})
}
const hoverCardCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
b := mujica.Button(c, "Lady Sakiko", mujica.ButtonOptions{Variant: mujica.Ghost})
mujica.HoverCard(c, b, mujica.HoverCardOptions{}, func() {
ui.Text(c, "Keyboardist of the court.")
})
}
ui.Render(view, 400, 300, 1)`
func hoverCardDemo(c *ui.Context, log func(string)) {
shown := ui.Local(c.Root(), "086", func() bool { return false })
b := mujica.Button(c, "Lady Sakiko", mujica.ButtonOptions{Variant: mujica.Ghost})
card := mujica.HoverCard(c, b, mujica.HoverCardOptions{}, func() {
ui.Text(c, "Lady Sakiko").FontWeight(600)
ui.Text(c, "Keyboardist of the court; writes the masquerade's score.").TextColor(mujica.Tokens(c).TextMuted)
if mujica.Button(c, "Follow", mujica.ButtonOptions{Variant: mujica.Outline}).Clicked() {
log("follow")
}
})
if (card != nil) != *shown {
*shown = card != nil
log(fmt.Sprintf("card shown = %v", *shown))
}
}
const dropdownMenuCode = `pinned := false
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.DropdownMenu(c, "Actions", mujica.DropdownMenuOptions{}, func(m *mujica.PopupMenu) {
m.Group("Letter", func() {
if m.Item("Rename", mujica.PopupMenuItemOptions{}) {
fmt.Println("rename")
}
m.Check("Pinned", &pinned, mujica.PopupMenuItemOptions{KeepOpen: true})
})
m.Submenu("Move to", mujica.PopupMenuItemOptions{}, func(m *mujica.PopupMenu) {
m.Item("Archive", mujica.PopupMenuItemOptions{})
})
m.Separator()
m.Item("Burn", mujica.PopupMenuItemOptions{Disabled: true})
})
}
ui.Render(view, 400, 300, 1)`
type menuDemoState struct{ pinned, sealed, locked, disabled bool }
// overlayMenu fills the shared demo menu, logging every choice.
func overlayMenu(s *menuDemoState, log func(string)) func(m *mujica.PopupMenu) {
return func(m *mujica.PopupMenu) {
m.Group("Letter", func() {
if m.Item("Rename", mujica.PopupMenuItemOptions{}) {
log("rename")
}
if m.Check("Pinned", &s.pinned, mujica.PopupMenuItemOptions{KeepOpen: true}) {
log(fmt.Sprintf("pinned = %v", s.pinned))
}
if m.Check("Sealed", &s.sealed, mujica.PopupMenuItemOptions{}) {
log(fmt.Sprintf("sealed = %v", s.sealed))
}
})
m.Group("Move", func() {
m.Submenu("Move to", mujica.PopupMenuItemOptions{}, func(m *mujica.PopupMenu) {
for _, f := range []string{"Archive", "Drafts", "Vault"} {
if m.Item(f, mujica.PopupMenuItemOptions{}) {
log("move to " + f)
}
}
})
})
m.Separator()
if m.Item("Burn", mujica.PopupMenuItemOptions{Disabled: s.locked}) {
log("burn")
}
}
}
func dropdownMenuDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "087", func() menuDemoState { return menuDemoState{locked: true} })
overlayControls(c, func() {
overlayCheck(c, log, &s.locked, "Burn disabled")
overlayCheck(c, log, &s.disabled, "Disabled")
})
mujica.DropdownMenu(c, "Actions", mujica.DropdownMenuOptions{Disabled: s.disabled}, overlayMenu(s, log))
}
const contextMenuCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
row := ui.Row(c).Padding(12).Children(func() {
ui.Text(c, "Letter to the Countess")
})
mujica.ContextMenu(c, row, mujica.ContextMenuOptions{}, func(m *mujica.PopupMenu) {
if m.Item("Reply", mujica.PopupMenuItemOptions{}) {
fmt.Println("reply")
}
m.Item("Delete", mujica.PopupMenuItemOptions{})
})
}
ui.Render(view, 400, 300, 1)`
func contextMenuDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "088", func() menuDemoState { return menuDemoState{} })
overlayControls(c, func() {
overlayCheck(c, log, &s.locked, "Burn disabled")
overlayCheck(c, log, &s.disabled, "Disabled")
})
k := mujica.Tokens(c)
row := ui.Column(c).Height(160).FillWidth().Padding(16).Gap(8).Background(k.Surface).Border(1, k.Border).Radius(4)
row.Children(func() {
ui.Text(c, "Right-click anywhere here, or focus the button and press Shift+F10.").TextColor(k.TextMuted)
if mujica.Button(c, "Letter to the Countess", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() {
log("letter clicked")
}
})
mujica.ContextMenu(c, row, mujica.ContextMenuOptions{Disabled: s.disabled}, overlayMenu(s, log))
}
const popconfirmCode = `open := false
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
b := mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary})
if b.Clicked() {
open = true
}
if mujica.Popconfirm(c, b, &open, mujica.PopconfirmOptions{Title: "Archive this letter?"}).Confirmed {
fmt.Println("archived")
}
}
ui.Render(view, 400, 300, 1)`
type popconfirmDemoState struct{ open, danger bool }
func popconfirmDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "089", func() popconfirmDemoState { return popconfirmDemoState{} })
overlayControls(c, func() { overlayCheck(c, log, &s.danger, "Danger") })
b := mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary})
if b.Clicked() {
s.open = true
log("open")
}
r := mujica.Popconfirm(c, b, &s.open, mujica.PopconfirmOptions{Title: "Archive this letter?", Description: "It leaves the inbox.", Danger: s.danger})
if r.Confirmed {
log("confirmed")
}
if r.Cancelled {
log("cancelled")
}
}
const overlayCode = `open := true
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Overlay(c, &open, mujica.OverlayOptions{Place: mujica.OverlayBottomRight, Label: "Now playing"}, func() {
ui.Text(c, "Symbol I: △")
})
}
ui.Render(view, 400, 300, 1)`
type overlayDemoState struct {
open, modal, dim bool
place string
}
func overlayDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "090", func() overlayDemoState { return overlayDemoState{place: "BottomRight"} })
names := []string{"Center", "Top", "Bottom", "Left", "Right", "TopLeft", "TopRight", "BottomLeft", "BottomRight"}
place := mujica.OverlayCenter
for i, n := range names {
if n == s.place {
place = mujica.OverlayPlace(i)
}
}
overlayControls(c, func() {
if ui.Select(c, &s.place, names).Changed() {
log("place = " + s.place)
}
overlayCheck(c, log, &s.modal, "Modal")
if s.modal {
overlayCheck(c, log, &s.dim, "Dim")
}
})
if mujica.Button(c, "Show overlay", mujica.ButtonOptions{}).Clicked() {
s.open = true
log("open")
}
was := s.open
mujica.Overlay(c, &s.open, mujica.OverlayOptions{Place: place, Modal: s.modal, Dim: s.modal && s.dim, Label: "Now playing"}, func() {
ui.Text(c, "Now playing").FontWeight(600)
ui.Text(c, "Symbol I: △").TextColor(mujica.Tokens(c).TextMuted)
if mujica.Button(c, "Close", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.open = false
}
})
if was && !s.open {
log("closed")
}
}