Raxol.UI.Harness.OverlayPanel (Raxol v2.6.1)

View Source

Pure, read-only footer-region content panel: a fixed-height scrollable block of pre-formatted lines, driven entirely by Raxol.UI.Harness.InputEvent-normalized events. No process, no I/O -- state is a plain map, and every function here is (t(), ...) -> t() | result, exactly Raxol.UI.Harness.OverlayPicker's style.

OverlayPicker vs. this module

OverlayPicker is filterable and selects -- a query row, ranked matches, {:picked, item} on Enter. This module is its read-only sibling: no query row, no selection, no {:picked, _} outcome ever. It exists for content a footer panel needs to show, not choose from -- the worktracks/memory/plan read-models Raxol.Harness.PanelProjection folds. Same host contract (fixed claimed height, exact-row-count rendering, InputEvent-normalized handle_key/2), different shape of interaction: scroll instead of filter-and-pick.

height/1 is 1 + max_visible, fixed at construction from :max_visible alone -- it never tracks length(lines) the way a naive "shrink to fit" panel would. This mirrors OverlayPicker's own fixed-height rationale exactly: the footer viewport this panel is hosted inside grows its DECSTBM split ONCE, at open, to fit the claimed height. If height instead tracked content length, a put_lines/2 call mid-interaction (a footer repaint pulling a fresh PanelProjection.render_lines/2 result) could change the claimed row count and force a mid-interaction re-pin -- a visible flicker. A panel that claims its full potential height up front, then pads unused rows with blank lines, costs a few empty footer rows in exchange for zero re-pinning churn.

State shape

%{
  kind: :worktracks | :memory | :plan,
  title: String.t(),
  lines: [String.t()],
  offset: non_neg_integer(),
  max_visible: pos_integer()
}

Rendering: text leaves only, exactly height/1 of them

render/1 returns a %{type: :column, children: [...]} view map whose children are ALWAYS exactly height(t) %{type: :text} leaves: one title row, then max_visible content rows (padded with blank leaves when there are fewer lines than that). This is the same row-accounting contract OverlayPicker establishes for its host's footer row budget -- load-bearing here for the identical reason: a caller (the harness surface's footer composer) sizes the scroll-region split from height/1 alone and must never have to guess the actual child count. The title row shows a "title (first-last/total)" range indicator only when content overflows max_visible; otherwise it is the plain title. No width truncation happens here -- Raxol.Harness.Surface.ViewText is the one trust boundary that owns display-width truncation and control-byte sanitization, so this module hands it full, untruncated line content (already clamped upstream by Raxol.Harness.PanelProjection's own hostile-content discipline).

Summary

Functions

The default :max_visible content-row cap new/1 uses. Exposed so hosts clamp against THIS value rather than re-encoding the literal -- one source of truth, mirroring OverlayPicker.default_max_visible/0.

Handles one normalized InputEvent.t(). Returns {:continue, t()} for everything except :escape, which returns :dismissed (defensive -- in the assembled harness surface, the Keymap's :overlay guard captures ESC before it ever reaches this function, exactly as documented on OverlayPicker.handle_key/2). Read-only: printable characters, Enter, and paste are all inert no-ops. NEVER returns {:picked, _}.

The rows this panel claims -- fixed at construction from :max_visible alone, never the live length(lines) (see moduledoc, "no per-keystroke footer re-pin").

Builds a fresh panel.

Replaces the panel's content (e.g. a fresh PanelProjection.render_lines/2 result on repaint), clamping a now-stale offset into the new content's valid scroll range.

Renders the fixed-height view map (see moduledoc, "Rendering"): one title row followed by exactly max_visible content-row leaves.

Types

t()

@type t() :: %{
  kind: atom(),
  title: String.t(),
  lines: [String.t()],
  offset: non_neg_integer(),
  max_visible: pos_integer()
}

Functions

default_max_visible()

@spec default_max_visible() :: pos_integer()

The default :max_visible content-row cap new/1 uses. Exposed so hosts clamp against THIS value rather than re-encoding the literal -- one source of truth, mirroring OverlayPicker.default_max_visible/0.

handle_key(t, norm)

@spec handle_key(t(), Raxol.UI.Harness.InputEvent.t()) ::
  {:continue, t()} | :dismissed

Handles one normalized InputEvent.t(). Returns {:continue, t()} for everything except :escape, which returns :dismissed (defensive -- in the assembled harness surface, the Keymap's :overlay guard captures ESC before it ever reaches this function, exactly as documented on OverlayPicker.handle_key/2). Read-only: printable characters, Enter, and paste are all inert no-ops. NEVER returns {:picked, _}.

height(map)

@spec height(t()) :: pos_integer()

The rows this panel claims -- fixed at construction from :max_visible alone, never the live length(lines) (see moduledoc, "no per-keystroke footer re-pin").

new(opts)

@spec new(keyword()) :: t()

Builds a fresh panel.

Options

  • :kind (required) -- :worktracks, :memory, or :plan; used only to derive the default title.
  • :title (default derived from :kind -- "Worktracks"/"Memory"/"Plan")
  • :max_visible (default 8) -- the cap on content rows (excluding the title row) height/1 claims.
  • :lines (default []) -- initial content, typically Raxol.Harness.PanelProjection.render_lines/2's output.

put_lines(t, lines)

@spec put_lines(t(), [String.t()]) :: t()

Replaces the panel's content (e.g. a fresh PanelProjection.render_lines/2 result on repaint), clamping a now-stale offset into the new content's valid scroll range.

render(t)

@spec render(t()) :: map()

Renders the fixed-height view map (see moduledoc, "Rendering"): one title row followed by exactly max_visible content-row leaves.