Files
MujicaUI/examples/gallery/pages_layout.go
T

498 lines
20 KiB
Go

package main
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 and the +N button; Enter or Space activates them. +N opens the folded items, Tab moves through them, Escape closes the list. 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 {
return ui.Column(c).Height(h).Border(1, mujica.Tokens(c).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
if 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")
})
})
}