Files
8bitMyGoUI/gallery/demos_navigation.go
ZacharyZhang-NY b4b4d67b92 Audit keyboard, focus, veils, motion and clocks across all 500
A cross-cutting pass, each fix with a test that failed first.

Veils: MyGo still hands a key to a base that turned off this frame,
so every element key now goes through pixel.Shortcut, which an
archtest enforces. Input handlers record and the build applies them
only while enabled; the editor turns away a veil-frame letter or
copy; value bases get a copy and keep only enabled turns. Popups
give a held focus back to their opener when the veil lifts.

Motion: S5 is enforced at the clock. Every Tick, Since, Step,
Toward and Shot runs at 4-15 fps; blinks keep 1 Hz on 4 fps steps,
fast animations step at 15, and drawn time is quantized to frames.

Clocks stepping back: Since holds at zero without overflow, and
k.Elapsed moves a stored start to now, so nothing goes negative,
runs backward or indexes out of range.

Also: logs count typed text instead of holding it, gallery names
use glyphs the font has, removable rows are keyed, components take
their own Options (archtest), and sweeps over all 508 demos check
veiled keys, clicks, wheels, the clock stepping back and names.
2026-10-12 16:03:04 -04:00

302 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gallery
import (
"fmt"
"slices"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/navigation"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
var navigationEntries = []Entry{
{131, "Tabs", "标签页", "navigation", "Folder tabs; the chosen one joins its panel. Arrows move.", demoTabs},
{132, "VerticalTabs", "竖排标签", "navigation", "Tabs down the left edge, for settings screens.", demoVerticalTabs},
{133, "Sidebar", "侧边栏", "navigation", "Sections of items with icons and badges. Up and Down choose; one Tab stop.", demoSidebar},
{134, "Breadcrumbs", "面包屑", "navigation", "Where you are, split by pointers. Every part but the last walks back.", demoBreadcrumbs},
{135, "CommandMenu", "指令菜单", "navigation", "FIGHT, MAGIC, ITEM, RUN, with a bobbing ▶. Enter picks; Escape backs out.", demoCommandMenu},
{136, "MenuBar", "菜单栏", "navigation", "A bar of menus. Click one, then the arrows go across and down.", demoMenuBar},
{137, "DropdownMenu", "下拉菜单", "navigation", "A button with a menu: icons, rules and shortcut names.", demoDropdownMenu},
{138, "ContextMenu", "右键菜单", "navigation", "Right click, or the menu key, opens a menu at the pointer.", demoContextMenu},
{139, "Pagination", "分页", "navigation", "◀ 1 … 4 5 6 … 12 ▶. The current page is pressed in.", demoPagination},
{140, "PageDots", "页码点", "navigation", "A dot per page; click one, or use the arrows.", demoPageDots},
{141, "Steps", "关卡阶段", "navigation", "Flags along a trail: cleared, waving where you are, locked ahead.", demoSteps},
{142, "NavRail", "导航栏", "navigation", "A rail of icons. Hover or focus one and it says its name.", demoNavRail},
{143, "BottomNav", "底部导航", "navigation", "Icons over labels along the bottom, for small screens.", demoBottomNav},
{144, "CommandPalette", "命令面板", "navigation", "Cmd+K, type a few letters, press Enter.", demoCommandPalette},
{145, "BackForward", "前进后退", "navigation", "◀ and ▶ walking a router's history.", demoBackForward},
{146, "TableOfContents", "目录", "navigation", "A manual's sections; ▶ marks where you are.", demoTableOfContents},
{147, "LevelSelect", "选关", "navigation", "Stages in a grid: locked, open, or cleared with stars.", demoLevelSelect},
{148, "WorldMap", "世界地图", "navigation", "Places on dotted trails. The arrows walk the hero; Enter goes in.", demoWorldMap},
{149, "Toolbar", "工具栏", "navigation", "Tool buttons as one Tab stop; arrows move along.", demoToolbar},
{150, "DocumentTabs", "文档标签", "navigation", "Tabs you can close with × or Delete.", demoDocumentTabs},
}
type navDemo struct {
tab, vtab, side, toc, rail, bottom, level, node, doc int
page, dot int
crumbs, docs []string
palette bool
router *ui.Router
pen, fill bool
newDocs int
}
func navState(c *ui.Context) *navDemo {
return ui.Local(c.Root(), demoKey("navigation"), func() navDemo {
r := ui.NewRouter("/town")
r.Push("/field")
return navDemo{page: 5, dot: 1, level: 2, crumbs: []string{"World", "Cave", "B1", "B2"},
docs: []string{"map.txt", "save1.dat", "notes.md"}, router: r, pen: true}
})
}
func demoTabs(c *ui.Context, log func(string)) {
s := navState(c)
names := []string{"ITEMS", "MAGIC", "STATUS"}
body := []string{"Potion ×3, Ether ×1, Tent ×1", "Fire, Cure, Haste", "HP 42/42 MP 12/18 LV 7"}
if navigation.Tabs(c, "Menu", names, &s.tab, navigation.TabsOptions{}, func() {
pixel.Text(c, body[s.tab], pixel.TextOptions{})
}).Changed {
log(names[s.tab])
}
}
func demoVerticalTabs(c *ui.Context, log func(string)) {
s := navState(c)
names := []string{"Video", "Audio", "Controls"}
if navigation.VerticalTabs(c, "Settings", names, &s.vtab, navigation.VerticalTabsOptions{}, func() {
pixel.Text(c, names[s.vtab]+" settings go here.", pixel.TextOptions{Slot: pixel.Faint})
}).Changed {
log(names[s.vtab])
}
}
func demoSidebar(c *ui.Context, log func(string)) {
s := navState(c)
if navigation.Sidebar(c, "Places", []navigation.SidebarSection{
{Title: "WORLD", Items: []navigation.SidebarItem{{Icon: icon.Home, Label: "Town"}, {Icon: icon.Map, Label: "Field"}, {Icon: icon.Skull, Label: "Dungeon"}}},
{Title: "INBOX", Items: []navigation.SidebarItem{{Icon: icon.Mail, Label: "Letters", Badge: "3"}}},
{Title: "SYSTEM", Items: []navigation.SidebarItem{{Icon: icon.Settings, Label: "Options"}}},
}, &s.side, navigation.SidebarOptions{}).Changed {
log(fmt.Sprintf("item %d", s.side))
}
}
func demoBreadcrumbs(c *ui.Context, log func(string)) {
s := navState(c)
if r := navigation.Breadcrumbs(c, "Path", s.crumbs, navigation.BreadcrumbsOptions{}); r.Clicked >= 0 {
s.crumbs = s.crumbs[:r.Clicked+1]
log("back to " + s.crumbs[r.Clicked])
}
if len(s.crumbs) < 4 && input.Button(c, "GO DEEPER", input.ButtonOptions{Size: input.Small}).Clicked {
s.crumbs = append(s.crumbs, fmt.Sprintf("B%d", len(s.crumbs)-1))
log("deeper")
}
}
func demoCommandMenu(c *ui.Context, log func(string)) {
items := []navigation.MenuItem{{Label: "FIGHT"}, {Label: "MAGIC", Disabled: true}, {Label: "ITEM"}, {Label: "RUN"}}
r := navigation.CommandMenu(c, "Command", items, navigation.CommandMenuOptions{})
if r.Chosen >= 0 {
log(items[r.Chosen].Label)
}
if r.Canceled {
log("back")
}
}
func demoMenuBar(c *ui.Context, log func(string)) {
menus := []navigation.Menu{
{Title: "File", Items: []navigation.MenuItem{{Label: "New game"}, {Label: "Load", Icon: icon.FolderOpen}, {Label: "Save", Icon: icon.Save, Keys: "CTRL+S"}, {Separator: true}, {Label: "Quit"}}},
{Title: "Edit", Items: []navigation.MenuItem{{Label: "Undo", Keys: "CTRL+Z"}, {Label: "Redo", Keys: "CTRL+Y", Disabled: true}}},
{Title: "Help", Items: []navigation.MenuItem{{Label: "Controls"}, {Label: "About"}}},
}
if r := navigation.MenuBar(c, "Main menu", menus, navigation.MenuBarOptions{}); r.Item >= 0 {
log(menus[r.Menu].Title + " › " + menus[r.Menu].Items[r.Item].Label)
}
}
func demoDropdownMenu(c *ui.Context, log func(string)) {
items := []navigation.MenuItem{{Label: "Swap", Icon: icon.Refresh}, {Label: "Equip", Icon: icon.Sword}, {Label: "Status", Icon: icon.User}, {Separator: true}, {Label: "Dismiss", Icon: icon.Trash, Disabled: true}}
if r := navigation.DropdownMenu(c, "Party", items, navigation.DropdownMenuOptions{}); r.Chosen >= 0 {
log(items[r.Chosen].Label)
}
}
func demoContextMenu(c *ui.Context, log func(string)) {
k := pixel.K(c)
items := []navigation.MenuItem{{Label: "Inspect", Icon: icon.Search}, {Label: "Pet", Icon: icon.Heart}, {Separator: true}, {Label: "Throw", Icon: icon.Bolt}}
if r := navigation.ContextMenu(c, "Slime", items, navigation.ContextMenuOptions{}, func() {
ui.Box(c).Size(k.D(160), k.D(40)).Label("Right click the slime").Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Dither(0, 0, g.W, g.H, pixel.Face, pixel.Good, 2)
g.Text(8, 12, "Right click the slime", pixel.Faint, 1)
})
}); r.Chosen >= 0 {
log(items[r.Chosen].Label)
}
}
func demoPagination(c *ui.Context, log func(string)) {
s := navState(c)
if navigation.Pagination(c, "Pages", &s.page, 12, navigation.PaginationOptions{}).Changed {
log(fmt.Sprintf("page %d", s.page))
}
}
func demoPageDots(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := navState(c)
tips := []string{"Jump with Space.", "Hold Shift to run.", "Talk to everyone.", "Check the pots.", "Save often."}
ui.Column(c).Gap(k.D(4)).Children(func() {
pixel.Text(c, tips[s.dot-1], pixel.TextOptions{})
if navigation.PageDots(c, "Tips", &s.dot, len(tips), navigation.PageDotsOptions{}).Changed {
log(fmt.Sprintf("tip %d", s.dot))
}
})
}
func demoSteps(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := navState(c)
stops := []string{"Gate", "Forest", "Cave", "Peak", "Castle"}
ui.Column(c).Gap(k.D(6)).Children(func() {
navigation.Steps(c, stops, s.level, navigation.StepsOptions{})
ui.Row(c).Gap(k.D(4)).Children(func() {
if input.Button(c, "BACK", input.ButtonOptions{Size: input.Small, Disabled: s.level == 0}).Clicked {
s.level--
log("at " + stops[s.level])
}
if input.Button(c, "ONWARD", input.ButtonOptions{Size: input.Small, Disabled: s.level == len(stops)-1}).Clicked {
s.level++
log("at " + stops[s.level])
}
})
})
}
func demoNavRail(c *ui.Context, log func(string)) {
s := navState(c)
items := []navigation.NavItem{{Icon: icon.Home, Label: "Home"}, {Icon: icon.Map, Label: "Map"}, {Icon: icon.Gem, Label: "Inventory"}, {Icon: icon.Settings, Label: "Options"}}
if navigation.NavRail(c, "Rail", items, &s.rail, navigation.NavRailOptions{}).Changed {
log(items[s.rail].Label)
}
}
func demoBottomNav(c *ui.Context, log func(string)) {
s := navState(c)
items := []navigation.NavItem{{Icon: icon.Home, Label: "Home"}, {Icon: icon.Flag, Label: "Quests"}, {Icon: icon.Chest, Label: "Bag"}, {Icon: icon.User, Label: "Me"}}
if navigation.BottomNav(c, "Bottom", items, &s.bottom, navigation.BottomNavOptions{}).Changed {
log(items[s.bottom].Label)
}
}
func demoCommandPalette(c *ui.Context, log func(string)) {
s := navState(c)
cmds := []navigation.MenuItem{{Label: "Save game", Keys: "CTRL+S"}, {Label: "Load game"}, {Label: "Toggle music"}, {Label: "Change palette", Keys: "P"}, {Label: "Rename hero"}, {Label: "Delete save", Disabled: true}}
if input.Button(c, "OPEN (CMD+K)", input.ButtonOptions{Size: input.Small}).Clicked {
s.palette = true
log("palette open")
}
if r := navigation.CommandPalette(c, "Commands", cmds, &s.palette, navigation.CommandPaletteOptions{}); r.Ran >= 0 {
log("ran " + cmds[r.Ran].Label)
}
}
func demoBackForward(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := navState(c)
ui.Row(c).Gap(k.D(6)).AlignItems(ui.Center).Children(func() {
if navigation.BackForward(c, s.router, navigation.BackForwardOptions{}).Moved {
log("now at " + s.router.Path())
}
pixel.Text(c, s.router.Path(), pixel.TextOptions{Slot: pixel.Accent})
for _, place := range []string{"/castle", "/shop"} {
if input.Button(c, place, input.ButtonOptions{Size: input.Small}).Clicked {
s.router.Push(place)
log("went to " + place)
}
}
})
}
func demoTableOfContents(c *ui.Context, log func(string)) {
s := navState(c)
items := []navigation.TocItem{{Title: "Getting started"}, {Title: "Controls", Level: 1}, {Title: "Saving", Level: 1}, {Title: "Combat"}, {Title: "Magic", Level: 1}, {Title: "Credits"}}
if navigation.TableOfContents(c, "Manual", items, &s.toc, navigation.TableOfContentsOptions{}).Changed {
log(items[s.toc].Title)
}
}
func demoLevelSelect(c *ui.Context, log func(string)) {
s := navState(c)
stages := []navigation.Stage{
{Name: "1-1", Cleared: true, Stars: 3}, {Name: "1-2", Cleared: true, Stars: 2}, {Name: "1-3", Cleared: true, Stars: 1},
{Name: "1-4"}, {Name: "1-5", Locked: true}, {Name: "2-1", Locked: true}, {Name: "2-2", Locked: true},
{Name: "2-3", Locked: true}, {Name: "2-4", Locked: true}, {Name: "BOSS", Locked: true},
}
r := navigation.LevelSelect(c, "Stages", stages, &s.level, navigation.LevelSelectOptions{})
if r.Changed {
log("on " + stages[s.level].Name)
}
if r.Chosen {
log("enter " + stages[s.level].Name)
}
}
func demoWorldMap(c *ui.Context, log func(string)) {
s := navState(c)
nodes := []navigation.MapNode{{Name: "Home", X: 24, Y: 100}, {Name: "Woods", X: 84, Y: 70}, {Name: "Lake", X: 150, Y: 100}, {Name: "Tower", X: 120, Y: 28}, {Name: "Lair", X: 206, Y: 40, Locked: true}}
paths := [][2]int{{0, 1}, {1, 2}, {1, 3}, {3, 4}, {2, 4}}
r := navigation.WorldMap(c, "World", nodes, paths, &s.node, navigation.WorldMapOptions{})
if r.Changed {
log("walked to " + nodes[s.node].Name)
}
if r.Entered {
log("entered " + nodes[s.node].Name)
}
}
func demoToolbar(c *ui.Context, log func(string)) {
s := navState(c)
navigation.Toolbar(c, "Tools", navigation.ToolbarOptions{}, func() {
if input.Toggle(c, "", &s.pen, input.ToggleOptions{Icon: icon.Edit, Label: "Pen"}).Changed {
log(fmt.Sprintf("pen %v", s.pen))
}
if input.Toggle(c, "", &s.fill, input.ToggleOptions{Icon: icon.Drop, Label: "Fill"}).Changed {
log(fmt.Sprintf("fill %v", s.fill))
}
if input.IconButton(c, icon.Prev, "Undo", input.IconButtonOptions{}).Clicked {
log("undo")
}
if input.IconButton(c, icon.Trash, "Clear", input.IconButtonOptions{Variant: input.Danger}).Clicked {
log("clear")
}
})
}
func demoDocumentTabs(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := navState(c)
ui.Column(c).Gap(k.D(4)).Children(func() {
r := navigation.DocumentTabs(c, "Files", s.docs, &s.doc, navigation.DocumentTabsOptions{})
if r.Changed {
log("open " + s.docs[s.doc])
}
if r.Closed >= 0 {
log("closed " + s.docs[r.Closed])
s.docs = slices.Delete(s.docs, r.Closed, r.Closed+1)
s.doc = max(0, min(s.doc, len(s.docs)-1))
}
if input.Button(c, "NEW TAB", input.ButtonOptions{Size: input.Small}).Clicked {
s.newDocs++
s.docs = append(s.docs, fmt.Sprintf("untitled%d.txt", s.newDocs))
log("new tab")
}
})
}