package main import ( "errors" "fmt" "strings" "time" "github.com/egoist/mygo/ui" mujica "mujicaui" "mujicaui/icons" "mujicaui/theme" ) const choiceCategory = "Choices" var choicePages = []page{ {ID: "031", Name: "Switch", ZhName: "开关", Category: choiceCategory, Demo: switchDemo, Code: `wifi := true view := func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) if mujica.Switch(c, &wifi, "Wi-Fi", mujica.SwitchOptions{}).Changed() { log.Println("Wi-Fi:", wifi) } }`, Keys: "Tab focuses the switch; Space turns it over."}, {ID: "032", Name: "Toggle", ZhName: "切换按钮", Category: choiceCategory, Demo: toggleDemo, Code: `bold := false view := func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) mujica.Toggle(c, &bold, "", mujica.ToggleOptions{Icon: icons.Must("bold"), Label: "Bold"}) }`, Keys: "Tab focuses the toggle; Space or Enter presses or releases it."}, {ID: "033", Name: "ToggleGroup", ZhName: "切换按钮组", Category: choiceCategory, Demo: toggleGroupDemo, Code: `align := []string{"left"} items := []mujica.ToggleItem[string]{ {Value: "left", Label: "Left"}, {Value: "center", Label: "Center"}, {Value: "right", Label: "Right"}, } view := func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) mujica.ToggleGroup(c, &align, items, mujica.ToggleGroupOptions{Label: "Align"}) }`, Keys: "Tab enters the group at the first enabled item, later at the last focused one; Left/Right (Up/Down when vertical), Home and End move; Space presses."}, {ID: "034", Name: "Slider", ZhName: "滑块", Category: choiceCategory, Demo: sliderDemo, Code: `volume := 40.0 view := func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) mujica.Slider(c, &volume, mujica.SliderOptions{Step: 5, ShowValue: true, Label: "Volume"}) }`, Keys: "Arrows move one step, Home and End go to the ends; the move applies when the key is released."}, {ID: "035", Name: "RangeSlider", ZhName: "范围滑块", Category: choiceCategory, Demo: rangeSliderDemo, Code: `low, high := 20.0, 80.0 view := func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) mujica.RangeSlider(c, &low, &high, mujica.RangeSliderOptions{Step: 10, Label: "Price"}) }`, Keys: "Tab moves between the two thumbs; arrows move one step; Home and End go as far as the other thumb allows."}, {ID: "036", Name: "Select", ZhName: "选择器", Category: choiceCategory, Demo: selectDemo, Code: `plan := "" options := []mujica.SelectOption[string]{ {Value: "free", Label: "Free"}, {Value: "pro", Label: "Pro"}, {Value: "team", Label: "Team", Disabled: true}, } view := func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) mujica.Select(c, &plan, options, mujica.SelectOptions{Clearable: true, Label: "Plan"}) }`, Keys: "Enter, Space, Up or Down open; arrows, Home, End move; Enter chooses; Escape closes; typing letters on the closed select jumps to a match; Backspace/Delete clear when Clearable."}, {ID: "037", Name: "MultiSelect", ZhName: "多选选择器", Category: choiceCategory, Demo: multiSelectDemo, Code: `tags := []int{1} options := []mujica.SelectOption[int]{ {Value: 1, Label: "Rose"}, {Value: 2, Label: "Lily"}, {Value: 3, Label: "Iris"}, } view := func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) mujica.MultiSelect(c, &tags, options, mujica.MultiSelectOptions{Max: 2, Label: "Flowers"}) }`, Keys: "Enter, Space or Down open with the search focused; Down enters the list; Up/Down move; Space checks; Escape closes; Backspace on the field removes the last tag."}, {ID: "038", Name: "Combobox", ZhName: "组合框", Category: choiceCategory, Demo: comboboxDemo, Code: `font := "" fonts := []string{"Garamond", "Georgia", "Didot"} view := func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) r := mujica.Combobox(c, &font, fonts, mujica.ComboboxOptions{AllowCustom: true, Label: "Font"}) if r.QueryChanged() { log.Println("search:", r.Query()) } }`, Keys: "Typing filters; Down opens or moves; Up moves; Enter chooses; Escape closes; Tab leaves (keeping typed text with AllowCustom)."}, {ID: "039", Name: "Cascader", ZhName: "级联选择", Category: choiceCategory, Demo: cascaderDemo, Code: `var path []string level := func(p []string) mujica.CascaderLevel { if len(p) == 0 { return mujica.CascaderLevel{Nodes: []mujica.CascaderNode{{Value: "fr", Label: "France"}}} } return mujica.CascaderLevel{Nodes: []mujica.CascaderNode{{Value: "paris", Label: "Paris", Leaf: true}}} } view := func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) mujica.Cascader(c, &path, mujica.CascaderOptions{Level: level, Label: "City"}) }`, Keys: "Enter, Space or Down open; Up/Down move in a column; Right opens a branch; Left goes back; Enter chooses; Escape closes."}, {ID: "040", Name: "TreeSelect", ZhName: "树选择", Category: choiceCategory, Demo: treeSelectDemo, Code: `var roles []string nodes := []mujica.TreeSelectNode{ {ID: "court", Label: "Court", Children: []mujica.TreeSelectNode{ {ID: "king", Label: "King"}, {ID: "queen", Label: "Queen"}, }}, } view := func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) mujica.TreeSelect(c, &roles, nodes, mujica.TreeSelectOptions{Multiple: true, Label: "Roles"}) }`, Keys: "Enter, Space or Down open with the search focused; Down enters the tree; Up/Down move; Right expands or enters; Left collapses or goes to the parent; Space checks; Escape closes."}, } // choiceDemo lays out a demo: parameter controls, then the samples. func choiceDemo(c *ui.Context, controls, samples func()) { k := mujica.Tokens(c) ui.Column(c).Gap(20).AlignItems(ui.Start).Children(func() { ui.Text(c, "Parameters").Font(theme.SerifFont).FontSize(mujica.FontSize(c, theme.H3Size)).TextColor(k.Text) ui.Row(c).Gap(20).Wrap().AlignItems(ui.Center).Children(controls) ui.Box(c).Height(1).FillWidth().Background(k.Ornament) ui.Text(c, "States").Font(theme.SerifFont).FontSize(mujica.FontSize(c, theme.H3Size)).TextColor(k.Text) ui.Column(c).Gap(16).AlignItems(ui.Start).Children(samples) }) } // choiceFlag is a parameter switch that logs its change. func choiceFlag(c *ui.Context, on *bool, label string, log func(string)) { if mujica.Switch(c, on, label, mujica.SwitchOptions{}).Changed() { log(fmt.Sprintf("%s = %v", label, *on)) } } // choiceLabeled shows a caption above a sample. func choiceLabeled(c *ui.Context, caption string, fn func()) { ui.Column(c).Gap(6).Children(func() { ui.Text(c, caption).FontSize(mujica.FontSize(c, theme.CaptionSize)).TextColor(mujica.Tokens(c).TextMuted) fn() }) } func switchDemo(c *ui.Context, log func(string)) { s := *ui.Local(c.Root(), "031", func() *struct{ wifi, disabled, loading, pending bool } { return &struct{ wifi, disabled, loading, pending bool }{wifi: true} }) choiceDemo(c, func() { choiceFlag(c, &s.disabled, "Disabled", log) choiceFlag(c, &s.loading, "Loading", log) }, func() { if mujica.Switch(c, &s.wifi, "Wi-Fi", mujica.SwitchOptions{Disabled: s.disabled, Loading: s.loading}).Changed() { log(fmt.Sprintf("Wi-Fi changed: %v", s.wifi)) } // A caller applying the change: loading for a moment, then done. at := ui.Local(c.Root(), "031-at", func() time.Time { return time.Time{} }) if mujica.Switch(c, &s.pending, "Backup (saves for 1 s)", mujica.SwitchOptions{Loading: !at.IsZero()}).Changed() { *at = c.Now() log(fmt.Sprintf("Backup requested: %v", s.pending)) } if !at.IsZero() { if c.Now().Sub(*at) >= time.Second { *at = time.Time{} log("Backup saved") } else { c.After(time.Second - c.Now().Sub(*at)) } } off, on := false, true ui.Row(c).Gap(24).Children(func() { mujica.Switch(c, &off, "Disabled off", mujica.SwitchOptions{Disabled: true}) mujica.Switch(c, &on, "Disabled on", mujica.SwitchOptions{Disabled: true}) }) }) } func toggleDemo(c *ui.Context, log func(string)) { s := *ui.Local(c.Root(), "032", func() *struct{ bold, italic, underline, disabled bool } { return &struct{ bold, italic, underline, disabled bool }{bold: true} }) choiceDemo(c, func() { choiceFlag(c, &s.disabled, "Disabled", log) }, func() { ui.Row(c).Gap(8).Children(func() { for _, t := range []struct { on *bool label, ico string }{{&s.bold, "Bold", "bold"}, {&s.italic, "Italic", "italic"}, {&s.underline, "Underline", "underline"}} { if mujica.Toggle(c, t.on, "", mujica.ToggleOptions{Icon: icons.Must(t.ico), Label: t.label, Disabled: s.disabled}).Changed() { log(fmt.Sprintf("%s: %v", t.label, *t.on)) } } }) if mujica.Toggle(c, &s.bold, "Bold text", mujica.ToggleOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("Bold: %v", s.bold)) } }) } func toggleGroupDemo(c *ui.Context, log func(string)) { type state struct { value []string multiple, allowEmpty, vertical, disabled bool } s := *ui.Local(c.Root(), "033", func() *state { return &state{value: []string{"left"}} }) items := []mujica.ToggleItem[string]{ {Value: "left", Label: "Left"}, {Value: "center", Label: "Center"}, {Value: "right", Label: "Right"}, {Value: "justify", Label: "Justify", Disabled: true}, } choiceDemo(c, func() { if mujica.Switch(c, &s.multiple, "Multiple", mujica.SwitchOptions{}).Changed() { s.value = s.value[:min(len(s.value), 1)] log(fmt.Sprintf("Multiple = %v", s.multiple)) } choiceFlag(c, &s.allowEmpty, "AllowEmpty", log) choiceFlag(c, &s.vertical, "Vertical", log) choiceFlag(c, &s.disabled, "Disabled", log) }, func() { opts := mujica.ToggleGroupOptions{Multiple: s.multiple, AllowEmpty: s.allowEmpty, Vertical: s.vertical, Disabled: s.disabled, Label: "Align"} if mujica.ToggleGroup(c, &s.value, items, opts).Changed() { log(fmt.Sprintf("value: %v", s.value)) } ui.Text(c, fmt.Sprintf("Value: %v", s.value)).TextColor(mujica.Tokens(c).TextMuted) }) } func sliderDemo(c *ui.Context, log func(string)) { type state struct { value, step float64 marks, showValue, disabled bool } s := *ui.Local(c.Root(), "034", func() *state { return &state{value: 40, step: 5, showValue: true} }) choiceDemo(c, func() { choiceFlag(c, &s.marks, "Marks", log) choiceFlag(c, &s.showValue, "ShowValue", log) choiceFlag(c, &s.disabled, "Disabled", log) choiceLabeled(c, fmt.Sprintf("Step %v", s.step), func() { if mujica.Slider(c, &s.step, mujica.SliderOptions{Max: 25, Step: 5, Label: "Step"}).Changed() { log(fmt.Sprintf("Step = %v", s.step)) } }) }, func() { var marks []mujica.SliderMark if s.marks { for v := 0.0; v <= 100; v += 25 { marks = append(marks, mujica.SliderMark{Value: v, Label: fmt.Sprint(v)}) } } ui.Column(c).Width(360).Children(func() { opts := mujica.SliderOptions{Step: s.step, Marks: marks, ShowValue: s.showValue, Disabled: s.disabled, Label: "Volume"} if mujica.Slider(c, &s.value, opts).Changed() { log(fmt.Sprintf("Volume: %v", s.value)) } }) ui.Text(c, fmt.Sprintf("Value: %v", s.value)).TextColor(mujica.Tokens(c).TextMuted) }) } func rangeSliderDemo(c *ui.Context, log func(string)) { type state struct { low, high float64 showValue, disabled, marks bool } s := *ui.Local(c.Root(), "035", func() *state { return &state{low: 20, high: 80, showValue: true} }) choiceDemo(c, func() { choiceFlag(c, &s.marks, "Marks", log) choiceFlag(c, &s.showValue, "ShowValue", log) choiceFlag(c, &s.disabled, "Disabled", log) }, func() { var marks []mujica.SliderMark if s.marks { marks = []mujica.SliderMark{{Value: 0, Label: "£0"}, {Value: 500, Label: "£500"}, {Value: 1000, Label: "£1000"}} } ui.Column(c).Width(360).Children(func() { opts := mujica.RangeSliderOptions{Max: 1000, Step: 50, Marks: marks, ShowValue: s.showValue, Disabled: s.disabled, Label: "Price", Format: func(v float64) string { return fmt.Sprintf("£%.0f", v) }} if mujica.RangeSlider(c, &s.low, &s.high, opts).Changed() { log(fmt.Sprintf("Price: %v–%v", s.low, s.high)) } }) ui.Text(c, fmt.Sprintf("Range: £%.0f – £%.0f", s.low, s.high)).TextColor(mujica.Tokens(c).TextMuted) }) } // choiceDelay is how long the demos' pretend server takes. const choiceDelay = 700 * time.Millisecond // choiceDue reports whether a request started at "at" has finished, asking for a frame when it will. func choiceDue(c *ui.Context, at time.Time) bool { if left := choiceDelay - c.Now().Sub(at); left > 0 { c.After(left) return false } return true } var choiceFruits = []mujica.SelectOption[string]{ {Value: "apple", Label: "Apple"}, {Value: "banana", Label: "Banana"}, {Value: "blueberry", Label: "Blueberry"}, {Value: "cherry", Label: "Cherry", Disabled: true}, {Value: "date", Label: "Date"}, {Value: "fig", Label: "Fig"}, } func selectDemo(c *ui.Context, log func(string)) { type state struct { value, picked string clearable, disabled, noData bool } s := *ui.Local(c.Root(), "036", func() *state { return &state{value: "banana", clearable: true} }) choiceDemo(c, func() { choiceFlag(c, &s.clearable, "Clearable", log) choiceFlag(c, &s.disabled, "Disabled", log) choiceFlag(c, &s.noData, "No options", log) }, func() { options := choiceFruits if s.noData { options = nil } choiceLabeled(c, "Fruit", func() { if mujica.Select(c, &s.value, options, mujica.SelectOptions{Clearable: s.clearable, Disabled: s.disabled, Label: "Fruit"}).Changed() { log(fmt.Sprintf("Fruit: %q", s.value)) } }) choiceLabeled(c, "Placeholder", func() { if mujica.Select(c, &s.picked, choiceFruits, mujica.SelectOptions{Placeholder: "Pick a fruit", Clearable: true}).Changed() { log(fmt.Sprintf("Picked: %q", s.picked)) } }) }) } func multiSelectDemo(c *ui.Context, log func(string)) { type state struct { values []string limit, disable bool } s := *ui.Local(c.Root(), "037", func() *state { return &state{values: []string{"apple", "date"}} }) choiceDemo(c, func() { choiceFlag(c, &s.limit, "Max 3", log) choiceFlag(c, &s.disable, "Disabled", log) }, func() { limit := 0 if s.limit { limit = 3 } choiceLabeled(c, "Fruits", func() { r := mujica.MultiSelect(c, &s.values, choiceFruits, mujica.MultiSelectOptions{Max: limit, MaxTags: 2, Disabled: s.disable, Label: "Fruits"}) r.Element.Width(320) if r.Changed() { log(fmt.Sprintf("Fruits: %v", s.values)) } }) }) } var choiceFonts = []string{"Baskerville", "Bodoni", "Caslon", "Didot", "Garamond", "Georgia", "Jenson", "Palatino"} func comboboxDemo(c *ui.Context, log func(string)) { type state struct { value, query, asked string custom, remote bool at time.Time results []string } s := *ui.Local(c.Root(), "038", func() *state { return &state{custom: true} }) choiceDemo(c, func() { choiceFlag(c, &s.custom, "AllowCustom", log) choiceFlag(c, &s.remote, "Remote results", log) }, func() { options, loading := choiceFonts, false if s.remote { // The caller fetches results for the query and hands them back. if s.asked != s.query && choiceDue(c, s.at) { s.asked = s.query s.results = nil for _, f := range choiceFonts { if strings.Contains(strings.ToLower(f), strings.ToLower(s.query)) { s.results = append(s.results, f) } } log(fmt.Sprintf("results for %q: %d", s.query, len(s.results))) } options, loading = s.results, s.asked != s.query } choiceLabeled(c, "Typeface", func() { r := mujica.Combobox(c, &s.value, options, mujica.ComboboxOptions{AllowCustom: s.custom, ExternalFilter: s.remote, Loading: loading, Label: "Typeface"}) if r.QueryChanged() { s.query, s.at = r.Query(), c.Now() log(fmt.Sprintf("query: %q", s.query)) } if r.Changed() { log(fmt.Sprintf("Typeface: %q", s.value)) } }) disabled := "Didot" choiceLabeled(c, "Disabled", func() { mujica.Combobox(c, &disabled, choiceFonts, mujica.ComboboxOptions{Disabled: true}) }) }) } // choiceAtlas is the cascader demo's pretend server data. var choiceAtlas = map[string][]mujica.CascaderNode{ "": {{Value: "eu", Label: "Europe"}, {Value: "as", Label: "Asia"}, {Value: "aq", Label: "Antarctica", Disabled: true}}, "eu": {{Value: "fr", Label: "France"}, {Value: "it", Label: "Italy"}}, "as": {{Value: "jp", Label: "Japan"}, {Value: "cn", Label: "China"}}, "eu/fr": {{Value: "paris", Label: "Paris", Leaf: true}, {Value: "lyon", Label: "Lyon", Leaf: true}}, "eu/it": {{Value: "rome", Label: "Rome", Leaf: true}}, "as/jp": {{Value: "tokyo", Label: "Tokyo", Leaf: true}, {Value: "kyoto", Label: "Kyoto", Leaf: true}}, "as/cn": {{Value: "beijing", Label: "Beijing", Leaf: true}}, } func cascaderDemo(c *ui.Context, log func(string)) { type state struct { path []string levels map[string]mujica.CascaderLevel pending map[string]time.Time anyLevel, fail, ds bool } s := *ui.Local(c.Root(), "039", func() *state { return &state{levels: map[string]mujica.CascaderLevel{"": {Nodes: choiceAtlas[""]}}, pending: map[string]time.Time{}} }) for key, at := range s.pending { if choiceDue(c, at) { delete(s.pending, key) if s.fail { s.levels[key] = mujica.CascaderLevel{Status: mujica.CascaderFailed, Err: errors.New("server busy")} log(fmt.Sprintf("load %q failed", key)) continue } s.levels[key] = mujica.CascaderLevel{Nodes: choiceAtlas[key]} log(fmt.Sprintf("loaded %q", key)) } } level := func(p []string) mujica.CascaderLevel { if lv, ok := s.levels[strings.Join(p, "/")]; ok { return lv } return mujica.CascaderLevel{Status: mujica.CascaderIdle} } load := func(p []string) { key := strings.Join(p, "/") s.levels[key] = mujica.CascaderLevel{Status: mujica.CascaderLoading} s.pending[key] = c.Now() log(fmt.Sprintf("load %q", key)) } choiceDemo(c, func() { choiceFlag(c, &s.anyLevel, "AnyLevel", log) choiceFlag(c, &s.fail, "Fail loads", log) choiceFlag(c, &s.ds, "Disabled", log) }, func() { choiceLabeled(c, "City (children load on demand)", func() { opts := mujica.CascaderOptions{Level: level, Load: load, AnyLevel: s.anyLevel, Disabled: s.ds, Label: "City"} r := mujica.Cascader(c, &s.path, opts) r.Element.Width(320) if r.Changed() { log(fmt.Sprintf("path: %v", s.path)) } }) }) } var choiceRoles = []mujica.TreeSelectNode{ {ID: "court", Label: "Court", Children: []mujica.TreeSelectNode{ {ID: "king", Label: "King"}, {ID: "queen", Label: "Queen"}, {ID: "jester", Label: "Jester", Disabled: true}, {ID: "guard", Label: "Royal guard", Children: []mujica.TreeSelectNode{{ID: "captain", Label: "Captain"}, {ID: "archer", Label: "Archer"}}}, }}, {ID: "church", Label: "Church", Children: []mujica.TreeSelectNode{{ID: "bishop", Label: "Bishop"}, {ID: "monk", Label: "Monk"}}}, {ID: "merchant", Label: "Merchant"}, } func treeSelectDemo(c *ui.Context, log func(string)) { type state struct { sel []string multiple, independent, disabled bool } s := *ui.Local(c.Root(), "040", func() *state { return &state{sel: []string{"queen", "bishop"}, multiple: true} }) choiceDemo(c, func() { if mujica.Switch(c, &s.multiple, "Multiple", mujica.SwitchOptions{}).Changed() { s.sel = s.sel[:min(len(s.sel), 1)] log(fmt.Sprintf("Multiple = %v", s.multiple)) } choiceFlag(c, &s.independent, "Independent rule", log) choiceFlag(c, &s.disabled, "Disabled", log) }, func() { rule := mujica.TreeSelectCascade if s.independent { rule = mujica.TreeSelectIndependent } choiceLabeled(c, "Roles", func() { opts := mujica.TreeSelectOptions{Multiple: s.multiple, Rule: rule, MaxTags: 2, Disabled: s.disabled, Label: "Roles"} r := mujica.TreeSelect(c, &s.sel, choiceRoles, opts) r.Element.Width(320) if r.Changed() { log(fmt.Sprintf("Roles: %v", s.sel)) } }) }) }