Files
Dawn-Godot/.claude/docs/ui.md
T

8.7 KiB

UI System

Scene structure

RootUI (Control, fullscreen, always visible)
├── DebugMenu
├── GameMenu
│   ├── GameMenuPartyTab
│   └── GameMenuItemsTab
├── ChatBoxContainer
│   └── InteractIndicator
├── ModalBackdrop          ← shared backdrop; repositions dynamically
├── PauseMenu
│   ├── PauseMain
│   └── PauseSettings
├── QuitConfirmDialog
└── MainMenuConfirmDialog

Child order matters — later siblings render on top. ModalBackdrop shifts its own position in the tree at runtime to sit just below whichever modal overlay is currently topmost (see ModalBackdrop).

RootUI is a permanent child of RootScene and registers itself with the UI singleton on _enter_tree. Access its children through the UI singleton.

UI singleton

UI (autoload) is the global access point.

Accessor Type Notes
UI.DEBUG_MENU DebugMenu Dev scene-jump overlay
UI.GAME_MENU GameMenu JRPG-style in-game menu
UI.PAUSE_MENU PauseMenu Pause overlay; also pauses the scene tree
UI.QUIT_DIALOG QuitConfirmDialog "Quit to desktop?" confirm; call .open() to show
UI.MAIN_MENU_DIALOG ConfirmDialog "Return to main menu?" confirm; call .open() to show
UI.BACKDROP ModalBackdrop Shared semi-transparent backdrop; managed automatically
UI.dialogueActive bool true for the entire duration of a DialogueAction
UI.activeConversation bool true only during a CONVERSATION-mode dialogue
UI.chatBoxContainer Control Parent node for world-space dialogue textboxes

dialogueActive is set by DialogueManager signals — it is broader than any single textbox being visible. Movement blocks on dialogueActive and on UI.GAME_MENU.isOpen().

ClosableMenu

Base class for any togglable panel. Extends Control; the isOpen:bool export drives visible.

menu.open()    # shows, emits opened
menu.close()   # hides, emits closed
menu.toggle()

Signals: opened, closed.

All new menus that need standard show/hide behaviour should extend ClosableMenu.

ConfirmDialog

Reusable "Yes / No" confirmation overlay at res://ui/component/ConfirmDialog.gd. Extends ClosableMenu.

dialog.open()                        # shows, focuses No (safe default), emits opened
dialog.confirmed                     # signal — fires after Yes is pressed, before close

Focus locking: focus_neighbor_top/bottom on both buttons is wired so controller navigation cannot escape the dialog to elements behind it.

ui_cancel closes the dialog the same as pressing No.

Subclassing:

class_name MyConfirmDialog extends ConfirmDialog

func _ready() -> void:
  super._ready()
  confirmed.connect(func(): do_the_thing())

QuitConfirmDialog uses this pattern — it extends ConfirmDialog and connects confirmed → get_tree().quit() in its own _ready().

Adding a new confirm dialog:

  1. Create a .tscn with ConfirmDialog.gd as script; set label text in the scene
  2. Add btnYes/btnNo node path exports pointing to your two buttons
  3. Add the instance to RootUI.tscn after PauseMenu (so it renders on top)
  4. Register it with the backdrop in RootUI._ready(): modalBackdrop.register(myDialog)
  5. Expose via RootUI.gd export + UISingleton.gd accessor if other systems need it
  6. Connect myDialog.confirmed wherever the action should fire

ModalBackdrop

res://ui/component/ModalBackdrop.gd — a single fullscreen semi-transparent ColorRect shared across all modal overlays.

How it works: each registered overlay's opened/closed signals are connected. When any overlay opens, the backdrop becomes visible and move_child()s itself in the scene tree to sit immediately before the highest-index open overlay. When all overlays close, it hides.

Result: only one backdrop is ever visible, and it always sits between the game world (or lower overlays) and the frontmost open overlay — even when overlays are stacked (e.g. QuitConfirmDialog over PauseMenu).

Registering a new overlay:

# In RootUI._ready() — overlay must be a direct child of RootUI and have opened/closed signals
modalBackdrop.register(myOverlay)

PauseMenu, QuitConfirmDialog, and MainMenuConfirmDialog are all registered.

AdvancedRichText

RichTextLabel subclass (@tool) used inside world-space dialogue textboxes. Handles:

  • Smart word-wrap (TextServer.AUTOWRAP_WORD_SMART)
  • Pagination via maxLines / startLine exports
  • Inline input icons: [input action=interact]text[/input] → replaced with the icon image from res://ui/input/{action}.tres
  • Optional auto-translation via Godot's tr()

Supported icon actions: interact, pause, debug, up, down, left, right.

Game menu

JRPG-style in-game menu at res://ui/gamemenu/. Open with the menu input (Tab on keyboard, Y on controller). Blocks player movement while open via EntityMovement._canMove().

Structure:

  • Left sidebar: ItemList tab selector (Party, Items)
  • Right panel: active tab content
Tab Content
Party One card per PartyMember — name, status, HP/MP, ATK/DEF/SPD/MAG/LCK
Items One row per ItemStack in PARTY.BACKPACK — item name + quantity

Both tabs are populated dynamically on open; call refresh() on the active tab directly if data changes while the menu is already open.

Key methods on GameMenu:

Method Effect
open() Shows menu, refreshes active tab, grabs sidebar focus
close() Hides menu
isOpen() -> bool Visibility state

ui_cancel or menu closes the menu. The menu input opens it only when UI.dialogueActive is false and the textbox is closed.

To add a new tab: add a value to GameMenu.Tab, create a tab scene/script under ui/gamemenu/, instance it in GameMenu.tscn as a sibling of the other tabs, add an @export for it in GameMenu.gd, and add the match branch in _selectTab().

Pause menu

PauseMenu wraps PauseMain (button list) and PauseSettings (settings tabs). Opening it calls get_tree().paused = true; closing restores it.

Method Effect
PauseMenu.open() Pauses tree, shows container, opens PauseMain, emits opened
PauseMenu.close() Unpauses tree, hides everything, emits closed
PauseMenu.isOpen() -> bool Visibility state

ui_cancel behaviour inside PauseMenu:

  • If QuitConfirmDialog or MainMenuConfirmDialog is open → ignored (the dialog handles it)
  • If PauseSettings is open → closes settings, reopens PauseMain
  • Otherwise → closes PauseMenu

PauseMain buttons:

Button Behaviour
Resume Closes PauseMenu
Settings Opens PauseSettings
Main Menu Opens MainMenuConfirmDialog; on confirm → SCENE.setScene(INITIAL)
Quit Game Opens QuitConfirmDialog; on confirm → get_tree().quit()

When either confirm dialog cancels, focus returns to the button that opened it.

Cannot open on main menu: Pause.gd checks SCENE.currentScene == INITIAL and skips opening when already closed.

Main menu

res://ui/mainmenu/MainMenu.tscn. Buttons: New Game, Settings, Quit Game.

  • New Game → SCENE.setScene(OVERWORLD) + OVERWORLD.mapChange(...)
  • Settings → opens the MainMenuSettings overlay
  • Quit Game → opens UI.QUIT_DIALOG; on cancel, focus returns to the Quit button

Settings menu

SettingsMenu is a shared tabbed component instanced in both MainMenuSettings and PauseSettings. Three tabs: Gameplay, Sound, Graphics — currently contain placeholder labels only.

Debug menu

DebugMenu (toggle via debug input — F1) provides four scene-jump buttons:

Button Action
Overworld SCENE.setScene(OVERWORLD)
Battle SCENE.setScene(BATTLE)
Cooking SCENE.setScene(COOKING)
Initial SCENE.setScene(INITIAL)

Access via UI.DEBUG_MENU. Starts hidden.

Theme & assets

  • Global theme: res://ui/UI Theme.tres — applied to all UI nodes
  • Input icons: res://ui/input/{action}.tres — one file per action name
  • Font: configured globally in project settings

Adding a new menu

  1. Create a scene whose root extends ClosableMenu (or Control if open/close isn't needed)
  2. Add it as a child of RootUI.tscn — position after PauseMenu if it should render above it
  3. Export a typed reference on RootUI.gd and wire it in the Inspector
  4. Expose via a getter on UISingleton.gd if other systems need access (follow the PAUSE_MENU / GAME_MENU pattern)
  5. If it needs a backdrop, register it: modalBackdrop.register(myMenu) in RootUI._ready()