Input and focus¶
When several panels overlap on screen, only one of them owns the
mouse and keyboard for that frame. This page explains how Interlude
UI resolves that owner each frame, what ConsumeInput and
PanelConfig.AcceptsInput do, and how mouse hit-testing works.
This page is the canonical reference for focus resolution. The Panels and modals guide introduces the priority order briefly; the full model lives here.
What focus resolution does¶
Several panels can be visible at once, and many can overlap the mouse in the same frame. Input-focus resolution picks the single panel that owns the mouse and keyboard for that frame, so a click reaches one panel and the panels beneath it stay inert.
The priority order¶
BeginFrame runs three steps:
- Blocking panels (panels with
BlocksInput = true, set byShowModal) are sorted to the end of the panel list. This makes them "topmost" for the walk that follows. - The frame walks the panel list once and remembers the last panel that matched.
- A panel matches if
BlocksInputis true, or ifAcceptsInputis true and its rect contains the mouse.
The last match wins. Nothing earlier in the list sees input that frame.
This is why the built-in debugger overlay (TICK_DEBUGGER) stays
reachable even when a modal is open: the debugger panel is itself
shown via ShowModal, so any later modal still slots in after it
in the blocking-panel suffix and gives input back when it closes.
How mouse hit-tests resolve¶
BeginFrame reads m_input.GetMousePosition() and inverts the Y
axis to match layout space. The flipped point feeds the priority
walk above. For each candidate panel, the test is
LayoutToScreen(window.Rect).Contains(mousePos): convert the panel
rect from layout space to screen space and check containment.
The Y-flip is the same one applied throughout the input path. For the broader coordinate-space model, refer to Coordinate spaces.
Clip-stack intersection¶
Every pointer hit-test intersects the widget's screen rect against
the active clip rect before checking pointer position. This applies
to both hover detection (IsHovered) and click detection
(Button).
If the intersection is empty (the widget is entirely outside the
clip rect), the widget can't be hovered or clicked, regardless of
where the pointer is.
This means geometry clipped from view is also clipped from
hit-testing. The most common case is a button inside a scroll view
that has been scrolled out of the visible port: the button is not
renderable at the pointer's position, so it's also not pressable
there. The same rule applies to any PushClipRect scope: content
pushed past the clip boundary becomes inert to input.
For details on the clip stack itself, refer to Stacks. For the scroll-view patterns that rely on this behaviour, refer to Add a scroll view.
What focus means in practice¶
The panel selected by the priority walk becomes the hovered panel
for the frame. Widgets inside the hovered panel receive
IsHovered, click, and keyboard events. Widgets outside it still
render, but their Button and Textbox calls do not respond to
input that frame.
Focus is resolved once per frame, in BeginFrame. After that, it
is constant for the rest of the frame. Calls into your DoUI body
read the same hovered panel from start to end.
ConsumeInput¶
ui.ConsumeInput() claims the current frame's input from inside a
panel body. It records that input was consumed; EndFrame carries
the flag into the next frame and then clears it, so the consumed
state is visible for exactly one frame.
For full-screen modal dialogs, you generally don't need it: a
panel shown via ShowModal already blocks input to anything
underneath. Reach for ConsumeInput when you have a non-blocking
panel that still needs to swallow clicks inside some sub-rect (for
example, a transparent HUD overlay where clicks within a button
shouldn't fall through to the scene beneath).
Exclude a panel from input¶
A PanelConfig with AcceptsInput = false (the shape that
PanelConfig.OverlayDefault gives you) makes a panel that draws
but never claims input, regardless of whether it overlaps the
mouse. Read-only HUDs that render on top of the game but should
not intercept clicks use this.
The panel is skipped during the matching step of the priority walk above. It still renders normally; it simply never wins focus.
Popup auto-close¶
Popups (panels with AutoCloseOnOutsideClick = true, the shape
that BeginPopup gives you) get a separate behaviour: BeginFrame
watches for mouse-button-down events and walks the open popups.
Any popup whose rect does not contain the mouse position hides
itself automatically.
To combine outside-click dismiss with input-blocking, set the
config explicitly and show via ShowModal:
new PanelConfig { AutoCloseOnOutsideClick = true,
BackdropColor = ... }, then ui.ShowModal(id).
Pitfalls¶
Two modals open at once. : The last blocking panel in the panel list owns input. Closing it transfers input to whichever blocking panel is now last. If you need multiple modal layers, be deliberate about open order.
The modal has no dim background.
: PanelConfig.BackdropColor is missing or null. Set it to a
translucent colour; the backdrop draws automatically before
the panel body runs.
A normal panel never receives input.
: A modal panel is open, even if it is not visually obvious.
Modal panels with no on-screen body (an empty BeginPanel
block, for example) still block input below them. Hide the
modal when it is not in use.
A panel intercepts input you didn't want it to.
: You showed it via ShowModal. Modal panels block everything
underneath. Switch to ShowPanel to keep the panel visible
without blocking, or close it before handing input back.
Additional resources¶
- Panels and modals: the broader panel context, including the visibility state machine.
- Frame lifecycle: where focus resolution sits in the per-frame flow.
- Stacks: the panel stack, and the per-panel
BlocksInputflag that drives modal behaviour. - Show a modal: the procedure that applies the patterns described here.
- Navigation: a separate focus model that routes directional input, confirm, and cancel inside the hovered panel.