Pure surface registry: the single source for "what is where on screen".
Given the frame's editor state, placements/1 returns an ordered list of
MingaEditor.Layout.SurfaceRegistry.Placement entries, each carrying a
surface_id, a rect (terminal cells, the existing Layout.rect/0
convention), a z band, and a hit_kind. The list is ordered back-to-front
by z (lowest first), so a stable sort by z reproduces paint order and a
reverse walk reproduces hit-test precedence.
This module is a calculation, not a process: state in, placements out. There
is no ETS, no GenServer, no cache of its own. It is consumed where
MingaEditor.FocusTree/MingaEditor.Layout are consumed today.
The input rule (design of record, epic #2330)
Clients resolve clicks on content they render and send semantic intents
(gui_actions); the BEAM owns placement, stacking, and containment for
registry-placed surfaces. That is the governing line for all surface input.
A frontend hit-tests its own rendered content (a completion row, a
notification action, an observatory node) and emits an intent like
"item N clicked", exactly as SwiftUI's native hit-test already does. The BEAM
does not re-derive what a click means on rendered content. What it owns is
structure: which rect a surface occupies (placements), which surface wins when
rects overlap (z-order arbitration, since stacking depends on editor state only
the BEAM has), and containment, so a click that misses every interactive
element of a registry-placed surface is swallowed instead of falling through to
the buffer underneath (MingaEditor.Input.OverlaySink). The picker is the one
documented exception: it predates this rule and stays BEAM-resolved as shipped.
One source, derived from the focus tree
The registry is built by flattening the existing MingaEditor.FocusTree. The
focus tree is already the BEAM's authority for mouse routing: it carries the
per-frame Layout rects plus the single active overlay, with children stored
in rendered z-order (back to front). By projecting that same tree into
placement entries, the registry rect for every surface is, by construction,
the exact rect the focus tree (and therefore every hit-test that routes
through it) uses. That is the behaviour-neutrality guarantee the epic asks
for: registry rect == the rect each handler previously computed, because both
read the same tree.
Scope honesty: single active overlay
FocusTree.add_modal_overlays/3 encodes the same exclusive precedence that
Go's overlayLines() chain encodes today: at most one modal overlay (picker
OR completion) is live per frame. The registry preserves that decision as
data; it emits the single active overlay and nothing else. Multiple
simultaneous overlays are newly expressible as a list but are deliberately
NOT produced here. Enabling them is out of scope (see #2268, AC-4).
Enumeration history (#2268 -> #2281). The Go compositor's overlayLines() chain
once stacked surfaces that were not focus-tree nodes via a hand-ordered rank
table. That table is now gone: every overlay surface is a focus-tree node with a
BEAM-authoritative rect.
Cursor-anchored popups: hover popup, signature help. Both are floating popups whose exact on-screen box the BEAM computes for layout (
HoverPopup.box/3/SignatureHelp.box/3, driven byFloatingWindow).FocusTree.add_floating_overlays/2adds them as overlay nodes fromshell_state; they occupy the@z_floating_overlayregion (hover 290 > signature help 280).Footer-band secondary overlays (#2281): float popup, agent context, tool manager, extension panel, observatory, edit timeline, notifications, extension overlay. The owner ruled these mouse-driven (#2330), so the BEAM owning their footer-band geometry is now the designed layout.
FocusTree.add_footer_band_overlays/3adds each visible one (perMingaEditor.Layout.FooterOverlays) as an overlay node with a bottom-anchored full-width rect fromMingaEditor.Layout.OverlayBand(porting the GomaxOverlayHeightclamp). They carry the exact historical stacking z (270/260/240/190/180/170/160/150), so Go composites the single highest-z winner by its placement rect instead of footer-appending. The single-active model still holds (#2268 AC-4): the tree may express several placements, but Go renders one. Their click events route toMingaEditor.Input.OverlaySink, which swallows mouse events so a click over a visible overlay never reaches the buffer underneath (AC-2). Per-surface activation semantics land as epic #2330 children.
surface_id namespace and the identity-unification call (#2268)
surface_id/1 maps each focus-tree content_type to a stable atom in the
registry's namespace; surface_id_u16/1 maps that atom to its u16 wire
value and hit_kind_u8/1 maps a hit_kind atom to its u8 wire value. This
module is the single source of both mappings.
Unification decision (#2268 proper): the schema (surface_placement in
docs/protocol_schema.toml, generated decoders on every frontend) carries
surface_id as a raw u16 and hit_kind as a raw u8. It deliberately does
NOT add a surface_id/hit_kind enum: that would be a new schema vocabulary,
and the consult's instruction was to keep the schema as the cross-language
source of truth without a new vocabulary. The cross-language source of truth
is therefore the wire shape plus the generated codec; the numeric identity of
each surface stays authoritative here, and the emitter
(Minga.Frontend.Adapter.GUI.SurfaceLayoutEncoder) consumes these functions
rather than re-deriving numbers. One writer (this module), one reader (the
encoder). hit_kind_u8/1 reuses the window-encoder hit-kind numbering
(Minga.Frontend.Adapter.GUI.WindowEncoder 1..6) and extends it with
:chrome (7) and :overlay (8) so a placement's hit kind and a window hit
region speak the same u8.
hit_kind
hit_kind reuses the window-scoped convention already encoded by
Minga.RenderModel.Window.HitRegion and Minga.Frontend.Adapter.GUI.WindowEncoder
(:text, :gutter, :fold_control, :modeline, :status_bar, :divider).
Surface-level entries extend it with :chrome (structural chrome that routes
to a handler but is not buffer text) and :overlay (a modal overlay surface).
It is a coarse classification of what a click on the surface means, not a
precise intra-surface region; intra-window hit regions stay with the window
encoder.
What is NOT unified here (documented per the epic)
Several handlers compute a region's interpretation from math that is too entangled to swap behind a rect lookup without rewriting interaction semantics. For those, the registry is the source of the surface RECT, but the handler keeps its own interpretation:
MingaEditor.Input.AgentMousesplits the agent window content rect into a chat sub-column vs a preview sub-column usingchat_width_pctmath, and splits the prompt area off the bottom usingPromptRendererheight math. The registry emits the agent window/panel rect; the chat/preview/prompt sub-division stays inAgentMouse(it is interaction semantics, not a placed surface). Forcing it into the registry would mean inventing sub-surfaces that nothing else places.- Tab-bar and modeline segment click regions
(
tab_bar_click_regions,modeline_click_regions) are authored at render time as text-property spans, not rects. The registry places the tab_bar and status_bar/modeline surfaces; the per-segment command lookup stays where it is. - Intra-window buffer geometry (gutter width, fold column, scroll position to
buffer line) stays in
MingaEditor.Mouse.HitTest. The registry places the window content rect; translating a cell to a buffer position is window interpretation, not surface placement.
These are left intentionally. The registry's job in this slice is to be the one authority for surface rects and z-order, not to absorb every handler's interpretation of a click inside its surface.
Summary
Types
Coarse classification of what a click on a surface means.
A surface identity in the registry's namespace.
A placement projected to its wire shape: surface_id/hit_kind already mapped to
their numeric identity, rect as a {row, col, width, height} cell map, z verbatim.
Functions
Half-open rect containment: row in [r, r+h) and col in [c, c+w).
Builds placements from an already-constructed focus tree.
Maps a registry hit_kind atom to its u8 wire value.
Returns the frame's surface placements ordered back-to-front by z.
Returns the rect of the first placed surface with surface_id, or nil.
Like rect_for/2 but reads an already-computed placement list.
Maps a focus-tree content_type to a registry surface_id, or nil for
content types that are not independently placed surfaces (the viewport root
and the per-window :window container, whose content child is the placed
surface).
Maps a registry surface_id atom to its u16 wire value.
Returns the frame's placements projected to their wire shape.
Returns true when (row, col) falls inside the placed surface surface_id.
Types
@type hit_kind() ::
:text
| :gutter
| :fold_control
| :modeline
| :status_bar
| :divider
| :chrome
| :overlay
Coarse classification of what a click on a surface means.
@type surface_id() ::
:tab_bar
| :editor_area
| :window
| :buffer_content
| :agent_chat_window
| :agent_chat_content
| :modeline
| :file_tree
| :sidebar
| :custom_sidebar
| :agent_panel
| :status_bar
| :minibuffer
| :bottom_panel
| :picker_backdrop
| :picker
| :completion_backdrop
| :completion_menu
| :hover_popup
| :signature_help
| :float_popup
| :agent_context
| :tool_manager
| :extension_panel
| :observatory
| :edit_timeline
| :notifications
| :extension_overlay
A surface identity in the registry's namespace.
@type wire_placement() :: %{ surface_id: 0..65535, rect: %{ row: non_neg_integer(), col: non_neg_integer(), width: non_neg_integer(), height: non_neg_integer() }, z: non_neg_integer(), hit_kind: 0..255 }
A placement projected to its wire shape: surface_id/hit_kind already mapped to
their numeric identity, rect as a {row, col, width, height} cell map, z verbatim.
Functions
@spec contains?(MingaEditor.Layout.rect(), integer(), integer()) :: boolean()
Half-open rect containment: row in [r, r+h) and col in [c, c+w).
@spec from_tree(MingaEditor.FocusTree.t()) :: [ MingaEditor.Layout.SurfaceRegistry.Placement.t() ]
Builds placements from an already-constructed focus tree.
Exposed so callers that already hold the cached tree (mouse routing, render
input) do not rebuild it. The tree's child order is rendered z-order; this
walk assigns each node a z band and preserves back-to-front ordering.
@spec hit_kind_u8(hit_kind()) :: 0..255
Maps a registry hit_kind atom to its u8 wire value.
Reuses the window-encoder hit-kind numbering
(Minga.Frontend.Adapter.GUI.WindowEncoder: text 1, gutter 2, fold_control 3,
modeline 4, divider 5, status_bar 6) so a placement's hit kind and a per-window
hit region speak the same u8, and extends it with :chrome (7) and :overlay
(8) for surface-level entries. Consumed by SurfaceLayoutEncoder.
@spec placements(map()) :: [MingaEditor.Layout.SurfaceRegistry.Placement.t()]
Returns the frame's surface placements ordered back-to-front by z.
Pure: takes editor or render-pipeline state and returns a list of
Placement structs. The list is the single source of surface rects and
z-order for both compositing (sort by z) and hit-testing (reverse the
sorted list).
@spec rect_for(map(), surface_id()) :: MingaEditor.Layout.rect() | nil
Returns the rect of the first placed surface with surface_id, or nil.
This is the read site hit-testers use to ask the registry "where is surface
X?" instead of re-deriving the rect from Layout fields. Because placements
are projected from the focus tree, this rect is the same one mouse routing
hit-tests against. When several surfaces share an id (e.g. :modeline per
window), the frontmost (highest z, last in paint order) is returned.
@spec rect_for_in([MingaEditor.Layout.SurfaceRegistry.Placement.t()], surface_id()) :: MingaEditor.Layout.rect() | nil
Like rect_for/2 but reads an already-computed placement list.
@spec surface_id(MingaEditor.FocusTree.Node.content_type()) :: surface_id() | nil
Maps a focus-tree content_type to a registry surface_id, or nil for
content types that are not independently placed surfaces (the viewport root
and the per-window :window container, whose content child is the placed
surface).
@spec surface_id_u16(surface_id()) :: 0..65535
Maps a registry surface_id atom to its u16 wire value.
Single BEAM-side source of the surface numbering. The schema carries
surface_id as a raw u16 (no enum, by decision: see the moduledoc), and
Minga.Frontend.Adapter.GUI.SurfaceLayoutEncoder consumes this function
rather than re-deriving numbers.
@spec wire_placements(map()) :: [wire_placement()]
Returns the frame's placements projected to their wire shape.
This is the boundary between the registry (which owns the surface/hit-kind
numbering) and the wire encoder (which only lays out bytes). The encoder
consumes these plain maps, so it never depends on MingaEditor.*: the registry
stays the single authority for the numeric identity (#2268 unification call),
and the emitter stays a pure byte layout over data. Order is preserved
(back-to-front by z), so the wire list IS the compositing order.
@spec within?(map(), surface_id(), integer(), integer()) :: boolean()
Returns true when (row, col) falls inside the placed surface surface_id.
Half-open rect containment matching FocusTree.Node.contains?/3, so a
registry-backed bounds check agrees with focus-tree hit-testing.