MingaEditor.Layout.SurfaceRegistry (Minga v0.1.0)

Copy Markdown View Source

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 by FloatingWindow). FocusTree.add_floating_overlays/2 adds them as overlay nodes from shell_state; they occupy the @z_floating_overlay region (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/3 adds each visible one (per MingaEditor.Layout.FooterOverlays) as an overlay node with a bottom-anchored full-width rect from MingaEditor.Layout.OverlayBand (porting the Go maxOverlayHeight clamp). 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 to MingaEditor.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.AgentMouse splits the agent window content rect into a chat sub-column vs a preview sub-column using chat_width_pct math, and splits the prompt area off the bottom using PromptRenderer height math. The registry emits the agent window/panel rect; the chat/preview/prompt sub-division stays in AgentMouse (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

hit_kind()

@type hit_kind() ::
  :text
  | :gutter
  | :fold_control
  | :modeline
  | :status_bar
  | :divider
  | :chrome
  | :overlay

Coarse classification of what a click on a surface means.

surface_id()

@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.

wire_placement()

@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

contains?(arg, row, col)

@spec contains?(MingaEditor.Layout.rect(), integer(), integer()) :: boolean()

Half-open rect containment: row in [r, r+h) and col in [c, c+w).

from_tree(root)

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.

hit_kind_u8(atom)

@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.

placements(state)

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).

rect_for(state, surface_id)

@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.

rect_for_in(placements, surface_id)

Like rect_for/2 but reads an already-computed placement list.

surface_id(arg1)

@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).

surface_id_u16(atom)

@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.

wire_placements(state)

@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.

within?(state, surface_id, row, col)

@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.