|
|
|
@@ -10,7 +10,7 @@ RootUI (Control, fullscreen, always visible)
|
|
|
|
|
│ └── GameMenuItemsTab
|
|
|
|
|
├── ChatBoxContainer
|
|
|
|
|
│ └── InteractIndicator
|
|
|
|
|
├── ModalBackdrop ← shared backdrop; repositions dynamically
|
|
|
|
|
├── ModalBackdrop ← shared backdrop; z_index managed by FocusStack
|
|
|
|
|
├── PauseMenu
|
|
|
|
|
│ ├── PauseMain
|
|
|
|
|
│ └── PauseSettings
|
|
|
|
@@ -18,7 +18,7 @@ RootUI (Control, fullscreen, always visible)
|
|
|
|
|
└── 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](#modalbackdrop)).
|
|
|
|
|
Child order matters — later siblings render on top. `ModalBackdrop` stays at a fixed tree position; its `z_index` is driven automatically by the FocusStack (see [ModalBackdrop](#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.
|
|
|
|
|
|
|
|
|
@@ -33,24 +33,38 @@ Child order matters — later siblings render on top. `ModalBackdrop` shifts its
|
|
|
|
|
| `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.FOCUS_STACK` | `UIFocusStack` | Ordered stack of open `ClosableMenu` layers; only the top layer processes input |
|
|
|
|
|
| `UI.BACKDROP` | `ModalBackdrop` | Shared semi-transparent backdrop; driven by `FOCUS_STACK.activeLayerChanged` |
|
|
|
|
|
| `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()`.
|
|
|
|
|
`dialogueActive` is set by `DialogueManager` signals — it is broader than any single textbox being visible. Movement and camera orbit block when `UI.FOCUS_STACK.top() != null` or `UI.activeConversation` is true.
|
|
|
|
|
|
|
|
|
|
## ClosableMenu
|
|
|
|
|
|
|
|
|
|
Base class for any togglable panel. Extends `Control`; the `isOpen:bool` export drives `visible`.
|
|
|
|
|
Base class for all togglable panels. Extends `Control`; the `isOpen:bool` export drives `visible`.
|
|
|
|
|
|
|
|
|
|
```gdscript
|
|
|
|
|
menu.open() # shows, emits opened
|
|
|
|
|
menu.close() # hides, emits closed
|
|
|
|
|
menu.open() # shows, pushes to FocusStack (if canClose), emits opened
|
|
|
|
|
menu.close() # pops from FocusStack (if canClose), hides, emits closed
|
|
|
|
|
menu.toggle()
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Signals: `opened`, `closed`.
|
|
|
|
|
**Key exports / properties:**
|
|
|
|
|
|
|
|
|
|
| Name | Default | Notes |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `isOpen:bool` | `false` | Property — read directly (`menu.isOpen`), not a method |
|
|
|
|
|
| `canClose:bool` | `true` | When true, open/close interact with `UI.FOCUS_STACK` and input is only processed while on top. When false, the menu is passive — shown/hidden externally, never enters the stack. |
|
|
|
|
|
|
|
|
|
|
**Signals:** `opened`, `closed`, `focusGained`, `focusLost`.
|
|
|
|
|
|
|
|
|
|
**Focus management** (only relevant when `canClose = true`):
|
|
|
|
|
- `_grabInitialFocus()` — virtual; override to place focus on the correct element on first open.
|
|
|
|
|
- `_savedFocusNode` — focus owner is captured on `_onFocusLost()` and restored on `_onFocusGained()`, so pressing back from a sub-dialog returns focus to the button that opened it.
|
|
|
|
|
- A `gui_focus_changed` focus trap runs while the layer is on top — if focus escapes to a node outside this layer, it is snapped back.
|
|
|
|
|
- `set_process_unhandled_input(false)` is set in `_ready()` for canClose menus; input is only re-enabled via `_onFocusGained()` while the layer is on top of the stack.
|
|
|
|
|
|
|
|
|
|
All new menus that need standard show/hide behaviour should extend `ClosableMenu`.
|
|
|
|
|
|
|
|
|
@@ -83,26 +97,20 @@ func _ready() -> void:
|
|
|
|
|
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
|
|
|
|
|
4. Expose via `RootUI.gd` export + `UISingleton.gd` accessor if other systems need it
|
|
|
|
|
5. Connect `myDialog.confirmed` wherever the action should fire
|
|
|
|
|
|
|
|
|
|
No manual backdrop registration needed — `ModalBackdrop` activates automatically when any `ClosableMenu` in RootUI enters the FocusStack.
|
|
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
**How it works:** `ModalBackdrop` connects to `UI.FOCUS_STACK.activeLayerChanged` in `_ready()`. When a layer becomes active it checks whether that layer is a direct sibling (i.e. a child of RootUI). If yes: backdrop becomes visible, sets `mouse_filter = MOUSE_FILTER_STOP` (blocking all clicks on anything behind it), and sets `z_index = layer.z_index - 5`. If the top layer is from a different parent (e.g. settings inside the main menu scene), or the stack empties, the backdrop 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`).
|
|
|
|
|
**Result:** only one backdrop is ever visible, it always renders between the game world and the frontmost RootUI-level overlay, and it blocks mouse events from reaching anything behind it.
|
|
|
|
|
|
|
|
|
|
**Registering a new overlay:**
|
|
|
|
|
|
|
|
|
|
```gdscript
|
|
|
|
|
# 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.
|
|
|
|
|
No registration is required — `ModalBackdrop` is self-contained. Do not call `register()` on it.
|
|
|
|
|
|
|
|
|
|
## AdvancedRichText
|
|
|
|
|
|
|
|
|
@@ -130,15 +138,15 @@ JRPG-style in-game menu at `res://ui/gamemenu/`. Open with the `menu` input (**T
|
|
|
|
|
|
|
|
|
|
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`:**
|
|
|
|
|
**Key members on `GameMenu`:**
|
|
|
|
|
|
|
|
|
|
| Method | Effect |
|
|
|
|
|
| Member | Notes |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `open()` | Shows menu, refreshes active tab, grabs sidebar focus |
|
|
|
|
|
| `close()` | Hides menu |
|
|
|
|
|
| `isOpen() -> bool` | Visibility state |
|
|
|
|
|
| `open()` | Shows menu, refreshes active tab, pushes to FocusStack, grabs sidebar focus |
|
|
|
|
|
| `close()` | Pops from FocusStack, hides menu |
|
|
|
|
|
| `isOpen:bool` | Property — read directly, not a method |
|
|
|
|
|
|
|
|
|
|
`ui_cancel` or `menu` closes the menu. The `menu` input opens it only when `UI.dialogueActive` is false and the textbox is closed.
|
|
|
|
|
`ui_cancel` or `menu` closes the menu. `menu` is handled in `_input` (always fires) and opens the menu only when `UI.FOCUS_STACK.top() == null` and `UI.dialogueActive` is false.
|
|
|
|
|
|
|
|
|
|
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()`.
|
|
|
|
|
|
|
|
|
@@ -146,17 +154,18 @@ To add a new tab: add a value to `GameMenu.Tab`, create a tab scene/script under
|
|
|
|
|
|
|
|
|
|
`PauseMenu` wraps `PauseMain` (button list) and `PauseSettings` (settings tabs). Opening it calls `get_tree().paused = true`; closing restores it.
|
|
|
|
|
|
|
|
|
|
| Method | Effect |
|
|
|
|
|
| Member | Notes |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `PauseMenu.open()` | Pauses tree, shows container, opens PauseMain, emits `opened` |
|
|
|
|
|
| `PauseMenu.close()` | Unpauses tree, hides everything, emits `closed` |
|
|
|
|
|
| `PauseMenu.isOpen() -> bool` | Visibility state |
|
|
|
|
|
| `PauseMenu.open()` | Pushes to FocusStack, pauses tree, opens PauseMain, emits `opened` |
|
|
|
|
|
| `PauseMenu.close()` | Unpauses tree, closes sub-panels, pops from FocusStack, emits `closed` |
|
|
|
|
|
| `PauseMenu.isOpen:bool` | Property — read directly, not a method |
|
|
|
|
|
|
|
|
|
|
**`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
|
|
|
|
|
|
|
|
|
|
(`QuitConfirmDialog` and `MainMenuConfirmDialog` sit above PauseMenu on the FocusStack and consume `ui_cancel` themselves — PauseMenu's `_unhandled_input` does not fire while they are open.)
|
|
|
|
|
|
|
|
|
|
**PauseMain buttons:**
|
|
|
|
|
|
|
|
|
|
| Button | Behaviour |
|
|
|
|
@@ -166,9 +175,9 @@ To add a new tab: add a value to `GameMenu.Tab`, create a tab scene/script under
|
|
|
|
|
| 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.
|
|
|
|
|
When either confirm dialog closes, focus automatically returns to the button that opened it via `_savedFocusNode` in the FocusStack.
|
|
|
|
|
|
|
|
|
|
**Cannot open on main menu:** `Pause.gd` checks `SCENE.currentScene == INITIAL` and skips opening when already closed.
|
|
|
|
|
**Cannot open on main menu:** `Pause.gd` checks `SCENE.currentScene == INITIAL` and skips opening.
|
|
|
|
|
|
|
|
|
|
## Main menu
|
|
|
|
|
|
|
|
|
@@ -204,7 +213,9 @@ Access via `UI.DEBUG_MENU`. Starts hidden.
|
|
|
|
|
## 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()`
|
|
|
|
|
2. Override `_grabInitialFocus()` to place focus on the first interactive element on open
|
|
|
|
|
3. Add it as a child of `RootUI.tscn` — position after `PauseMenu` if it should render above it
|
|
|
|
|
4. Export a typed reference on `RootUI.gd` and wire it in the Inspector
|
|
|
|
|
5. Expose via a getter on `UISingleton.gd` if other systems need access (follow the `PAUSE_MENU` / `GAME_MENU` pattern)
|
|
|
|
|
|
|
|
|
|
`ModalBackdrop` activates automatically for any `ClosableMenu` that is a direct child of `RootUI`. No registration step needed. Internal sub-panels (like `PauseSettings`) should extend `Control` directly and not enter the FocusStack.
|
|
|
|
|