Files
sacred_k/docs/hud.md
T
khallaandClaude Opus 5.5 a7f8496b2b Quest dialogs: stay centred, clickable, closable with Esc
- Quest dialogs are tooltips. A tooltip now keeps the offset it gets when it
  becomes visible (the hovered window's, or centred for dialogs) and records
  it for hit tests; its own event handler is shifted too. Hidden windows no
  longer count as hovered.
- Esc closes a visible dialog the way its answer buttons do: hide, then
  cDialogManager::Answer with no answer (ends the input lock, nothing chosen).
- Autotest can select a save game row (-Save).
- Names for the UI manager, tooltip and dialog functions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 00:39:29 +02:00

89 lines
4.8 KiB
Markdown

# HUD layout in widened views (`src/mods/hud.cpp`)
The UI is laid out for 1024x768. In a widened render target (e.g. 1835x768)
the windows are moved as a whole, without touching their own coordinates:
the bottom bar and all dialogs are centred, the party panel (portrait, health)
sits at the right edge. Main menu and loading screen keep the classic 1024x768
frame (see `widescreen.md`).
## How a window is moved
A window's render and event methods are hooked (`HUD_RENDER`, `HUD_RENDER3`,
`HUD_EVENT`). While one of them runs on a thread, everything the window sees or
draws is translated by its offset `dx` (outermost window wins, children and
shared base methods are inside the scope):
| What | Where | Translation |
|---|---|---|
| screen-space vertices (`XYZRHW`) | `IDirect3DDevice7::DrawPrimitive` / `DrawIndexedPrimitive` (vtable 25 / 26) | x + dx |
| 3D items (belt, equipment) | ortho projection centred on the screen | none needed |
| mouse events | `cEvent_mouseMove` x at +4 (previous) and +0xc; button down / up x at +8 | x - dx, restored afterwards |
| mouse cursor object (`*0xcdbadc`, accessor `0x6550f0`) | x at +4, previous x at +0xc (polled for hover, item pick-up `0x6559a0`) | the accessor returns a per-thread copy with x - dx; dwords the window changed are written back afterwards |
| `GetMouseClientPos` (`0x66e500`) | polled position | x - dx |
## Calls outside a window's scope
- Tooltips (`cUI_Tooltip::vf14` `0x6e7300`) are separate windows that the
hovered window positions in its own layout: they draw with the offset of the
moved window under the cursor (noted while windows render, expires after
150 ms).
- Hit tests (`vf1c`: `0x731d40`, taskbar `0x6dfb10`) are also called by the UI
manager with plain render coordinates, e.g. "is UI under the cursor" for
world picking (`0x75a370`). They use the offset the top-level window (parent
chain at +0x50) was last drawn with.
- The item carried by the cursor (`0x6555e0`, drawn by the render loop) and the
manager's status line / message box (`0x75bbf0`, `0x75c200`, laid out around
x = 512) draw with the centre offset.
## UI events
`vf04` of an event is its category: 1 mouse move, 2 button down, 3 button up,
4 key (code at +8), 5 UI command (`cEventUI2`: id at +4, vf08 subtype). Only
mouse events carry positions; category 5 events with subtype 0x10 test a point
at +0x48 in `cUI_Window2::vf10`, but commands such as "open ESC menu" (0x11)
keep unrelated data there.
## Why the cursor is copied, not shifted
UI windows render on the render thread while world input (picking,
hold-to-move, `0x611f10`) reads the same cursor object on another thread.
Shifting the shared object for the duration of a window's scope let the world
sample a position ~400 px off now and then (jerky hold-to-move). Every user of
the cursor goes through the accessor `0x6550f0`, so inside a moved window it
returns a thread-local copy instead (with its own CRITICAL_SECTION at +0x24;
copying and writing back hold the real object's lock).
## Full-screen layers
A primitive drawn inside a moved window that covers the whole 1024x768 layout
(e.g. the dimming behind the exit confirmation) is stretched across the render
width instead of shifted.
## Standalone controls (quest dialog)
Some dialogs (e.g. accepting a quest) are not one of the hooked window
classes: their buttons and texts sit directly in the UI manager as top-level
controls (measured: the clicked button's root window was the cUI_Button2
itself). They were drawn unshifted or with whichever window's scope happened
to be active, so the dialog jumped between the 1024 layout and the centre and
clicks only hit at the unshifted position. `cUI_Button2`, `cUI_Static2`,
`cUI_StaticText64`, `cUI_StaticText64FX` (render and event) and
`cUI_TextLines` (render) are hooked as centred windows too; inside other
windows they are in that window's scope already (outermost wins).
## Tooltips and quest dialogs (2026-10-16)
Quest dialogs are tooltips: `cUI_Manager::tooltipCreate` (`0x75a870`) hands
out a panel from a pool at cUI_Manager +0x124 (`UI_WND_TOOLTIP`, created
1024x768, flags 0x804 while hidden); the quest dialog used one at 136,306,
752x157 (flags 0x805). Tooltips used to be drawn with the offset of the moved
window under the cursor in every frame, and without recording an offset for
hit tests: dialogs jumped to the 1024 frame when the cursor left the UI, and
their clicks only hit at the unshifted position. Now a tooltip keeps the
offset it gets when it becomes visible (the hovered window's, or the centre
when no moved window is under the cursor: a dialog) and records it for hit
tests. Visible dialog tooltips: `mods::hud::VisibleDialogTooltips`.
The standalone control hooks (buttons, texts) above turned out not to be what
the quest dialog is made of; they stay, for top-level controls in general.