RenderContext

⚠ verified v0.0.496 (current: v0.0.505)

RenderContext

Passed to on_render. Accumulate draw commands; FrameDone is auto-emitted.

Properties

PropertyTypeDescription
xfloatPane origin x (usually 0)
yfloatPane origin y (usually 0)
wfloatPane width in logical pixels
hfloatPane height in logical pixels
widthfloatAlias for w
heightfloatAlias for h
frame_idintMonotonically increasing render counter
elapsedfloatSeconds since previous on_render (0.0 on first frame)
workspace_rootstrAbsolute path to the workspace root
capabilitieslist[str]Granted capability strings
feature_flagslist[str]Enabled feature flag strings
emitEmitterEmitter instance for out-of-frame commands

Methods

register_scroll_consumer

def register_scroll_consumer(component: Any) -> None

Declare that component wants to receive scroll events this frame.

Call from a component’s render() method. The SDK will automatically call component.handle_scroll(delta_y) on incoming scroll events and schedule a re-render — no on_scroll_delta override needed in the app.


declare_min_size

def declare_min_size(width: float, height: float) -> None

Override the manifest-declared minimum size at runtime.

Emits DrawCommand::SetMinSize. The host stores this and uses it as the live effective minimum for this pane going forward, superseding the manifest value. Use when a view mode needs more space than the default (e.g. a detail view vs a list view).


clear

def clear(fill: str = '#000000') -> None

Fill the entire pane with a single color. Shorthand for a full-size rect.


rect

def rect(x: float, y: float, w: float, h: float, fill: str, radius: float = 0.0) -> None

Draw a filled rectangle.

Args: x: Left edge in logical pixels y: Top edge in logical pixels w: Width in logical pixels h: Height in logical pixels fill: Fill color as hex string (e.g., “#ff0000” or “#ff0000aa” with alpha) radius: Corner radius in pixels (0.0 = sharp corners, default)


circle

def circle(cx: float, cy: float, r: float, fill: str) -> None

Draw a filled circle.

Args: cx: Center X in logical pixels cy: Center Y in logical pixels r: Radius in logical pixels fill: Fill color as hex string; supports 8-digit hex (#rrggbbaa) for alpha


arc

def arc(cx: float, cy: float, r: float, start_angle: float, end_angle: float, fill: str) -> None

Draw a filled pie slice. Angles in radians, clockwise from east (right). Full circle: start_angle=0, end_angle=6.2832 (2*pi). Example pie slice: arc(cx, cy, r, 0, math.pi * 0.5, fill=“#ff0000”)


text

def text(x: float, y: float, text: str, size: float, color: str, monospace: bool = False, bold: bool = False, align: str = 'top_left', max_width: 'float | None' = None, elide: bool = True, selectable: bool = False, max_lines: 'int | None' = None) -> None

Draw text. align controls how (x, y) maps to the text box:

  • “top_left” (default) — (x, y) is the top-left corner.
  • “center” — (x, y) is the visual center.
  • “top_center” — (x, y) is the top-center.
  • “right” — (x, y) is the top-right corner.

Use “center” when placing text inside a fixed-size container (a badge, a button, a pie-chart label) — the host uses real font metrics, which is noticeably more accurate than Python-side math with approximate character-width ratios.

max_width — when set, the host clips the text at this pixel width. elide — when True (default), a ”…” is appended at the clip point; when False, the text is hard-clipped with no marker. selectable — when True, the host renders the text as a real egui label so the user can drag-select inside it and Cmd+C copies the current selection. Default False (#200).

max_width, elide, and selectable are sent explicitly on the wire so the host always has required fields (no serde defaults). The SDK fills in None / True / False when the caller omits them.


markdown

def markdown(x: float, y: float, w: float, text: str, base_size: float = 14.0, color: 'str | None' = None) -> None

Render markdown text via the host’s egui_commonmark renderer.

The host creates a child Ui at (x, y) with width w and renders the markdown with proper formatting (bold, italic, code blocks, lists, etc.).

base_size — body font size in pt (headers scale relative to this). color — body text colour (hex); defaults to the active theme fg.


image

def image(src: str, x: float, y: float, w: float, h: float, fit: str = 'contain') -> None

Draw an image from a file path or URL.

fit controls scaling: “contain” (default, letterbox), “cover” (crop to fill), or “fill” (stretch). Host must implement DrawCommand::Image for this to render.


copy_to_clipboard

def copy_to_clipboard(text: str) -> None

Write text to the OS clipboard via the host.

Uses _emit (immediate write) rather than _queue so this works from on_key and other hook handlers where frame_done() is never called.


badge

def badge(x: float, y_center: float, label: str, fill: 'str | None' = None, fg: 'str | None' = None, font_size: float = 11.0, radius: float = 6.0) -> None

Render a host-measured pill badge.

The host measures the label with real egui font metrics, sizes the pill (text_w + padding), and centres the text — no Python width math.

x — left edge of the badge. y_center — vertical centre of the badge. label — text to display. fill — pill background colour. fg — text colour. font_size— label pt size. radius — corner radius (8.0 = fully rounded pill; 4.0 = tag chip).


button

def button(id: str, x: float, y: float, w: float, h: float, label: str, fill: str = '#313244', hover_fill: str = '#45475a', active_fill: str = '#585b70', text_color: str = '#cdd6f4', font_size: float = 13.0, radius: float = 6.0) -> bool

Draw a button and return True if it was clicked this frame.

Hover state is derived from the current mouse position (requires mouse_tracking = true in the manifest, or degrades gracefully without it). Click state is derived from buffered click events since the previous frame.

id is currently unused but reserved for future focus tracking.


key_chip_row

def key_chip_row(x: float, y: float, keys: 'list[str]', description: 'str | None' = None, font_size: float = 11.0) -> None

Render a row of keycap chips followed by an optional description.

The host measures each chip with real egui font metrics and flows them left-to-right — no Python width math.

x, y — origin of the chip row (left edge, top of chips). keys — list of key labels, e.g. [”⌘”, “K”] for a chord. description— optional trailing text label after the chips. font_size — chip label pt size.

For multi-pair shortcut rows with horizontal flow + multi-line wrapping, use ctx.shortcuts(...) instead — that’s the right primitive for footer-style “[k] desc · [j] desc · …” layouts.


text_row

def text_row(x: float, y: float, items: 'list[dict]', gap: float = 8.0, align: str = 'left_center') -> None

Render multiple text segments horizontally with configurable spacing.

The host measures each text segment with real font metrics and flows them left-to-right — no Python width math.

x, y — origin of the text row. items — list of text segment dicts, each with keys: - text — string to display (required) - color — color string (default: theme.fg) - size — font size in pt (default: BODY) - monospace — bool (default: False) gap — spacing between items in pixels (default: 8.0). align — vertical alignment; use “left_center” to center items on the y-axis at their midline (default: “left_center”).

Example:

ctx.text_row(
    x=24.0, y=200.0,
    items=[
        {"text": "12:34:56", "color": "#6c7086", "size": CAPTION, "monospace": True},
        {"text": "key pressed", "size": CAPTION},
    ],
    gap=12.0,
)

shortcuts

def shortcuts(x: float, y: float, max_width: float, pairs: 'list[tuple]', font_size: float = 11.0) -> None

Render a multi-group shortcut row with host-measured layout.

The host owns ALL geometry: chip widths from real font metrics, horizontal flow with inter-group spacing, multi-line wrapping when the next group would exceed max_width. SDK callers send one DrawCommand and trust the result — no Python width math, no truncation, no overflow.

pairs is a list of (keys, description) tuples where keys is either a single string or a list of strings (multi-key chord). Example:

ctx.shortcuts(
    x=24.0, y=12.0, max_width=ctx.w - 48.0,
    pairs=[
        (["[", "]"], "week"),
        ("t", "today"),
        (["j", "k"], "commit"),
        ("?", "help"),
    ],
)

measure_text

async def measure_text(text: str, font_size: float, monospace: bool = False) -> 'tuple[float, float]'

Measure text at font_size using the host’s real font metrics.

Sends a MeasureText DrawCommand and awaits TextMeasured from the host. Returns (width, height) in logical pixels.

Use this only when layout depends on measured text width (e.g. flowing multiple badges horizontally). Avoid on hot render paths — prefer passing max_width on ctx.text() for simple truncation.

Must be called with await from an async hook.


measure_text_wrapped

async def measure_text_wrapped(text: str, font_size: float, max_width: float, max_lines: 'int | None' = None) -> float

Measure the height of text wrapped at max_width using real host font metrics.

If max_lines is set, clamps the result to that many rows. Returns height in logical pixels.

Call at data-load time (once per item), not inside on_render() — each call is an async IPC roundtrip.


avatar

def avatar(src: str, x: float, y_center: float, radius: float) -> None

Render a circular clipped image.

src accepts a handle from emit.load_image() or a local file path. x is the left edge of the circle’s bounding box (circle centre = x + radius). y_center is the vertical centre of the circle. radius is the circle radius in logical pixels.


skeleton

def skeleton(x: float, y: float, w: float, h: float, radius: float = 4.0) -> None

Render an animated shimmer placeholder rect for loading states.

The host drives a ~20fps pulsing animation — no app-side timer needed.


push_clip

def push_clip(x: float, y: float, w: float, h: float) -> None

Push a clip rect onto the host’s clip stack.

All subsequent draws are clipped to the intersection of this rect with the current top of the stack (or the pane rect if the stack is empty). Must be balanced with a matching pop_clip(). Use _render_clipped on Component subclasses instead of calling this directly.


pop_clip

def pop_clip() -> None

Pop the most recently pushed clip rect. Must balance a push_clip().


line

def line(x1: float, y1: float, x2: float, y2: float, color: str, width: float = 1.0) -> None

Draw a line segment.

Args: x1: Start X in logical pixels y1: Start Y in logical pixels x2: End X in logical pixels y2: End Y in logical pixels color: Line color as hex string width: Line width in logical pixels (default: 1.0)


render

def render(tree: Any, fill: 'str | None' = None) -> None

Render a declarative UI tree (see plexi_sdk.ui). Clears the pane to fill first, then lays out tree into the full pane rect.

fill defaults to the active host theme background (ctx.theme.bg).

Example: from plexi_sdk.ui import Column, Header, Footer, Spacer ctx.render(Column([ Header(“My App”), Spacer(grow=True), Footer(“status line”), ]))


simple_list

def simple_list(items: 'list[dict]', selected: int = 0, item_height: float = 40.0, x: float = 0.0, y: float = 0.0, w: 'float | None' = None, h: 'float | None' = None) -> None

Draw a scrollable list of items with optional selection highlight.

Args: items: List of item dicts, each with keys “text” (required) and optional “disabled” selected: Index of the highlighted item (0-indexed) item_height: Height of each item in logical pixels x: Left edge of the list y: Top edge of the list w: Width of the list (defaults to full pane width) h: Height of the list (defaults to remaining pane height below y)


list_view

def list_view(list_id: str, items: 'list[dict]', selected: int = 0, loading: bool = False, error: 'str | None' = None, x: float = 0.0, y: float = 0.0, w: float = 0.0, h: float = 0.0) -> None

Host-native scrollable list with j/k navigation and typed row slots.

Items must be dicts with "type": "row" or "type": "custom_cell". Use :class:plexi_sdk.ui.ListRow to build row descriptors.

The host handles: j/k selection, scroll-to-selected, skeleton loading rows (when loading=True), error text, and empty state. The app receives :meth:on_list_select / :meth:on_list_activate callbacks instead of managing scroll state.

Args: list_id: Stable identifier for this list (must not change across frames). items: List of row/custom_cell dicts. Use ListRow(...).to_dict(). selected: Index of the currently highlighted item. loading: When True, renders skeleton rows instead of items. error: When set, renders an error message instead of items. x: Left edge in pane-local coordinates (0 = left edge). y: Top edge in pane-local coordinates (0 = top of pane). w: Width (0 = full pane width). h: Height (0 = remaining height below y).


begin_scroll

def begin_scroll(id: str, x: float, y: float, w: float, h: float, content_height: float) -> None

Begin a host-managed vertical scroll region (#446).

Declares a viewport at (x, y, w, h) within this pane. This rect does two things: it clips all draw commands until the matching end_scroll() call, AND it defines where the host detects scroll gestures. The host only fires on_scroll when the pointer is inside this rect.

Common mistake: if your scrollable list occupies only part of the pane (e.g. the right half), set x/w to cover the full pane width anyway so the user can scroll from anywhere — not just when hovering over the list. Clipping is only applied to content drawn inside the block, so content drawn before begin_scroll is unaffected regardless of x/w.

The host tracks the scroll position across frames and calls on_scroll(ctx, id, offset_y) whenever the user scrolls so the app can re-render content translated by offset_y.

content_height is the total virtual height of the scrollable content in logical pixels — the host uses this to size the scrollbar thumb. Pass the full height of all items even if most are off-screen.

id must be stable across frames — use a descriptive string rather than a counter. Typical pattern:

def on_scroll(self, ctx, id, offset_y):
    if id == "main-list":
        self.scroll_y = offset_y

def on_render(self, ctx):
    ctx.begin_scroll("main-list", 0, 48, ctx.w, ctx.h - 48,
                     content_height=100 * ROW_H)
    for i, item in enumerate(self.items):
        y = i * ROW_H - self.scroll_y
        ctx.text(8, y, item, size=14, color=ctx.theme.fg)
    ctx.end_scroll()

end_scroll

def end_scroll() -> None

Close the most recently opened scroll region. Must balance begin_scroll.


text_input

def text_input(id: str, x: float, y: float, w: float, placeholder: str = '', multiline: bool = False, h: float = 24.0) -> 'str | None'

Text input — host-owned buffer, submit-only.

Emits a DrawCommand::TextInput and returns the most recently submitted value for id if any landed since the previous frame, else None. The host owns the buffer entirely — typed characters never reach the app between keystrokes. On Enter the host emits PlexiEvent::TextSubmitted { id, value } and clears its buffer.

The host auto-focuses the widget on its first visible frame so the user can type immediately without clicking.

When multiline=True, Enter submits and Shift+Enter inserts a newline. When multiline=False (default), Enter submits immediately.

Pattern (poll on every frame):

submitted = ctx.text_input("note", x=12, y=12, w=300,
                           placeholder="Type a note…")
if submitted is not None:
    save_note(submitted)

Real-time validation (per-keystroke access) is out of scope — see issue #283.


log

def log(level: str, message: str) -> None

info

def info(message: str) -> None

warn

def warn(message: str) -> None

error

def error(message: str) -> None

debug

def debug(message: str) -> None

notify

def notify(title: str, priority: int, body: str = '', level: str = 'info', actions: 'list | None' = None, image_inline: 'dict | None' = None, image_pipe_id: 'str | None' = None, timeout_secs: 'int | None' = None, on_dismiss: 'str | None' = None) -> None

Post a message notification. See Emitter.notify. priority is required (int, higher = more urgent). image_inline / image_pipe_id (#74) optionally attach an image. Scope is resolved from the app’s manifest — not an argument.


notify_choice

async def notify_choice(title: str, options: list, priority: int, body: str = '', level: str = 'info', required: bool = False, image_inline: 'dict | None' = None, image_pipe_id: 'str | None' = None, timeout_secs: 'int | None' = None, on_dismiss: 'str | None' = None) -> str

Post a choice notification and await until the user picks. priority is required (int, higher = more urgent). image_inline / image_pipe_id (#74) optionally attach an image. Returns the chosen option’s value (or label if no value set), or “cancel” if the user dismissed. Use with await.


notify_with_image

async def notify_with_image(title: str, body: str, image_bytes: bytes, mime: str, priority: int, level: str = 'info', choices: 'list | None' = None) -> 'str | None'

Convenience: post a notification with an inline base64 image. See Emitter.notify_with_image — handles base64 + 50KB cap. Use with await.


notify_input

async def notify_input(title: str, priority: int, prompt: str = '', body: str = '', level: str = 'info', required: bool = False, timeout_secs: 'int | None' = None, on_dismiss: 'str | None' = None) -> str

Post an input notification and await until the user submits. priority is required (int, higher = more urgent). Returns the typed text, or “cancel” if dismissed. Use with await.


notify_and_wait

async def notify_and_wait(title: str, priority: int, body: str = '', level: str = 'info', actions: 'list | None' = None, timeout_secs: 'int | None' = None, on_dismiss: 'str | None' = None) -> str

Post a message notification and await for acknowledge/cancel. priority is required (int, higher = more urgent). See Emitter.notify_and_wait. Use with await.


notify_async

def notify_async(title: str, priority: int, body: str = '', level: str = 'info', actions: 'list | None' = None, image_inline: 'dict | None' = None, image_pipe_id: 'str | None' = None, timeout_secs: 'int | None' = None, on_dismiss: 'str | None' = None, on_response: 'Any' = None) -> str

Non-blocking notify_and_wait. Returns immediately with notify_id. priority is required. See Emitter.notify_async.


notify_choice_async

def notify_choice_async(title: str, options: list, priority: int, body: str = '', level: str = 'info', required: bool = False, image_inline: 'dict | None' = None, image_pipe_id: 'str | None' = None, timeout_secs: 'int | None' = None, on_dismiss: 'str | None' = None, on_response: 'Any' = None) -> str

Non-blocking notify_choice. Returns immediately with notify_id. priority is required. See Emitter.notify_choice_async.


notify_input_async

def notify_input_async(title: str, priority: int, prompt: str = '', body: str = '', level: str = 'info', required: bool = False, timeout_secs: 'int | None' = None, on_dismiss: 'str | None' = None, on_response: 'Any' = None) -> str

Non-blocking notify_input. Returns immediately with notify_id. priority is required. See Emitter.notify_input_async.


notify_and_wait_async

def notify_and_wait_async(title: str, priority: int, body: str = '', level: str = 'info', actions: 'list | None' = None, timeout_secs: 'int | None' = None, on_dismiss: 'str | None' = None, on_response: 'Any' = None) -> str

Non-blocking notify_and_wait. Returns immediately with notify_id. priority is required. See Emitter.notify_and_wait_async.


status_summary

def status_summary(text: str) -> None

set_timer

def set_timer(timer_id: str, after_ms: int) -> None

cancel_timer

def cancel_timer(timer_id: str) -> None

set_mouse_tracking

def set_mouse_tracking(enabled: bool) -> None

Enable or disable on_mouse_move delivery for this pane. Call with True in on_init to start receiving mouse-move events. Delegates to emit.set_mouse_tracking().


schedule_render

def schedule_render(after_ms: int = 16) -> None

Ask the host to send a new Render event after after_ms milliseconds. Delegates to emit.schedule_render(). 16 ms ≈ 60 fps. 32 ms ≈ 30 fps.


get_secret

async def get_secret(key: str) -> 'str | None'

Request a secret by key. Alias for emit.get_secret(). Use with await.


load_state

def load_state() -> dict

Return persisted app state loaded at startup. {} if nothing saved.


save_state

def save_state(state: dict) -> None

Persist state to workspace (if workspace active) or global storage. Fire-and-forget — no acknowledgement from host.


http_request

async def http_request(url: str, method: str = 'GET', headers: 'dict[str, str] | None' = None, body: 'str | None' = None) -> str

HTTP request brokered through the host. Requires net.http capability. Use with await.


ai_query

async def ai_query(model_tier: str, system: str, messages: 'list[dict]', tools: 'list[dict] | None' = None) -> object

AI broker call through the host. Requires ai.query capability. Use with await.


spawn_pane

def spawn_pane(type_id: str, layout: str = 'split_v', args: 'list[str] | None' = None, pipe_id: 'str | None' = None, from_pane_id: 'int | None' = None, request_id: 'str | None' = None, target_context: 'int | None' = None) -> None

Request the host to open a new pane. Requires panes.spawn capability.


query_context_state

def query_context_state(context_id: int) -> None

Query the state of a context. Host responds via on_context_state.


render_tree

def render_tree(node: 'dict | Any') -> None

Emit a component tree command to the host.

node can be any object with a to_node() method (HasToNode), or a raw dict matching the UiNode wire format.

Emitted as: {“type”: “component_tree”, “root”: }


badge_child

def badge_child(label: str, fill: 'str | None' = None, fg: 'str | None' = None, font_size: float = 11.0, radius: float = 8.0) -> dict

Return a layout child node for a Badge. Use inside ctx.row/column/stack.


text_child

def text_child(text: str, size: float = 12.0, color: str = '#cdd6f4', monospace: bool = False, bold: bool = False, max_width: 'float | None' = None, elide: bool = False, selectable: bool = False) -> dict

Return a layout child node for Text. Use inside ctx.row/column/stack.


key_chip_child

def key_chip_child(label: str, font_size: float = 11.0) -> dict

Return a layout child node for a KeyChip. Use inside ctx.row/column/stack.


layout_node

def layout_node(direction: str, children: 'list[dict]', gap: float = 0.0) -> dict

Return a nested layout node for use inside ctx.row/column/stack.


row

def row(x: float, y: float, children: 'list[dict]', gap: float = 6.0) -> None

Emit a declarative flex-row layout tree.

The host resolves all child positions using taffy (flexbox) and real egui font metrics. Children are layout child dicts from badge_child(), text_child(), key_chip_child(), or layout_node().

x, y — pane-relative top-left anchor. children — list of layout child dicts. gap — space between children in pixels (default 6.0).

Example:

ctx.row(
    x=24.0, y=40.0,
    children=[
        ctx.badge_child("4 files"),
        ctx.text_child(" modified"),
    ],
    gap=6.0,
)

column

def column(x: float, y: float, children: 'list[dict]', gap: float = 6.0) -> None

Emit a declarative flex-column layout tree.

Same as row() but stacks children top-to-bottom.


stack

def stack(x: float, y: float, children: 'list[dict]') -> None

Emit a declarative stack layout — all children rendered at the same origin.

Use for layering (background rect behind text, etc.).


responsive

def responsive(x: float, y: float, tiers: 'list[dict]') -> None

Emit a responsive layout that adapts to the available pane aspect ratio.

The host evaluates tiers in order and renders the first match:

  • “landscape”: pane is wider than tall (ratio > 1.2)
  • “portrait”: pane is taller than wide (ratio < 0.83)
  • “square”: fallback for balanced aspect ratios

Each tier dict has: aspect (str), direction (str), children (list), gap (float).

Example:

ctx.responsive(
    x=0.0, y=0.0,
    tiers=[
        {"aspect": "landscape", "direction": "row",
         "children": [ctx.text_child("wide view")], "gap": 6.0},
        {"aspect": "portrait", "direction": "column",
         "children": [ctx.text_child("tall view")], "gap": 4.0},
        {"aspect": "square", "direction": "column",
         "children": [ctx.text_child("default")], "gap": 4.0},
    ],
)

frame_done

def frame_done() -> None

Flush all buffered draw commands for this frame.

Called automatically after on_render returns. Only call this manually if you’re rendering in multiple phases and need intermediate updates.