wijjit.elements.base.Element

class wijjit.elements.base.Element(id=None, classes=None, tab_index=None)[source]

Base class for all UI elements.

Parameters:
  • id (str, optional) – Unique identifier for this element

  • classes (str or list of str or set of str, optional) – CSS class names for styling. Can be: - String with space-separated classes: “btn-primary large” - List of class names: [“btn-primary”, “large”] - Set of class names: {“btn-primary”, “large”}

  • tab_index (int | None)

Variables:
  • id (str or None) – Element identifier

  • classes (set of str) – CSS class names for styling

  • focusable (bool) – Whether this element can receive focus

  • focused (bool) – Whether this element currently has focus

  • autofocus (bool) – Whether this element should receive focus when nothing else is focused (i.e. on the first render, or after the focused element disappears). Set declaratively from a template as autofocus=True.

  • hovered (bool) – Whether the mouse is currently over this element

  • bounds (Bounds or None) – Screen position and size

  • element_type (ElementType) – Type of this element

  • tab_index (int or None) – Tab order for focus navigation. Lower values receive focus first. Elements with None are ordered after elements with explicit tab_index, in document order. Set to -1 to exclude from tab navigation while remaining focusable (can still receive focus via click or programmatic focus).

  • on_double_click (callable or None) – Callback for double-click events. Signature: on_double_click(event: MouseEvent) -> None

  • on_context_menu (callable or None) – Callback for context menu (right-click) events. Return a list of menu items to display, or None to use default behavior. Signature: on_context_menu(event: MouseEvent) -> list | None

  • draggable (bool) – Whether this element can be dragged (default: False)

  • drop_target (bool) – Whether this element can receive drops (default: False)

  • on_drag_start (callable or None) – Callback when drag starts. Return drag data to continue, None to cancel. Signature: on_drag_start(event: MouseEvent) -> Any | None

  • on_drag (callable or None) – Callback during drag. Signature: on_drag(event: MouseEvent, drag_data: Any) -> None

  • on_drag_end (callable or None) – Callback when drag ends. Signature: on_drag_end(event: MouseEvent, drag_data: Any, dropped: bool) -> None

  • on_drag_over (callable or None) – Callback to check if drop is allowed. Return True to allow. Signature: on_drag_over(event: MouseEvent, drag_data: Any) -> bool

  • on_drop (callable or None) – Callback when something is dropped on this element. Signature: on_drop(event: MouseEvent, drag_data: Any, source_element: Element) -> bool

__init__(id=None, classes=None, tab_index=None)[source]
Parameters:
Return type:

None

Methods

__init__([id, classes, tab_index])

add_class(class_name)

Add a CSS class to this element.

apply_props(props)

Apply props from a VNode, skipping ephemeral props.

get_ephemeral_state()

Get ephemeral state that should survive reconciliation.

get_hardware_cursor_position()

Absolute screen cell where the hardware terminal cursor should park.

get_height_for_width(width)

Return the height this element needs when laid out at width.

get_intrinsic_size()

Get the intrinsic (preferred) size of the element.

handle_key(key)

Handle a key press.

handle_mouse(event)

Handle a mouse event.

has_class(class_name)

Check if element has a CSS class.

on_blur()

Called when element loses focus.

on_focus()

Called when element gains focus.

on_hover_enter()

Called when mouse enters element.

on_hover_exit()

Called when mouse exits element.

on_mount()

Called when element is first added to the element tree.

on_unmount()

Called when element is removed from the element tree.

on_update(changed_props)

Called when element props are updated during reconciliation.

remove_class(class_name)

Remove a CSS class from this element.

render_signature()

Hashable/comparable capture of everything render_to() reads.

render_to(ctx)

Render the element to a cell-based buffer.

restore_ephemeral_state(state)

Restore ephemeral state after reconciliation.

set_bounds(bounds)

Set the element's screen bounds.

toggle_class(class_name)

Toggle a CSS class on this element.

Attributes

captures_tab

Whether this element wants the Tab key instead of focus movement.

parent_frame

Get the parent Frame if this element is inside a scrollable frame.

supports_dynamic_sizing

Whether this element supports dynamic sizing.

add_class(class_name)[source]

Add a CSS class to this element.

Parameters:

class_name (str) – CSS class name to add

Return type:

None

Examples

>>> button = Button("Click me")
>>> button.add_class("btn-primary")
>>> "btn-primary" in button.classes
True
remove_class(class_name)[source]

Remove a CSS class from this element.

Parameters:

class_name (str) – CSS class name to remove

Return type:

None

Notes

Uses discard() instead of remove() to avoid KeyError if class doesn’t exist.

Examples

>>> button = Button("Click me", classes="btn-primary large")
>>> button.remove_class("large")
>>> "large" in button.classes
False
toggle_class(class_name)[source]

Toggle a CSS class on this element.

Parameters:

class_name (str) – CSS class name to toggle

Return type:

None

Notes

If the class is present, it will be removed. If absent, it will be added.

Examples

>>> button = Button("Click me")
>>> button.toggle_class("active")
>>> "active" in button.classes
True
>>> button.toggle_class("active")
>>> "active" in button.classes
False
has_class(class_name)[source]

Check if element has a CSS class.

Parameters:

class_name (str) – CSS class name to check

Returns:

True if element has the class, False otherwise

Return type:

bool

Examples

>>> button = Button("Click me", classes="btn-primary")
>>> button.has_class("btn-primary")
True
>>> button.has_class("btn-secondary")
False
abstractmethod render_to(ctx)[source]

Render the element to a cell-based buffer.

Parameters:

ctx (PaintContext) – Paint context with buffer, style resolver, and bounds

Return type:

None

Notes

This is the cell-based rendering API. All elements must implement this method. It provides: - Theme-based styling via ctx.style_resolver - Efficient diff rendering via cell buffer - Better compositing and effects - Direct access to the screen buffer for precise control

Elements should use the PaintContext methods to render: - ctx.write_text() - Write styled text - ctx.fill_rect() - Fill rectangular regions - ctx.draw_border() - Draw borders - ctx.clear() - Clear the element’s bounds

Examples

Implement cell-based rendering in a custom element:

>>> def render_to(self, ctx):
...     # Resolve element's style from theme
...     style = ctx.style_resolver.resolve_style(self, 'button')
...     # Write styled text to buffer
...     ctx.write_text(0, 0, self.label, style)
get_intrinsic_size()[source]

Get the intrinsic (preferred) size of the element.

This method calculates the natural size the element would like to be based on its content, without requiring actual rendering. Used by the layout engine to determine size constraints.

Returns:

(width, height) in characters/lines

Return type:

tuple[int, int]

Notes

Default implementation returns minimal size (1, 1). Elements should override this to return their actual intrinsic size based on content. This is used by the layout engine when sizing is set to “auto”.

Examples

TextElement calculates size from text content:

>>> def get_intrinsic_size(self):
...     lines = self.text.split("\n")
...     width = max(visible_length(line) for line in lines)
...     height = len(lines)
...     return (width, height)
render_signature()[source]

Hashable/comparable capture of everything render_to() reads.

Used by the renderer’s “skip unchanged elements” fast path (review 2.5, option c). On the incremental paint path, an element whose signature and on-screen geometry match the previous frame is not re-painted - its cells are already correct in the copied-in baseline. Returning None (the default) opts the element out: it is always repainted, the safe choice for any element whose full set of render inputs is not captured here.

Overrides must include _style_signature() plus every value the element’s render_to consults (content, cursor/scroll/selection, style/variant, …). The renderer compares geometry (bounds, clip, scroll) and a global theme epoch separately, so those need not be included. A signature that is too coarse (changes too often) is always safe - it only skips less; a signature that misses a render input is a correctness bug (stale paint), which the verify_skips render mode and the on-vs-off equivalence tests exist to catch.

Returns:

A comparable signature, or None to always repaint.

Return type:

object or None

get_hardware_cursor_position()[source]

Absolute screen cell where the hardware terminal cursor should park.

Returns:

(x, y) absolute screen coordinates of the element’s text caret as painted in the last render, or None when the element has no caret, is not focused, or the caret is clipped out of view (e.g. scrolled outside a frame interior).

Return type:

tuple[int, int] or None

Notes

After each frame the application queries the focused element and, when this returns a position, appends a cursor-move + show-cursor escape so the terminal’s real cursor blinks on the caret cell (see the HARDWARE_CURSOR config key). The default returns the _hw_cursor_pos attribute, which caret-bearing elements (TextInput, TextArea, CodeEditor) maintain during render_to via PaintContext.cursor_anchor().

property parent_frame: Any

Get the parent Frame if this element is inside a scrollable frame.

Returns:

Parent frame object, or None if no parent or parent was garbage collected

Return type:

Frame or None

Notes

Uses weak reference internally to prevent circular references and memory leaks.

property supports_dynamic_sizing: bool

Whether this element supports dynamic sizing.

Dynamic sizing elements can expand to fill available space and report minimal constraints to avoid inflating their parent container. This is used by the layout engine to optimize layout calculations.

Returns:

True if element supports dynamic sizing, False otherwise

Return type:

bool

Notes

Default implementation returns False. Elements that can efficiently expand to fill space (like TextArea, Markdown viewers) should override this to return True when they are configured with fill sizing.

Examples

TextArea with fill sizing supports dynamic sizing:

>>> class TextArea(Element):
...     def __init__(self, width="auto", height="auto") -> None:
...         self.width_spec = width
...         self.height_spec = height
...
...     @property
...     def supports_dynamic_sizing(self):
...         return self.width_spec == "fill" or self.height_spec == "fill"
property captures_tab: bool

Whether this element wants the Tab key instead of focus movement.

Returns:

True to be offered the Tab key before focus moves on.

Return type:

bool

Notes

Tab is claimed by the framework’s built-in focus navigation (Wijjit._handle_tab_key), which runs as a high-priority GLOBAL handler and cancels the event - so a focused element normally never sees Tab at all. Overriding this to return True gives the element first refusal: handle_key is called with the Tab key, and focus only moves on if it returns False.

Two rules keep this from producing a focus trap, and an override must respect both:

  • Only plain Tab is offered. Shift+Tab always moves focus backward and is never routed here, so there is always a way out. (Ctrl+Tab is not an option: terminals encode Ctrl+I as Tab, and prompt_toolkit maps ControlI to Keys.TAB.)

  • Return False from handle_key whenever the element has nothing useful to do with Tab, so focus moves on instead of stalling. An element that returns True unconditionally makes Tab a dead key.

This is a property rather than a fixed attribute so it can depend on live state - TextInput claims Tab only while its autocomplete popup is open, and releases it otherwise.

handle_key(key)[source]

Handle a key press.

This method is synchronous by design. See the module docstring for the rationale behind the async/sync split between key and mouse handlers.

Parameters:

key (Key) – The key that was pressed

Returns:

True if the key was handled, False otherwise

Return type:

bool

Notes

If your key handler needs to perform async operations, schedule them with asyncio.create_task() rather than converting this method to async. Example:

def handle_key(self, key: Key) -> bool::

    if key.char == 's' and key.ctrl::

        asyncio.create_task(self._save_async())
        return True
    return False
async handle_mouse(event)[source]

Handle a mouse event.

Parameters:

event (MouseEvent) – The mouse event that occurred

Returns:

True if the event was handled, False otherwise

Return type:

bool

Notes

This is an async method to support async operations during mouse handling (e.g., calling async APIs, awaiting async state updates). Subclasses should override this method for custom mouse handling.

The base implementation checks for double-click and context menu events and invokes the corresponding callbacks if set. Subclasses that override this method should call super().handle_mouse(event) to preserve this behavior, or handle these events themselves.

on_focus()[source]

Called when element gains focus.

Return type:

None

on_blur()[source]

Called when element loses focus.

Return type:

None

on_hover_enter()[source]

Called when mouse enters element.

Return type:

None

on_hover_exit()[source]

Called when mouse exits element.

Return type:

None

set_bounds(bounds)[source]

Set the element’s screen bounds.

Parameters:

bounds (Bounds) – New bounds

Return type:

None

get_height_for_width(width)[source]

Return the height this element needs when laid out at width.

Layout measures bottom-up (get_intrinsic_size()) before it knows how much width a child will get. That is wrong for anything whose height depends on its width - wrapped text being the obvious case, where the intrinsic pass reports one row for a line that will occupy three. A container that has already resolved a child’s width calls this instead.

Parameters:

width (int) – The width the element will be laid out at.

Returns:

Required height. The default ignores width and reports the intrinsic height, which is correct for every element whose height does not depend on its width.

Return type:

int

on_mount()[source]

Called when element is first added to the element tree.

Override this method to perform initialization that requires the element to be part of the tree, such as registering event handlers or starting animations.

Notes

This is called by the Reconciler when a new element is created during reconciliation.

Return type:

None

on_unmount()[source]

Called when element is removed from the element tree.

Override this method to perform cleanup such as cancelling timers, removing event handlers, or releasing resources.

Notes

This is called by the Reconciler when an element is deleted during reconciliation.

Return type:

None

on_update(changed_props)[source]

Called when element props are updated during reconciliation.

Override this method to respond to prop changes, such as recomputing derived state or triggering side effects.

Parameters:

changed_props (dict) – Map of prop_name -> (old_value, new_value) for changed props. Does not include ephemeral props (cursor, scroll, etc.)

Return type:

None

Notes

This is called by the Reconciler after props have been applied but before ephemeral state is restored.

Examples

>>> def on_update(self, changed_props):
...     if 'items' in changed_props:
...         old_items, new_items = changed_props['items']
...         self._recompute_layout()
get_ephemeral_state()[source]

Get ephemeral state that should survive reconciliation.

Ephemeral state includes cursor positions, scroll offsets, selection ranges, and other UI state that should persist even when the element’s props change. Override this in subclasses to preserve relevant state.

Returns:

Map of state_name -> value. Keys should match attribute names on the element for automatic restoration.

Return type:

dict

Notes

This is called by the Reconciler before applying prop changes. The returned state is later passed to restore_ephemeral_state().

Examples

>>> def get_ephemeral_state(self):
...     return {
...         'cursor_pos': self.cursor_pos,
...         'scroll_offset': self.scroll_offset,
...     }
restore_ephemeral_state(state)[source]

Restore ephemeral state after reconciliation.

This method receives the state returned by get_ephemeral_state() and should restore it to the element. The default implementation uses setattr for keys that match attribute names.

Parameters:

state (dict) – Map of state_name -> value from get_ephemeral_state()

Return type:

None

Notes

This is called by the Reconciler after applying prop changes and calling on_update().

Examples

>>> def restore_ephemeral_state(self, state):
...     super().restore_ephemeral_state(state)
...     # Handle special restoration logic
...     if 'scroll_offset' in state:
...         self.scroll_manager.scroll_to(state['scroll_offset'])
apply_props(props)[source]

Apply props from a VNode, skipping ephemeral props.

This method applies props to the element, but skips properties that are marked as ephemeral (cursor, scroll, selection, etc.) since those should be preserved from the existing element.

Parameters:

props (dict) – Props to apply from VNode

Return type:

None

Notes

Ephemeral props are defined in wijjit.core.vdom.EPHEMERAL_PROPS.