Files
stickman/docs/specs/speech_whole_sequence_show.md

16 KiB

Stage Controls for How the Whole Speech Shows

Status: Implemented; pending user verification (2026-10-02). This documents the stage controls that extend the approved comic speech composer and replace the Inspector Show section. Bubble layout presets are specified in Speech Bubble Layout Presets.

Current dialog layout: see the 2026-10-02 amendment at the end of this document. It supersedes older control grouping, Comic chain placement, and footer descriptions below.

1. User interaction

When speech editing opens, no bubble has stage focus. A compact panel beside the bubble group shows the three choices that affect the entire speech immediately. There is no Whole Speech button or additional mode switch.

Label Choices Stored values
💬 Show bubbles Show all / One at a time / Comic chain arrangement: "together" / "take_turns" / "comic_chain"
🔤 Words appear All at once / Letter by letter / Word by word action reveal: "instant" / "letter" / "word"
✨ Bubble motion Bounce in / Just appear action pop_in: true / false

Use large buttons with a persistent selected outline that remains visible on hover, a ribbon title such as Show speech, and optional small pictorial samples. A choice updates the recoverable speech draft and selection feedback immediately. Comic chain is cumulative: each bubble appears in order while earlier bubbles remain visible, and joined connectors are shown. Entering it applies the Stack positions; the separate Stack and Same spot actions only position bubbles. Choosing Show all or One at a time leaves Comic chain. Preview speech plays the full sequence with the chosen arrangement, reveal, and motion. The separate Save speech action remains the queue/rule commit point; Cancel discards the session.

Clicking a bubble or one of its tokens focuses that bubble and swaps the group panel for its existing Speech Style toolbar. The token is also selected when one was tapped. Done styling and a click on empty stage space clear bubble focus and restore the group panel. Empty stage clicks do not select, move, or edit characters or props beneath them. Adding a bubble still selects the new bubble, as previously approved; after Done styling, the group panel returns. A bubble drag focuses that bubble once the drag threshold is met. Clicking overlapping bubbles retains the existing temporary picker.

The group panel sits beside the visible bubble group and follows it after bubble movement, camera changes, resize, and text changes. Prefer a side with enough room; otherwise try the other side, then below or above. Clamp the panel within the usable stage rectangle and keep it off the bubbles whenever space permits. Use the existing stage theme, ribbon, 44 px minimum controls, and vertical wrapping; do not introduce horizontal stage scrolling. During a full replay or message dialog, hide or disable the group panel consistently with the current Speech Style toolbar. No group highlight overlay is required; the group title and three choice rows communicate scope.

Speech editor opened / Done styling / empty stage click
    ┌──────────────────────┐     ┌────────────────────────────┐
    │  Bubble group        │     │ Show speech                │
    │  [Hi!] [Everyone!]   │     │ 💬 Show bubbles            │
    │                      │     │ 🔤 Words appear            │
    │                      │     │ ✨ Bubble motion           │
    └──────────────────────┘     └────────────────────────────┘
                 │ click a bubble or token
                 ▼
    ┌──────────────────────┐     ┌────────────────────────────┐
    │ Selected bubble      │     │ Speech Style               │
    │ [Hi!]                │     │ Message · Timing           │
    │                      │     │ Frame · Letters · Effects  │
    │                      │     │ Done styling               │
    └──────────────────────┘     └────────────────────────────┘

The existing Timing control remains in Speech Style for this change. It already sets the action-wide duration_mode; this specification changes only the three former Show controls.

2. Ownership, nodes, and typed interfaces

Keep SpeechComposer (class_name SpeechComposer extends Control) as the owner of the draft and text editing. Its _selected_bubble: int remains a valid index for the Message editor and draft recovery. Add a separate transient _stage_focus_bubble: int = -1; -1 means the group panel is shown. Focus is UI state and is not serialized. Add typed methods such as:

signal stage_focus_changed(bubble_index: int) # -1 means the whole speech

func stage_focus_bubble_index() -> int
func clear_stage_focus() -> void
func choose_speech_show(key: StringName, value: Variant) -> void

choose_speech_show accepts only &"arrangement", &"reveal", and &"pop_in"; it validates each value against SpeechPayload constants, updates one action-level field, synchronizes the per-bubble render fields, recalculates duration through the existing _emit_draft(), and emits one draft_changed(action: Dictionary). Selecting Comic chain applies measured Stack centers and the arrangement together in one draft update. It must not accidentally overwrite the Message TextEdit content; preserve _save_active_editor_state() ordering. Existing select_bubble_from_stage, select_bubble_token_from_stage, and _add_bubble set stage focus. open_for and set_draft start with no stage focus; recovered _selected_bubble can still restore editor position independently.

Add a lightweight SpeechShowToolbar (class_name SpeechShowToolbar extends Control) as a sibling of SpeechStyleToolbar under the Sandbox Stage UI control. Its full-rect root ignores mouse input while its visible PanelContainer stops mouse input. Suggested public contract:

signal choice_selected(key: StringName, value: Variant)

func configure(theme_data: SandboxTheme) -> void
func set_context(action: Dictionary) -> void
func set_anchor_rect(group_viewport_rect: Rect2, stage_viewport_rect: Rect2) -> void
func set_busy(busy: bool) -> void
func contains_viewport_point(point: Vector2) -> bool

The toolbar only emits choices; it does not mutate a draft or save a scene. Its hierarchy is SpeechShowToolbar / PanelContainer / VBoxContainer / ribbon Label + three labelled HFlowContainer choice rows. Put its PanelContainer in the same stage UI layer as Speech Style. Reuse SandboxTheme colours, button sizes, and selected/hover border treatment.

SpeechBubble (class_name SpeechBubble extends Node2D) should expose func group_frames_viewport_rect() -> Rect2, the union of its rendered frame rectangles in viewport coordinates, using the same _layout_frame and canvas transform already used by selected_frame_viewport_rect(index). Return an empty rect when no static editor frames exist. This keeps screen geometry in the renderer rather than recomputing it from text or actor positions in the toolbar.

SandboxStage owns focus presentation and pointer routing. It connects SpeechShowToolbar.choice_selected to the composer's choose_speech_show, and SpeechComposer.stage_focus_changed to a single refresh path that shows exactly one of the group panel and Speech Style toolbar. _speech_composer_owns_event must include the group panel's visible rect so a click on a choice cannot pass through to stage bubble selection. The existing _resolve_speech_bubble_pointer_click empty-hit path calls clear_stage_focus() instead of only clearing the token. The Done styling handler also calls clear_stage_focus() rather than merely hiding Speech Style. Both panels use the current busy/preview gate, and stage cleanup hides both. The current _speech_style_toolbar_dismissed flag should be retired or derived from focus, because an independent hidden state can disagree with the new panel swap.

flowchart LR
    P[Stage pointer] --> S[SandboxStage]
    S -->|bubble/token click or empty click| C[SpeechComposer focus]
    C -->|stage_focus_changed| S
    S -->|focus = -1| G[SpeechShowToolbar]
    S -->|focus >= 0| B[SpeechStyleToolbar]
    G -->|choice_selected| S
    S -->|choose_speech_show| C
    C -->|draft_changed| S
    S --> R[SpeechBubble preview]

3. Action payload and migration

Use action-level fields as the source of truth for all three Show choices. Keep the existing arrangement field and add reveal and pop_in at the action level. The bubble visual.reveal and visual.pop_in keys remain synchronized render copies for the existing SpeechBubble and SpeechPayload.duration_for() paths; they are never independent editing controls after this change.

{
    "type": "speak",
    "arrangement": "take_turns", # or "together" / "comic_chain"
    "reveal": "letter",
    "pop_in": true,
    "duration_mode": "just_enough",
    "bubbles": [
        {"text": "Hi!", "visual": {"bubble_style": "talk", "reveal": "letter", "pop_in": true}},
        {"text": "Everyone!", "visual": {"bubble_style": "shout", "reveal": "letter", "pop_in": true}}
    ]
}

SpeechPayload.normalize(raw: Dictionary) -> Dictionary determines shared reveal and pop_in in this order: valid action-level values; otherwise the first bubble's visual values; otherwise the legacy single-text visual; otherwise "instant" and false. It writes the chosen values to the action and every normalized bubble visual. The same first-bubble rule deliberately resolves older multi-bubble actions whose per-bubble values differ. Loading such an action into editing and saving it makes all bubbles use the first bubble's reveal and motion. Preserve the first bubble's other visual properties and all bubble placements. arrangement accepts together, take_turns, and comic_chain, defaulting to together. Older bubble_layout: "comic_chain" values migrate to the canonical arrangement.

SpeechPayload.validate accepts old actions without the new top-level keys, and validates the keys when present (reveal in REVEAL_MODES, pop_in a bool). The scene decoder currently validates before it normalizes, so this backward compatibility is required there. StageSceneIO._encode_speech includes the new action-level fields in its explicit speech allowlist and writes a normalized speech action with synchronized bubble visuals. _decode_speech continues to accept pre-change scenes and returns the normalized action. ActionRegistry.to_rule_action and from_rule_action add reveal and pop_in to their explicit speak field lists; this prevents event-rule speech from silently losing the choices. Draft JSON uses the full action dictionary and therefore carries the fields naturally. No .scene file version bump is needed because all new keys are optional to the decoder.

SpeechComposer._new_bubble copies action-level reveal and pop_in into its visual; SpeechPayload.normalize enforces the same contract for actions created elsewhere. SpeechPayload.duration_for() should use the action-level reveal value when present and the first bubble fallback for legacy actions, or consume its synchronized normalized bubble copies. SpeechBubble.render_action() already normalizes before building labels and entrance scales, so runtime and editor preview share the same choices. Avoid calling normalize once per frame inside each individual bubble render; normalize once at the action boundary as it does today.

4. Lifecycle and acceptance

  1. Open a new speech or a saved speech: the group panel appears beside the group; no bubble is dimmed or highlighted as selected. Click a bubble or token: group panel hides and Speech Style appears. Done styling or empty stage click returns to the group panel, without changing the draft's text or positions.
  2. Choose each Bubbles, Words appear, and Bubble motion option: the selected border persists on hover; all existing bubbles use that choice on Preview speech. Add another bubble and preview again: the new bubble uses the same reveal and motion, regardless of which bubble was previously edited.
  3. Save speech, save the scene, reload it, and reopen queue editing: the three choices are selected correctly and playback matches. The same fields survive a reactive rule round trip and recoverable draft restore.
  4. Load a pre-change scene with mixed per-bubble reveal or motion values: the group panel reflects the first bubble, and every bubble follows that choice when edited or replayed. Existing legacy single-text actions still load and play; old scenes are not rejected for missing action-level fields.
  5. On a narrow stage, drag a bubble, pan/zoom the camera, and resize the window: the panel remains adjacent when space allows, stays within stage bounds, and does not obscure its choice labels. Its buttons do not trigger a bubble click or an object interaction behind it.
  6. During a full replay and while Message is open, group choices cannot be changed accidentally. Save speech and Cancel retain their current commit/discard semantics, and the inspector no longer displays the Show card.

5. Tradeoffs

  • Keeping synchronized visual.reveal and visual.pop_in copies avoids a broad renderer rewrite but duplicates two values in saved JSON. SpeechPayload.normalize() must remain the single synchronization boundary; future speech writers should use it before serialization.
  • Migrating mixed legacy bubbles from the first bubble makes the new group panel truthful and deterministic. It intentionally removes later bubbles' distinct reveal/motion choices when they are normalized into this speech-wide model.
  • A dedicated stage panel makes the group scope visible without adding a button or mode, at the cost of one more transient Control node and adaptive placement logic. A separate focus index preserves existing Message and draft recovery behavior.

6. Dialog layout amendment (2026-10-02)

Keep the whole-speech controls visually distinct from per-bubble styling:

  • Show bubbles has only Show all and One at a time.
  • Bubble layout has Stack and Same spot.
  • Connect bubbles is a separate on/off switch. Turning it on selects Stack if needed; choosing Same spot turns it off. It is available only when there are at least two bubbles.
  • With One at a time and Connect bubbles on, reveal one new bubble per step while keeping previous bubbles visible as part of the chain. With Show all, all connected bubbles are visible together.
  • More speech settings starts collapsed and contains Words appear, Bubble motion, Timing, and the reactive-only Stop current action option.
  • Preview speech, Done speech, and Cancel speech stay in a footer outside the scrolling content. Done speech commits the speech action; Cancel discards the editing session.
  • The panel stays beside the bubble group when space allows, otherwise moves below or above it. Cap its width to preserve stage visibility; scroll the settings content on short stages while keeping the footer reachable.
┌──────────────────────────────┐
│ Show speech                  │
│ Show bubbles                 │
│ [Show all] [One at a time]   │
│ Bubble layout                │
│ [Stack] [Same spot]          │
│ 🔗 Connect bubbles [Off/On]  │
│ ▸ More speech settings       │
│                              │
│ [▶ Preview] [Done] [Cancel]  │
└──────────────────────────────┘