One component per file; GAME OVER offers CONTINUE

PRD G1 asks for one component per file. Eight files held two or
three; their eleven extra components move to files of their own,
helpers stay where both sides use them, and an archtest now fails
any file that holds two.

PRD 4.4 and the request say an error always offers CONTINUE.
ErrorState's button read RETRY; it now reads CONTINUE, under the
CONTINUE? countdown, on the crash screen and the lost page alike.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-12 16:50:17 -04:00
1 parent edee517e8d
commit 1f3d38110e
25 files changed
+439 -359

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
package navigation
import (
"log/slog"
"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/pixel"
)
type BackForwardOptions struct {
Disabled bool
}
type BackForwardResult struct {
Element *ui.Element
// Moved reports a step through the history.
Moved bool
}
// BackForward walks a router's history with ◀ and ▶.
func BackForward(c *ui.Context, r *ui.Router, o BackForwardOptions) BackForwardResult {
k := pixel.K(c)
res := BackForwardResult{}
res.Element = ui.Row(c).Gap(k.D(2)).FocusGroup(ui.Horizontal).Role(ui.RoleToolbar).Label(k.Msg("history")).Children(func() {
back := input.IconButtonOptions{Size: input.Small, Disabled: o.Disabled || !r.CanGoBack()}
if input.IconButton(c, icon.ChevronLeft, k.Msg("back"), back).Clicked {
r.Back()
res.Moved = true
}
forth := input.IconButtonOptions{Size: input.Small, Disabled: o.Disabled || !r.CanGoForward()}
if input.IconButton(c, icon.ChevronRight, k.Msg("forward"), forth).Clicked {
r.Forward()
res.Moved = true
}
})
if res.Moved {
slog.Debug("8bit: history", "path", r.Path())
}
return res
}
+55
View File
@@ -0,0 +1,55 @@
package navigation
import (
"log/slog"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
type BottomNavOptions struct {
Disabled bool
}
type BottomNavResult struct {
Element *ui.Element
Changed bool
}
// BottomNav is a bar of labeled icons for narrow screens.
func BottomNav(c *ui.Context, text string, items []NavItem, selected *int, o BottomNavOptions) BottomNavResult {
k := pixel.K(c)
res := BottomNavResult{}
// The bases turn before they learn they are off.
v := *selected
res.Element = ui.RadioGroup(c, func() {
for i, it := range items {
shell, _, st := pixel.Shell(c, func() *ui.Element {
return ui.RadioBase(c, &v, i).Label(it.Label).Disabled(o.Disabled)
})
w := max(pixel.Measure(it.Label), 12) + 12
shell.Size(k.D(w), k.D(16+pixel.LineHeight)).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
ink := pixel.Faint
if i == *selected {
ink = pixel.Accent
g.HLine(2, 0, g.W-4, pixel.Accent)
} else if st.Hover {
ink = pixel.Ink
}
g.Sprite((g.W-it.Icon.W)/2, 2, it.Icon, 0, ink, 1)
g.Text((g.W-pixel.Measure(it.Label))/2, 14, it.Label, ink, 1)
g.Finish(st)
})
}
}).Row().Gap(k.D(4)).Padding(k.D(2), k.D(6)).Label(text).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Rect(0, 0, g.W, g.H, pixel.Face)
g.HLine(0, 0, g.W, pixel.Edge)
})
if res.Changed = keep(c, v, selected, o.Disabled); res.Changed {
slog.Debug("8bit: bottom nav", "label", text, "selected", *selected)
}
return res
}
+63
View File
@@ -0,0 +1,63 @@
package navigation
import (
"fmt"
"log/slog"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
type PageDotsOptions struct {
Disabled bool
}
type PageDotsResult struct {
Element *ui.Element
Changed bool
}
// PageDots shows a dot per page; clicks jump, arrows move.
func PageDots(c *ui.Context, text string, page *int, total int, o PageDotsOptions) PageDotsResult {
k := pixel.K(c)
check(*page, total)
shell, base, st := pixel.Shell(c, func() *ui.Element {
return ui.Box(c).Focusable().Role(ui.RoleSlider).Label(text).Description(fmt.Sprintf("%d/%d", *page, total)).Disabled(o.Disabled)
})
old := *page
for _, m := range []struct {
key ui.Key
to func() int
}{
{ui.KeyLeft, func() int { return *page - 1 }}, {ui.KeyRight, func() int { return *page + 1 }},
{ui.KeyHome, func() int { return 1 }}, {ui.KeyEnd, func() int { return total }},
} {
if pixel.Shortcut(base, 0, m.key) {
*page = max(1, min(total, m.to()))
}
}
// Taking Enter and Space keeps keys from clicking.
pixel.Shortcut(base, 0, ui.KeyEnter)
pixel.Shortcut(base, 0, ui.KeySpace)
if base.Clicked() {
x, _, _ := base.PointerPosition()
*page = max(1, min(total, int(x/k.Dot())/8+1))
}
shell.Size(k.D(total*8), k.D(8)).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
for i := range total {
if i+1 == *page {
g.Rect(i*8+1, 1, 6, 6, pixel.Accent)
} else {
g.Rect(i*8+2, 2, 4, 4, pixel.Shade)
}
}
g.Finish(st)
})
res := PageDotsResult{Element: shell, Changed: *page != old}
if res.Changed {
slog.Debug("8bit: page dots", "label", text, "page", *page)
}
return res
}
-53
View File
@@ -84,56 +84,3 @@ func check(page, total int) {
panic(fmt.Sprintf("navigation: page %d of %d", page, total))
}
}
type PageDotsOptions struct {
Disabled bool
}
type PageDotsResult struct {
Element *ui.Element
Changed bool
}
// PageDots shows a dot per page; clicks jump, arrows move.
func PageDots(c *ui.Context, text string, page *int, total int, o PageDotsOptions) PageDotsResult {
k := pixel.K(c)
check(*page, total)
shell, base, st := pixel.Shell(c, func() *ui.Element {
return ui.Box(c).Focusable().Role(ui.RoleSlider).Label(text).Description(fmt.Sprintf("%d/%d", *page, total)).Disabled(o.Disabled)
})
old := *page
for _, m := range []struct {
key ui.Key
to func() int
}{
{ui.KeyLeft, func() int { return *page - 1 }}, {ui.KeyRight, func() int { return *page + 1 }},
{ui.KeyHome, func() int { return 1 }}, {ui.KeyEnd, func() int { return total }},
} {
if pixel.Shortcut(base, 0, m.key) {
*page = max(1, min(total, m.to()))
}
}
// Taking Enter and Space keeps keys from clicking.
pixel.Shortcut(base, 0, ui.KeyEnter)
pixel.Shortcut(base, 0, ui.KeySpace)
if base.Clicked() {
x, _, _ := base.PointerPosition()
*page = max(1, min(total, int(x/k.Dot())/8+1))
}
shell.Size(k.D(total*8), k.D(8)).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
for i := range total {
if i+1 == *page {
g.Rect(i*8+1, 1, 6, 6, pixel.Accent)
} else {
g.Rect(i*8+2, 2, 4, 4, pixel.Shade)
}
}
g.Finish(st)
})
res := PageDotsResult{Element: shell, Changed: *page != old}
if res.Changed {
slog.Debug("8bit: page dots", "label", text, "page", *page)
}
return res
}
-46
View File
@@ -73,49 +73,3 @@ func tip(c *ui.Context, k pixel.Kit, at ui.Rect, s string) {
})
})
}
type BottomNavOptions struct {
Disabled bool
}
type BottomNavResult struct {
Element *ui.Element
Changed bool
}
// BottomNav is a bar of labeled icons for narrow screens.
func BottomNav(c *ui.Context, text string, items []NavItem, selected *int, o BottomNavOptions) BottomNavResult {
k := pixel.K(c)
res := BottomNavResult{}
// The bases turn before they learn they are off.
v := *selected
res.Element = ui.RadioGroup(c, func() {
for i, it := range items {
shell, _, st := pixel.Shell(c, func() *ui.Element {
return ui.RadioBase(c, &v, i).Label(it.Label).Disabled(o.Disabled)
})
w := max(pixel.Measure(it.Label), 12) + 12
shell.Size(k.D(w), k.D(16+pixel.LineHeight)).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
ink := pixel.Faint
if i == *selected {
ink = pixel.Accent
g.HLine(2, 0, g.W-4, pixel.Accent)
} else if st.Hover {
ink = pixel.Ink
}
g.Sprite((g.W-it.Icon.W)/2, 2, it.Icon, 0, ink, 1)
g.Text((g.W-pixel.Measure(it.Label))/2, 14, it.Label, ink, 1)
g.Finish(st)
})
}
}).Row().Gap(k.D(4)).Padding(k.D(2), k.D(6)).Label(text).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Rect(0, 0, g.W, g.H, pixel.Face)
g.HLine(0, 0, g.W, pixel.Edge)
})
if res.Changed = keep(c, v, selected, o.Disabled); res.Changed {
slog.Debug("8bit: bottom nav", "label", text, "selected", *selected)
}
return res
}
-14
View File
@@ -23,20 +23,6 @@ func Tabs(c *ui.Context, text string, labels []string, selected *int, o TabsOpti
return tabs(c, text, labels, selected, false, o, panel)
}
type VerticalTabsOptions struct {
Disabled bool
}
type VerticalTabsResult struct {
Element *ui.Element
Changed bool
}
// VerticalTabs are tabs down the panel's left edge.
func VerticalTabs(c *ui.Context, text string, labels []string, selected *int, o VerticalTabsOptions, panel func()) VerticalTabsResult {
return VerticalTabsResult(tabs(c, text, labels, selected, true, TabsOptions(o), panel))
}
// tabs lays tabs beside a panel; side means left.
func tabs(c *ui.Context, text string, labels []string, selected *int, side bool, o TabsOptions, panel func()) TabsResult {
k := pixel.K(c)
-36
View File
@@ -1,12 +1,8 @@
package navigation
import (
"log/slog"
"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/pixel"
)
@@ -20,35 +16,3 @@ func Toolbar(c *ui.Context, text string, o ToolbarOptions, content func()) *ui.E
g.Frame(0, 0, g.W, g.H, pixel.Raised, pixel.Face)
}).Children(content)
}
type BackForwardOptions struct {
Disabled bool
}
type BackForwardResult struct {
Element *ui.Element
// Moved reports a step through the history.
Moved bool
}
// BackForward walks a router's history with ◀ and ▶.
func BackForward(c *ui.Context, r *ui.Router, o BackForwardOptions) BackForwardResult {
k := pixel.K(c)
res := BackForwardResult{}
res.Element = ui.Row(c).Gap(k.D(2)).FocusGroup(ui.Horizontal).Role(ui.RoleToolbar).Label(k.Msg("history")).Children(func() {
back := input.IconButtonOptions{Size: input.Small, Disabled: o.Disabled || !r.CanGoBack()}
if input.IconButton(c, icon.ChevronLeft, k.Msg("back"), back).Clicked {
r.Back()
res.Moved = true
}
forth := input.IconButtonOptions{Size: input.Small, Disabled: o.Disabled || !r.CanGoForward()}
if input.IconButton(c, icon.ChevronRight, k.Msg("forward"), forth).Clicked {
r.Forward()
res.Moved = true
}
})
if res.Moved {
slog.Debug("8bit: history", "path", r.Path())
}
return res
}
+19
View File
@@ -0,0 +1,19 @@
package navigation
import (
"github.com/egoist/mygo/ui"
)
type VerticalTabsOptions struct {
Disabled bool
}
type VerticalTabsResult struct {
Element *ui.Element
Changed bool
}
// VerticalTabs are tabs down the panel's left edge.
func VerticalTabs(c *ui.Context, text string, labels []string, selected *int, o VerticalTabsOptions, panel func()) VerticalTabsResult {
return VerticalTabsResult(tabs(c, text, labels, selected, true, TabsOptions(o), panel))
}