Create a toast component for arbitrary messages #57

Closed
opened 2026-08-31 11:20:03 +01:00 by liamjd · 1 comment
Owner

A label panel which appears for about 5 seconds bottom right of the screen with an arbitrary text message. Triggered by a signal.

If the message contains reference to a location or an entity, clicking on it should centre the location or entity on the screen.

The toast can be closed with a X icon, or will disappear after the specified timeout period. If the period is specified <= 0, the toast should appear permanently until manually dismissed.

Toasts will stack on top of each other, oldest toast at the bottom. Dismissing a toast (either with the X or the timeout) should fade and slide it off the screen before removing the node entirely. The toast stack should then slide down to fill the vacated space.

  • In the future, this could also visually highlight the entity, but that is out of scope for the toast component.

The message body needs to be quite flexible:

  • Plain text
  • Text and a title
  • Text, title and icon
  • Reference to an Entity (Pawn, Building)
  • A map cell reference
A label panel which appears for about 5 seconds bottom right of the screen with an arbitrary text message. Triggered by a signal. If the message contains reference to a location or an entity, clicking on it should centre the location or entity on the screen. The toast can be closed with a X icon, or will disappear after the specified timeout period. If the period is specified <= 0, the toast should appear permanently until manually dismissed. Toasts will stack on top of each other, oldest toast at the bottom. Dismissing a toast (either with the X or the timeout) should fade and slide it off the screen before removing the node entirely. The toast stack should then slide down to fill the vacated space. - In the future, this could also visually highlight the entity, but that is out of scope for the toast component. The message body needs to be quite flexible: - Plain text - Text and a title - Text, title and icon - Reference to an Entity (Pawn, Building) - A map cell reference
Author
Owner

Spec (drafted with Claude)

Decisions made on 2026-09-26:

  • Click target: the whole toast body is clickable, with at most one target per toast. The X button does not focus the camera.
  • Camera: snaps to the target (CameraRig.focus_on()), clamped to the map. There is no tween.
  • Timeout: counts real seconds and ignores game pause and speed. Pause-on-hover is out of scope.
  • Wired events: construction complete, demolition complete, and task blocked.

Acceptance criteria

  • GameState.toast_requested(message: ToastMessage) is the one way to raise a toast from anywhere in the game.
  • ToastMessage carries text, plus an optional title, an optional icon, a timeout (default 5s, where <= 0 means sticky) and at most one target: an Entity (Pawn or Building) or a map cell. Setting one kind of target clears the other.
  • A toast appears bottom right. New toasts stack above older ones, so the oldest is at the bottom.
  • A toast dismisses itself when its timeout runs out. A sticky toast stays until the X is clicked.
  • Dismissing a toast, by X or by timeout, fades it and slides it off to the right. Its slot then collapses so the toasts above slide down, and only after that is the node freed. dismissed is emitted exactly once, even if the X is clicked during the fade.
  • Clicking the body of a toast with a target centres the camera on it. For a building that is the centre of its footprint, for a pawn its current cell, and for a cell reference that cell. The target still resolves after the entity has been removed from its registry, because the record keeps its last position.
  • Empty space in the toast column does not swallow clicks meant for the map.
  • Construction complete (UNDER_CONSTRUCTION → BUILT) raises a toast targeting the building.
  • Demolition complete (the building is removed while marked_for_demolition) raises a toast targeting the building's record. Cancelling a planned or under-construction building raises nothing.
  • A task becoming BLOCKED raises a toast showing the task's label and targeting its target_cell when it has one.

Outline

File Change
ui/toasts/toast_message.gd new ToastMessage (RefCounted): data, fluent about_entity() / about_cell() / lasting(), and focus_point(map)
ui/toasts/toast.gd + toast.tscn new Toast: renders one message, runs its countdown in _process, dismiss animation, dismissed and focus_requested signals
ui/toasts/toast_stack.gd + toast_stack.tscn new ToastStack (VBoxContainer, bottom right, MOUSE_FILTER_IGNORE): listens to toast_requested, push(), relays focus_requested
ui/toasts/toast_notifier.gd new ToastNotifier (Node): turns gameplay signals into messages. Its builders are static so they can be tested without wiring
entities/camera/camera_rig.gd focus_on(world: Vector3)
globals/GameState.gd signal toast_requested(message: ToastMessage)
common/game.tscn / game.gd put the ToastStack under UIHUDLayer and the ToastNotifier under Game; hand the notifier the task manager; connect focus_requested to camera_rig.focus_on(message.focus_point(map))

Tests: test/unit/test_toast_message.gd, test_toast.gd, test_toast_stack.gd, test_toast_notifier.gd, test_camera_focus.gd.

Out of scope / follow-ups

  • Highlighting the target entity (as noted in the issue).
  • Pausing the countdown on hover, and a cap on how many toasts are visible at once. task_blocked could flood the column if many tasks block together.
  • Why a task is blocked, which is #119. This toast only says that it is.
## Spec (drafted with Claude) Decisions made on 2026-09-26: - **Click target:** the whole toast body is clickable, with at most one target per toast. The X button does not focus the camera. - **Camera:** snaps to the target (`CameraRig.focus_on()`), clamped to the map. There is no tween. - **Timeout:** counts real seconds and ignores game pause and speed. Pause-on-hover is out of scope. - **Wired events:** construction complete, demolition complete, and task blocked. ### Acceptance criteria - [x] `GameState.toast_requested(message: ToastMessage)` is the one way to raise a toast from anywhere in the game. - [x] `ToastMessage` carries text, plus an optional title, an optional icon, a timeout (default 5s, where `<= 0` means sticky) and at most one target: an `Entity` (Pawn or Building) or a map cell. Setting one kind of target clears the other. - [x] A toast appears bottom right. New toasts stack above older ones, so the oldest is at the bottom. - [x] A toast dismisses itself when its timeout runs out. A sticky toast stays until the X is clicked. - [x] Dismissing a toast, by X or by timeout, fades it and slides it off to the right. Its slot then collapses so the toasts above slide down, and only after that is the node freed. `dismissed` is emitted exactly once, even if the X is clicked during the fade. - [x] Clicking the body of a toast with a target centres the camera on it. For a building that is the centre of its footprint, for a pawn its current cell, and for a cell reference that cell. The target still resolves after the entity has been removed from its registry, because the record keeps its last position. - [x] Empty space in the toast column does not swallow clicks meant for the map. - [x] Construction complete (UNDER_CONSTRUCTION → BUILT) raises a toast targeting the building. - [x] Demolition complete (the building is removed while `marked_for_demolition`) raises a toast targeting the building's record. Cancelling a planned or under-construction building raises nothing. - [x] A task becoming BLOCKED raises a toast showing the task's label and targeting its `target_cell` when it has one. ### Outline | File | Change | |---|---| | `ui/toasts/toast_message.gd` | new `ToastMessage` (RefCounted): data, fluent `about_entity()` / `about_cell()` / `lasting()`, and `focus_point(map)` | | `ui/toasts/toast.gd` + `toast.tscn` | new `Toast`: renders one message, runs its countdown in `_process`, dismiss animation, `dismissed` and `focus_requested` signals | | `ui/toasts/toast_stack.gd` + `toast_stack.tscn` | new `ToastStack` (VBoxContainer, bottom right, `MOUSE_FILTER_IGNORE`): listens to `toast_requested`, `push()`, relays `focus_requested` | | `ui/toasts/toast_notifier.gd` | new `ToastNotifier` (Node): turns gameplay signals into messages. Its builders are static so they can be tested without wiring | | `entities/camera/camera_rig.gd` | `focus_on(world: Vector3)` | | `globals/GameState.gd` | `signal toast_requested(message: ToastMessage)` | | `common/game.tscn` / `game.gd` | put the ToastStack under UIHUDLayer and the ToastNotifier under Game; hand the notifier the task manager; connect `focus_requested` to `camera_rig.focus_on(message.focus_point(map))` | Tests: `test/unit/test_toast_message.gd`, `test_toast.gd`, `test_toast_stack.gd`, `test_toast_notifier.gd`, `test_camera_focus.gd`. ### Out of scope / follow-ups - Highlighting the target entity (as noted in the issue). - Pausing the countdown on hover, and a cap on how many toasts are visible at once. `task_blocked` could flood the column if many tasks block together. - Why a task is blocked, which is #119. This toast only says that it is.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
liamjd/UntitledColonyBuilder#57
No description provided.