Skip to content

Client Windows

Ownership

client/cl_window.c owns transient server-authored gameplay windows. Persistent singleton HUD planes remain svc_layout layers. Do not add a UILAYOUTLAYER member for an inventory, journal, quest description, or other spawnable window.

The server owns a window's instance ID, opaque game-local class ID, flags, frame tree, and close lifecycle. The client owns z-order, keyboard focus, pointer capture, and the local drag offset. Updating an existing instance preserves its drag offset. Opening a UI_WINDOW_UNIQUE class replaces and focuses its existing instance.

Portrait cameras in transient layouts use a client-assigned namespace per open window instance, combined with the frame number. Replacing or closing a window releases every portrait camera ID in that namespace so retained particles and animation events cannot leak into a later window that reuses the same frame numbers. Persistent HUD layouts keep their layer-based IDs.

After opening a pause-owning modal svc_window, the client compares pause-owner presence with its last reported pause state. The first modal without UI_WINDOW_NO_PAUSE sends pause 1; closing the last pause-owning modal sends pause 0. Opening or closing nested modals sends nothing while a pause owner remains. The game keeps this client request as an independent pause owner, so it cannot clear a script-owned PauseGame(true). UI_WINDOW_NO_PAUSE keeps modal input capture while opting that window out of this pause handshake; the Warcraft III Allies dialog uses this split.

Windows form a doubly linked list. The head is backmost and the tail is frontmost. Drawing walks head to tail; pointer hit testing walks tail to head. Left-clicking a window unlinks it and appends it at the tail, making draw order and keyboard focus change together. Closing the focused window focuses the new tail.

SCR_DrawLayout() draws persistent layout layers first and then calls CL_WindowDraw(), placing transient windows above the HUD. Parsing and retaining svc_window does not make a window visible by itself; the screen layout pass owns submission.

The legacy menu.dll is a menu module, following the Quake II key_dest split: cl_scrn.c refreshes it only for key_menu, and cl_input.c forwards its keyboard/mouse events only for key_menu. The active-game pass draws server-authored layout and windows through SCR_DrawLayout()/CL_WindowDraw(); it must not call a UI-DLL game overlay or send UI-DLL input.

WoW incoming messages use FT_MESSAGE_QUEUE on LAYER_MESSAGE. The server emits one compact record per unread message and, when selected, one additional record carrying the server-owned popup text. cl_scrn.c draws both the unread icon pool and the popup; clicks send message_open <id> or message_close back to the server, which updates the authoritative layer.

UI_WINDOW_PRESENTATION is a separate, opt-in full-surface ownership flag. While such a window exists, the active-game draw path paints a black background and only the presentation window(s), skipping the world scene, persistent HUD layers and other transient windows. The window remains modal for input and uses normal pause/close arbitration; closing it automatically restores the ordinary view without persisting or mutating player UI flags. Warcraft III's retail score screen is its first caller; the client does not know score IDs.

UI_WINDOW_MODAL, UI_WINDOW_NO_PAUSE, UI_WINDOW_NO_ESCAPE, and UI_WINDOW_UNIQUE are independent. Modal means the topmost modal window consumes input outside its bounds. NO_PAUSE means that modal does not acquire the client-owned simulation pause. NO_ESCAPE consumes Escape without dismissing the window, for mandatory result/decision flows that must close through an explicit action. Unique means only one instance of that class may exist. Inventory and quest-detail windows can be unique without being modal; confirmation-style windows are normally modal pause owners, while the WC3 Allies and game-result windows are modal plus NO_PAUSE for their respective gameplay/script-owned timing semantics.

Wire Format

svc_window
  byte open
  long instance_id

UI_WINDOW_OPEN:
  long class_id
  long flags
  window frame records
  long 0, short 0
  long text_size
  byte text[text_size]

Window frame records retain the normal one-byte type-specific payload size. The text, tooltip, and onclick fields differ: they are 32-bit offsets into the trailing text arena instead of inline strings. Offset zero means no string; byte zero of every arena is therefore NUL. The client validates every nonzero offset and requires a terminating NUL within text_size before retaining the packet.

The arena is bounded by MAX_MSGLEN, not the 255-byte typed-payload field. This allows quest descriptions, message histories, and other long text blocks without inflating frame-specific structs. SCR_ClearWindow resolves offsets directly into the retained arena; it does not duplicate strings.

Input

  • Mouse-down on a window raises it and assigns keyboard focus.
  • Pointer events inside a client-managed window are consumed even when the window has no matching scrollable control; this keeps wheel bindings from also acting on the world behind the window.
  • Mouse-down on non-command background starts a drag when UI_WINDOW_MOVABLE is set.
  • Drag capture continues through motion and mouse-up outside the window.
  • Keyboard hotkeys are searched only in the focused window, or the topmost modal window.
  • While an edit box has focus, printable key-downs are consumed before button hotkeys are considered. The corresponding SDL text-input event owns character insertion, so letters such as A/S/D can be entered in save filenames without activating dialog buttons. After the edit loses focus, normal button hotkeys resume.
  • Escape closes the active window unless it has UI_WINDOW_NO_ESCAPE; a no-Escape modal consumes the key and stays open.
  • Clicking outside all non-modal windows clears focus.
  • A modal window blocks world hit testing, control groups, bindings, minimap actions, and manual camera movement.
  • A focused non-modal edit box yields LEFTARROW and RIGHTARROW to gameplay bindings, matching the existing Up/Down fallthrough so WC3 camera scrolling remains symmetric. Modal edit boxes keep all four arrows for local input and therefore do not move the world camera behind the dialog.
  • Key-up still reaches gameplay +command releases so opening or focusing a window cannot leave an input held.
  • close_window_command <command> forwards the suffix to the server and then closes the owning window; use it for transactional Accept/Cancel buttons that must commit or discard server-owned draft state before dismissal.
  • blackout_window_command <command> is valid only for a server-authored modal UI_WINDOW_PRESENTATION window. It immediately blocks local interaction, fades a black overlay over the window using client time, and forwards the suffix once the fade reaches full black. It retains the modal/presentation window throughout, so the renderer never reveals the outgoing world. A late same-ID window refresh preserves the fade and cannot re-show artwork after it completes. Navigation and eventual window removal remain server/session-owned; ordinary game windows cannot use this action to mask the game world.
  • Scrollable TextArea state is client-owned. Because a retained svc_window packet is reparsed for each draw/hit test, client/cl_window.c stores scroll fractions by frame number and reapplies them after each parse. Mouse wheel input, scrollbar arrows/track, and thumb dragging update that retained value rather than mutating only the temporary parsed frame.
  • Transient FT_EDITBOX/FT_GLUEEDITBOX text is client-owned for the window lifetime. The wire uiEditBox_t carries a stable control id and maxChars; client/cl_window.c retains text/cursor state by frame number, routes SDL text input through the same menu_text_input.h helpers used by front-end FDF edit boxes, and reapplies the text after each retained-layout parse. Frame numbers are scoped to one serialized layout, not globally unique across the HUD and transient windows. Live edit-text and cursor lookup must therefore be restricted to the transient window whose layout is currently prepared. Looking up retained edit state by frame number across every open window can replace an unrelated persistent HUD label that happens to reuse that number. Gameplay snapshots repeatedly call CL_SetGameplayInput(), so that function must preserve SDL text input while CL_WindowTextInputActive() reports a focused transient edit box. Unconditionally calling SDL_StopTextInput() there leaves the edit logically focused but prevents SDL from producing SDL_TEXTINPUT. Blur/close releases the transient text-input owner, and returning from the console to gameplay restores it when the edit is still focused.
  • Transient FT_LISTBOX selection is also client-owned. The client retains the selected row and scroll fraction. List rows may use display\thidden-value: drawing hides the suffix, while command placeholder expansion returns it.
  • An authored onclick may reference {ControlId}. Immediately before forwarding, the window client replaces that token with the current edit-box text or selected list value and escapes quotes/backslashes. This is a commit-time control-value handoff, not a per-keystroke client-to-server state stream. The WC3 named Save/Load panel is the first consumer.
  • Checkbox and slider controls may edit client cvars locally instead of sending a command; see Server-authored cvar controls.

Server-authored cvar controls

Options-style pages are authored by the game module but edit preferences that live on the client. The window client (client/cl_window.c) carries no list of preference names; three pieces cooperate instead:

Piece Owner Role
CVAR_UI flag the subsystem that registers the cvar Declares "server-authored UI may edit this". Set at Cvar_Get time, e.g. s_sound/s_volume in common/cvar.c, s_music/s_musicvolume in CL_MusicInit.
Control table the authoring game module One { control, cvar, checkbox\|slider } row per control. WC3 Sound page: options_cvar_controls[] in games/warcraft-3/game/hud/hud_menu.c. Each row becomes the control's onclick.
Binding + transaction client/cl_window.c Decodes the action, checks the flag, applies the edit live, and logs previous values for Cancel.

Actions (all defined in common/shared.h, all consumed locally):

Action Effect
local_cvar_checkbox <cvar> Click toggles the cvar between 0 and 1.
local_cvar_slider <cvar> Click/drag on an FT_SLIDER stores the normalized 0..1 position as %.3f.
local_cvar_begin_command <cmd> Opens an edit transaction, then forwards <cmd> to the server.
local_cvar_accept_command <cmd> Ends the transaction keeping the edits, then forwards <cmd>.
local_cvar_cancel_command <cmd> Restores every cvar edited since Begin, ends the transaction, then forwards <cmd>.

Trust boundary. A binding is honored only when the named cvar is already registered and carries CVAR_UI (Cvar_Flags(name) & CVAR_UI). A server therefore cannot edit renderer, network, or filesystem cvars, and cannot create new cvars by naming them. A rejected binding is dropped with a stderr diagnostic; it is never forwarded to the server as a command. To expose a new preference, add CVAR_UI where the cvar is registered and add a row to the authoring table; no change to cl_window.c is needed.

Display. On every window prepare the client overwrites the value of each bound frame from the current cvar, so the control always shows the live client value regardless of what the server serialized.

Transaction lifecycle.

  • Begin opens an empty log. The first edit of each cvar records the value it held before that edit (up to 32 cvars). Edits apply live, so audio changes are audible while the page is open.
  • The transaction is client-global, not per window: the Options panel is re-sent as the page changes, and closing or replacing an individual window by id leaves the transaction open.
  • A second Begin inside an unfinished transaction keeps the existing log. Leaving the page through a plain navigation button and re-entering therefore still lets Cancel restore the values from before the first Begin.
  • Accept and Cancel end the transaction. Edits made while no transaction is open are immediate and not restorable.
  • Escape is Cancel. When Escape dismisses a window that carries any local_cvar_* action while a transaction is open, the client restores the logged values before closing it. Escape on an unrelated window leaves the transaction untouched. Escape sends no server command, exactly as for other windows.
  • A full CL_WindowClear() ends an unfinished transaction without restoring it, preventing an old log from crossing a client-state/session reset.

Known limits. Sliders only store the normalized 0..1 fraction (no authored min/max/step), checkboxes only store 0/1, an edit past the 32-entry log is applied but not restorable, and logged values are truncated to 31 characters.

Client-owned button actions

Most authored window onclick strings are sent back to the game server. Besides the local_cvar_* actions above, four explicit action tokens are consumed locally by client/cl_window.c instead:

  • UI_WINDOW_CLOSE_ACTION (close_window) closes the owning window;
  • UI_WINDOW_CLOSE_NOTIFY_ACTION (close_window_notify) closes it and therefore participates in modal-list pause synchronization;
  • UI_WINDOW_DISCONNECT_ACTION (disconnect_game) defers a full world-to-front-end transition: disconnect the client, shut down the local server/game module, clear map-scoped renderer state, rebuild menu/FDF state, then show the front-end;
  • UI_WINDOW_QUIT_ACTION (quit_application) queues normal application shutdown.

Disconnect and quit are intended for explicit local activation in server-authored menus. They are never executed merely because a window packet is received. disconnect_game must use the deferred session-boundary action rather than calling CL_Disconnect() directly: gameplay teardown can clear the FDF/template pool, so showing menu_main before server/game shutdown leaves the front-end with missing bindings. Submenu/navigation actions remain ordinary server commands.

Legacy UI Windows

svc_ui_window retains the older WoW ui.ShowWindow named-XML toggle. It is migration debt and does not participate in the generic linked list, focus, dragging, or modal ownership. New gameplay windows must use svc_window.

Validation

make openwarcraft3
make test
build/bin/openwarcraft3 -data 'data/Warcraft III' -roc +set skip_cutscene 1 \
  +map 'Maps/Campaign/Human02.w3m' +exec /tmp/openwarcraft3-quest-window.cfg +com_frame_limit 175
build/bin/openwarcraft3 -data 'data/Warcraft III' -tft +set skip_cutscene 1 \
  +map 'Maps/Campaign/Human02.w3m' +exec /tmp/openwarcraft3-log-window.cfg +com_frame_limit 175

Each temporary config contains 150 wait commands followed by cmd quests or cmd log, then screenshot 5. This delay lets Human02 reach ca_active before the command is forwarded to the loopback server. A main-menu launch does not test this protocol: there is no connected game server to serialize or parse svc_window.

The connected Quest run found that authored FDF frames must use PF_UIWINDOWFRAME while ui_window_writing is true. Sending them as PF_UIFRAME made the inline-string codec dereference the first arena offset (0x1). Proxy and authored frames must both select the offset-aware codec. wc3_game.hud_authored_window_frame_uses_offset_codec guards this boundary.

The standalone net tests cover text offsets, a text arena above 255 bytes, screen-pass drawing, unique-class replacement, linked-list raise order, keyboard focus, non-modal/modal edit-box arrow ownership, and malformed packets without a frame terminator. The window_cvar_* tests cover cvar controls: live checkbox/slider edits, rejection of unflagged and unknown cvars, Accept/Cancel, Escape-as-Cancel, repeated Begin, and transaction reset on CL_WindowClear().

Standalone scrollbar callbacks

A server-authored standalone FT_SCROLLBAR can optionally set OnClick to an ordinary server command prefix. When a user moves the thumb, clicks the track, or wheels over that scrollbar, the generic window client appends its normalized [0,1] position to the authored command and forwards it. No command is sent when a window is opened/refreshed or a scrollbar is restored from a snapshot. The server must still authenticate the recipient, validate the numeric range, and map the value to its own content. Native text areas/listboxes keep their existing client-owned scrolling and are not forwarded. Warcraft III uses this for the dynamically authored score-table viewport; the client does not know about score rows, players, or Warcraft frame names.

See Also

Live edit-box rendering

A transient edit box keeps its in-progress value in client-local window state. The child STRING frame is only the initial server-authored value. String rendering therefore resolves a live edit value for edit text children before falling back to frame->text; otherwise the cursor can move while newly typed characters remain invisible.

Transient LISTBOX controls may own a direct SCROLLBAR child. List scrolling uses row-count/visible-row metrics: wheel and arrow clicks advance one row, track/thumb input maps proportionally across the scrollable row range, and the scrollbar is omitted from drawing when all rows fit.

Transient edit-box text rendering uses the parent edit control's serialized font and text color; the child STRING carries the value and geometry but does not independently own presentation.

  • Transient edit-box rendering is parent-owned: the edit control draws the live client-side value directly into its authored STRING/TEXT child rectangle using the serialized edit font/color; the child remains a geometry/value carrier and is not drawn separately.

  • Edit-box text is clipped to the parent control's inner backdrop rectangle, not the authored child STRING width; the child still supplies vertical placement. This keeps editable text/cursor width aligned with the visible input background.

Text entry and pointer activation

The shared client/menu_text_input.h engine holds an anchor and caret for edit selection. Shift+arrow extends the selected range, plain left/right collapses it, Ctrl+A selects the entire buffer, and typing, Backspace, or Delete replaces/removes the selection. SDL supplies its normal delayed repeat events for text editing: printable text arrives through SDL_TEXTINPUT, while editing keys (Backspace, Delete, arrows, Home/End) are allowed to repeat only into the focused edit widget. The gameplay binding path still ignores repeated key-downs.

A clickable client-window frame activates only if the same window and frame received the left-button press. Standalone Warcraft menu frames and persistent gameplay HUD controls likewise track which control received the press and suppress mismatched releases. This avoids accidental activation when pressing outside and releasing over a button.

The first edit box in a newly opened client window starts with its existing text selected. In the Warcraft III Save dialog, the timestamp is therefore selected and typing replaces it immediately; arrows collapse the selection. Transient edit state retains caret and anchor. Selection uses authored skin colour when available, otherwise grey.

The edit payload (uiEditBox_t) is unchanged. For edit frames only, the existing uiFrame_t.color carries optional FDF EditHighlightColor (it is otherwise unused by the edit renderer). Initial selection is local state, not an extra wire flag. This preserves the existing network layout exactly. Closing or replacing a window invalidates its press/drag capture. When a save list row changes the editable filename, the caret and selection anchor both move to the end of the new name.