The manager that holds widgets is the WidgetManager
ci/woodpecker/push/woodpecker Pipeline was successful

UIManager named the category, not the contents. Every other manager is
named after what it registers — ItemManager holds Items, SceneManager
holds Scenes — so the one that registers Widgets is the WidgetManager.

The alias, the Game field and both manual pages move together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 22:05:55 +02:00
co-authored by Claude Opus 5
parent e26f7345c1
commit 53f4df0466
7 changed files with 48 additions and 48 deletions
+9 -9
View File
@@ -180,7 +180,7 @@ type DialogueManager = Manager[Dialogue]
type ScriptManager = Manager[Script] type ScriptManager = Manager[Script]
type AssetManager = Manager[Asset] type AssetManager = Manager[Asset]
type VerbManager = Manager[Verb] type VerbManager = Manager[Verb]
type UIManager = Manager[Widget] type WidgetManager = Manager[Widget]
type ThemeManager = Manager[Theme] type ThemeManager = Manager[Theme]
``` ```
@@ -242,7 +242,7 @@ type Game struct {
ScriptManager *ScriptManager ScriptManager *ScriptManager
AssetManager *AssetManager AssetManager *AssetManager
VerbManager *VerbManager VerbManager *VerbManager
UIManager *UIManager WidgetManager *WidgetManager
ThemeManager *ThemeManager ThemeManager *ThemeManager
// exits // exits
@@ -273,7 +273,7 @@ Initialises every manager (empty), registers the default SCUMM verb set
(`look`, `use`, `talk`, `take`), installs all four preset themes, and (`look`, `use`, `talk`, `take`), installs all four preset themes, and
selects `classic-scumm` as the active theme. Widgets are **not** selects `classic-scumm` as the active theme. Widgets are **not**
auto-registered — call `RegisterDefaultUI(g)` (or one of its siblings) auto-registered — call `RegisterDefaultUI(g)` (or one of its siblings)
explicitly, or let `Run` install the default set if `UIManager` is empty. explicitly, or let `Run` install the default set if `WidgetManager` is empty.
The manager fields are ordinary `*Manager` values, so a domain that keeps its The manager fields are ordinary `*Manager` values, so a domain that keeps its
own registries can **assign them onto the Game** instead of copying every entry own registries can **assign them onto the Game** instead of copying every entry
@@ -1002,7 +1002,7 @@ could be used by triggers once those land.
## 10. The widget system ## 10. The widget system
The HUD is a tree of `Widget`s, each registered into `g.UIManager`. The The HUD is a tree of `Widget`s, each registered into `g.WidgetManager`. The
library ships built-in widgets for every classic adventure UI piece; library ships built-in widgets for every classic adventure UI piece;
domain code can register arbitrary new ones — chat panels, minimaps, domain code can register arbitrary new ones — chat panels, minimaps,
hotbars — without touching the library. hotbars — without touching the library.
@@ -1352,7 +1352,7 @@ func (m *Minimap) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
// ... render scene thumbnail, mark NPCs, etc. // ... render scene thumbnail, mark NPCs, etc.
} }
g.UIManager.Register(&Minimap{ g.WidgetManager.Register(&Minimap{
Name: "minimap", Name: "minimap",
Bounds: inkwell.Rect(220, 4, 96, 56), Bounds: inkwell.Rect(220, 4, 96, 56),
}) })
@@ -1554,7 +1554,7 @@ func Run(g *Game) error // toplevel — same as g.Run()
1. `g.Audio.attach(g.AssetManager)` — wire the audio player to whichever 1. `g.Audio.attach(g.AssetManager)` — wire the audio player to whichever
asset registry the game is carrying by now. asset registry the game is carrying by now.
2. `g.Validate()` — cross-check name references between managers. 2. `g.Validate()` — cross-check name references between managers.
3. If `UIManager` is empty, call `RegisterDefaultUI(g)`. 3. If `WidgetManager` is empty, call `RegisterDefaultUI(g)`.
4. Place the start scene directly (no transition), bump 4. Place the start scene directly (no transition), bump
`State.NoteVisit`, position registered actors, kick off music. `State.NoteVisit`, position registered actors, kick off music.
5. Compose `Seq(scene.OnEnter, OnStart script, OnFinale script)` and queue 5. Compose `Seq(scene.OnEnter, OnStart script, OnFinale script)` and queue
@@ -1575,7 +1575,7 @@ if scriptRunner != nil:
return return
clear hoverLabel clear hoverLabel
for w in reversed(UIManager): for w in reversed(WidgetManager):
w.Tick(uictx) // widgets consume input top-down w.Tick(uictx) // widgets consume input top-down
handleSceneInput() // hotspot resolution + right-click reset handleSceneInput() // hotspot resolution + right-click reset
@@ -1589,7 +1589,7 @@ fill Theme.SceneBackdrop (if any)
draw scene background image draw scene background image
for c in characters sorted by Y: for c in characters sorted by Y:
drawCharacter(c) drawCharacter(c)
for w in UIManager (registration order): for w in WidgetManager (registration order):
w.Draw(screen, uictx) w.Draw(screen, uictx)
draw transition overlay draw transition overlay
``` ```
@@ -1845,7 +1845,7 @@ inkwell/ # module git.teletypegames.org/games/inkwell
├── input.def.go # Input (consume-on-use) ├── input.def.go # Input (consume-on-use)
├── ui.widget.go # Widget interface, UICtx, Size, Align ├── ui.widget.go # Widget interface, UICtx, Size, Align
├── ui.manager.go # UIManager alias + reversed/ordered iterators ├── ui.manager.go # WidgetManager alias + reversed/ordered iterators
├── ui.theme.go # Theme + ThemeManager ├── ui.theme.go # Theme + ThemeManager
├── ui.theme_presets.go # 4 preset themes ├── ui.theme_presets.go # 4 preset themes
├── ui.defaults.go # RegisterDefaultUI/RadialVerbUI/RichUI ├── ui.defaults.go # RegisterDefaultUI/RadialVerbUI/RichUI
+1 -1
View File
@@ -16,7 +16,7 @@ func Run(g *Game) error {
} }
// If the domain didn't register any widgets, fall back to the SCUMM // If the domain didn't register any widgets, fall back to the SCUMM
// preset so the game is still playable. // preset so the game is still playable.
if g.UIManager.Len() == 0 { if g.WidgetManager.Len() == 0 {
RegisterDefaultUI(g) RegisterDefaultUI(g)
} }
+2 -2
View File
@@ -61,7 +61,7 @@ func (e *engine) Update() error {
// Top-down input: the widget drawn last (= registered last) gets the // Top-down input: the widget drawn last (= registered last) gets the
// click first, then the next-to-last, etc. A widget signals "I took it" // click first, then the next-to-last, etc. A widget signals "I took it"
// via g.Input.ConsumeLeft / ConsumeRight. // via g.Input.ConsumeLeft / ConsumeRight.
for _, w := range reversedWidgets(g.UIManager) { for _, w := range reversedWidgets(g.WidgetManager) {
w.Tick(uictx) w.Tick(uictx)
} }
@@ -192,7 +192,7 @@ func (e *engine) Draw(screen *ebiten.Image) {
// widgets in registration order // widgets in registration order
uictx := &UICtx{Game: g, DT: 1.0 / 60.0} uictx := &UICtx{Game: g, DT: 1.0 / 60.0}
for _, w := range orderedWidgets(g.UIManager) { for _, w := range orderedWidgets(g.WidgetManager) {
w.Draw(screen, uictx) w.Draw(screen, uictx)
} }
+2 -2
View File
@@ -21,7 +21,7 @@ type Game struct {
ScriptManager *ScriptManager ScriptManager *ScriptManager
AssetManager *AssetManager AssetManager *AssetManager
VerbManager *VerbManager VerbManager *VerbManager
UIManager *UIManager WidgetManager *WidgetManager
ThemeManager *ThemeManager ThemeManager *ThemeManager
// SceneRect is the part of the window the picture occupies. Zero means // SceneRect is the part of the window the picture occupies. Zero means
@@ -131,7 +131,7 @@ func NewGame(title string, w, h int) *Game {
ScriptManager: NewManager[Script](), ScriptManager: NewManager[Script](),
AssetManager: NewManager[Asset](), AssetManager: NewManager[Asset](),
VerbManager: NewManager[Verb](), VerbManager: NewManager[Verb](),
UIManager: NewManager[Widget](), WidgetManager: NewManager[Widget](),
ThemeManager: NewManager[Theme](), ThemeManager: NewManager[Theme](),
State: NewState(), State: NewState(),
Inventory: NewInventory(), Inventory: NewInventory(),
+28 -28
View File
@@ -1,20 +1,20 @@
package inkwell package inkwell
// RegisterDefaultUI installs the SCUMM-style HUD widgets into g.UIManager // RegisterDefaultUI installs the SCUMM-style HUD widgets into g.WidgetManager
// in the conventional Z-order (HotspotDebug at the back, Cursor on top). // in the conventional Z-order (HotspotDebug at the back, Cursor on top).
// Domains that want a different layout call this and then either tweak // Domains that want a different layout call this and then either tweak
// the registered widgets in place or replace them entirely. // the registered widgets in place or replace them entirely.
// //
// Layout assumes the default 320×200 internal resolution. // Layout assumes the default 320×200 internal resolution.
func RegisterDefaultUI(g *Game) { func RegisterDefaultUI(g *Game) {
g.UIManager.Register(&HotspotDebug{Name: "hotspot_debug"}) g.WidgetManager.Register(&HotspotDebug{Name: "hotspot_debug"})
g.UIManager.Register(&VerbBar{ g.WidgetManager.Register(&VerbBar{
Name: "verbs", Name: "verbs",
Origin: Point{X: 4, Y: 152}, Origin: Point{X: 4, Y: 152},
Cols: 2, Cols: 2,
Button: Size{W: 60, H: 14}, Button: Size{W: 60, H: 14},
}) })
g.UIManager.Register(&InventoryBar{ g.WidgetManager.Register(&InventoryBar{
Name: "inventory", Name: "inventory",
Origin: Point{X: 132, Y: 152}, Origin: Point{X: 132, Y: 152},
Slots: 8, Slots: 8,
@@ -22,11 +22,11 @@ func RegisterDefaultUI(g *Game) {
SlotSize: 22, SlotSize: 22,
Gap: 2, Gap: 2,
}) })
g.UIManager.Register(&StatusLine{Name: "status", Y: 142, Align: AlignCenter}) g.WidgetManager.Register(&StatusLine{Name: "status", Y: 142, Align: AlignCenter})
g.UIManager.Register(&SpeechBubble{Name: "speech", MaxWidth: 200, Padding: 3, OffsetY: 4, FallbackY: 14}) g.WidgetManager.Register(&SpeechBubble{Name: "speech", MaxWidth: 200, Padding: 3, OffsetY: 4, FallbackY: 14})
g.UIManager.Register(&DialogBox{Name: "dialog", Bounds: Rect(0, 140, float64(g.Width), 60), LineHeight: 14, Padding: 6}) g.WidgetManager.Register(&DialogBox{Name: "dialog", Bounds: Rect(0, 140, float64(g.Width), 60), LineHeight: 14, Padding: 6})
g.UIManager.Register(&EndCard{Name: "endcard"}) g.WidgetManager.Register(&EndCard{Name: "endcard"})
g.UIManager.Register(&Cursor{Name: "cursor"}) g.WidgetManager.Register(&Cursor{Name: "cursor"})
} }
// RegisterRichUI installs a "story-rich" HUD that matches the layout of // RegisterRichUI installs a "story-rich" HUD that matches the layout of
@@ -39,15 +39,15 @@ func RegisterDefaultUI(g *Game) {
// PlayerName / NPCName correspond to registered characters. Pass "" for // PlayerName / NPCName correspond to registered characters. Pass "" for
// NPCName to skip the NPC panel. // NPCName to skip the NPC panel.
func RegisterRichUI(g *Game, playerName, npcName string) { func RegisterRichUI(g *Game, playerName, npcName string) {
g.UIManager.Register(&TopBar{ g.WidgetManager.Register(&TopBar{
Name: "topbar", Name: "topbar",
Height: 12, Height: 12,
ScoreVar: "score", ScoreMax: 100, ScoreVar: "score", ScoreMax: 100,
TimeVar: "time", TimeVar: "time",
}) })
g.UIManager.Register(&HotspotDebug{Name: "hotspot_debug"}) g.WidgetManager.Register(&HotspotDebug{Name: "hotspot_debug"})
if playerName != "" { if playerName != "" {
g.UIManager.Register(&CharacterPanel{ g.WidgetManager.Register(&CharacterPanel{
Name: "panel_player", Name: "panel_player",
Bounds: Rect(2, 14, 92, 36), Bounds: Rect(2, 14, 92, 36),
Character: playerName, Character: playerName,
@@ -59,7 +59,7 @@ func RegisterRichUI(g *Game, playerName, npcName string) {
}) })
} }
if npcName != "" { if npcName != "" {
g.UIManager.Register(&CharacterPanel{ g.WidgetManager.Register(&CharacterPanel{
Name: "panel_npc", Name: "panel_npc",
Bounds: Rect(180, 14, 92, 36), Bounds: Rect(180, 14, 92, 36),
Character: npcName, Character: npcName,
@@ -70,7 +70,7 @@ func RegisterRichUI(g *Game, playerName, npcName string) {
}, },
}) })
} }
g.UIManager.Register(&InventoryBar{ g.WidgetManager.Register(&InventoryBar{
Name: "inventory", Name: "inventory",
Origin: Point{X: 4, Y: 188}, Origin: Point{X: 4, Y: 188},
Slots: 8, Slots: 8,
@@ -78,14 +78,14 @@ func RegisterRichUI(g *Game, playerName, npcName string) {
SlotSize: 10, SlotSize: 10,
Gap: 1, Gap: 1,
}) })
g.UIManager.Register(&ChatLog{ g.WidgetManager.Register(&ChatLog{
Name: "chat", Name: "chat",
Bounds: Rect(2, 148, 280, 38), Bounds: Rect(2, 148, 280, 38),
LineHeight: 9, LineHeight: 9,
Padding: 3, Padding: 3,
ShowBorder: true, ShowBorder: true,
}) })
g.UIManager.Register(&RadialVerbs{ g.WidgetManager.Register(&RadialVerbs{
Name: "verbs", Name: "verbs",
AlwaysVisible: true, AlwaysVisible: true,
Center: Point{X: 282, Y: 90}, Center: Point{X: 282, Y: 90},
@@ -97,18 +97,18 @@ func RegisterRichUI(g *Game, playerName, npcName string) {
"take": "Vedd", "take": "Vedd",
}, },
}) })
g.UIManager.Register(&SpeechBubble{Name: "speech", MaxWidth: 200, Padding: 3, OffsetY: 4, FallbackY: 18}) g.WidgetManager.Register(&SpeechBubble{Name: "speech", MaxWidth: 200, Padding: 3, OffsetY: 4, FallbackY: 18})
g.UIManager.Register(&DialogBox{Name: "dialog", Bounds: Rect(0, 90, float64(g.Width), 56), LineHeight: 12, Padding: 6}) g.WidgetManager.Register(&DialogBox{Name: "dialog", Bounds: Rect(0, 90, float64(g.Width), 56), LineHeight: 12, Padding: 6})
g.UIManager.Register(&EndCard{Name: "endcard"}) g.WidgetManager.Register(&EndCard{Name: "endcard"})
g.UIManager.Register(&Cursor{Name: "cursor"}) g.WidgetManager.Register(&Cursor{Name: "cursor"})
} }
// RegisterRadialVerbUI installs an alternative HUD that swaps the // RegisterRadialVerbUI installs an alternative HUD that swaps the
// permanent verb-bar for a verb-coin (right-click radial menu). Inventory, // permanent verb-bar for a verb-coin (right-click radial menu). Inventory,
// dialog, speech and cursor stay the same. // dialog, speech and cursor stay the same.
func RegisterRadialVerbUI(g *Game) { func RegisterRadialVerbUI(g *Game) {
g.UIManager.Register(&HotspotDebug{Name: "hotspot_debug"}) g.WidgetManager.Register(&HotspotDebug{Name: "hotspot_debug"})
g.UIManager.Register(&InventoryBar{ g.WidgetManager.Register(&InventoryBar{
Name: "inventory", Name: "inventory",
Origin: Point{X: 4, Y: 178}, Origin: Point{X: 4, Y: 178},
Slots: 14, Slots: 14,
@@ -116,10 +116,10 @@ func RegisterRadialVerbUI(g *Game) {
SlotSize: 22, SlotSize: 22,
Gap: 0, Gap: 0,
}) })
g.UIManager.Register(&StatusLine{Name: "status", Y: 168, Align: AlignCenter}) g.WidgetManager.Register(&StatusLine{Name: "status", Y: 168, Align: AlignCenter})
g.UIManager.Register(&SpeechBubble{Name: "speech", MaxWidth: 200, Padding: 3, OffsetY: 4, FallbackY: 14}) g.WidgetManager.Register(&SpeechBubble{Name: "speech", MaxWidth: 200, Padding: 3, OffsetY: 4, FallbackY: 14})
g.UIManager.Register(&DialogBox{Name: "dialog", Bounds: Rect(0, 140, float64(g.Width), 60), LineHeight: 14, Padding: 6}) g.WidgetManager.Register(&DialogBox{Name: "dialog", Bounds: Rect(0, 140, float64(g.Width), 60), LineHeight: 14, Padding: 6})
g.UIManager.Register(&RadialVerbs{Name: "verbs", Trigger: MouseButtonRight, Radius: 40}) g.WidgetManager.Register(&RadialVerbs{Name: "verbs", Trigger: MouseButtonRight, Radius: 40})
g.UIManager.Register(&EndCard{Name: "endcard"}) g.WidgetManager.Register(&EndCard{Name: "endcard"})
g.UIManager.Register(&Cursor{Name: "cursor"}) g.WidgetManager.Register(&Cursor{Name: "cursor"})
} }
+4 -4
View File
@@ -1,12 +1,12 @@
package inkwell package inkwell
// UIManager registers Widget instances. Same shape as every other manager. // WidgetManager registers Widget instances. Same shape as every other manager.
type UIManager = Manager[Widget] type WidgetManager = Manager[Widget]
// reversedWidgets iterates a manager's contents in reverse registration // reversedWidgets iterates a manager's contents in reverse registration
// order — used by the engine for top-down input dispatch (the widget // order — used by the engine for top-down input dispatch (the widget
// drawn on top gets the click first). // drawn on top gets the click first).
func reversedWidgets(m *UIManager) []Widget { func reversedWidgets(m *WidgetManager) []Widget {
all := m.All() all := m.All()
out := make([]Widget, len(all)) out := make([]Widget, len(all))
for i, w := range all { for i, w := range all {
@@ -16,4 +16,4 @@ func reversedWidgets(m *UIManager) []Widget {
} }
// orderedWidgets iterates in registration order — bottom-up draw. // orderedWidgets iterates in registration order — bottom-up draw.
func orderedWidgets(m *UIManager) []Widget { return m.All() } func orderedWidgets(m *WidgetManager) []Widget { return m.All() }
+2 -2
View File
@@ -140,11 +140,11 @@ func (r *RadialVerbs) consumeTrigger(g *Game) {
// whether its area covers the cursor — so the radial menu doesn't open // whether its area covers the cursor — so the radial menu doesn't open
// over an InventoryBar that would also want this click. // over an InventoryBar that would also want this click.
func (r *RadialVerbs) blockedAt(ctx *UICtx, p Point) bool { func (r *RadialVerbs) blockedAt(ctx *UICtx, p Point) bool {
for _, name := range ctx.Game.UIManager.Names() { for _, name := range ctx.Game.WidgetManager.Names() {
if name == r.Name { if name == r.Name {
continue continue
} }
w := ctx.Game.UIManager.MustGet(name) w := ctx.Game.WidgetManager.MustGet(name)
// DialogBox advertises its bounds via clickBlocker even when no // DialogBox advertises its bounds via clickBlocker even when no
// dialog is open, so the verb-coin would refuse to pop up over // dialog is open, so the verb-coin would refuse to pop up over
// scenery that happens to sit under the dialog rect. Skip the // scenery that happens to sit under the dialog rect. Skip the