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 elementclasses (
strorlistofstrorsetofstr, 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:
focusable (
bool) – Whether this element can receive focusfocused (
bool) – Whether this element currently has focusautofocus (
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 asautofocus=True.hovered (
bool) – Whether the mouse is currently over this elementbounds (
BoundsorNone) – Screen position and sizeelement_type (
ElementType) – Type of this elementtab_index (
intorNone) – 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 (
callableorNone) – Callback for double-click events. Signature: on_double_click(event: MouseEvent) -> Noneon_context_menu (
callableorNone) – 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 | Nonedraggable (
bool) – Whether this element can be dragged (default: False)drop_target (
bool) – Whether this element can receive drops (default: False)on_drag_start (
callableorNone) – Callback when drag starts. Return drag data to continue, None to cancel. Signature: on_drag_start(event: MouseEvent) -> Any | Noneon_drag (
callableorNone) – Callback during drag. Signature: on_drag(event: MouseEvent, drag_data: Any) -> Noneon_drag_end (
callableorNone) – Callback when drag ends. Signature: on_drag_end(event: MouseEvent, drag_data: Any, dropped: bool) -> Noneon_drag_over (
callableorNone) – Callback to check if drop is allowed. Return True to allow. Signature: on_drag_over(event: MouseEvent, drag_data: Any) -> boolon_drop (
callableorNone) – Callback when something is dropped on this element. Signature: on_drop(event: MouseEvent, drag_data: Any, source_element: Element) -> bool
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 that should survive reconciliation.
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 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.
Called when mouse enters element.
Called when mouse exits element.
on_mount()Called when element is first added to the element tree.
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.
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
Whether this element wants the Tab key instead of focus movement.
Get the parent Frame if this element is inside a scrollable frame.
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:
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’srender_toconsults (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 theverify_skipsrender mode and the on-vs-off equivalence tests exist to catch.
- 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]orNone
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_CURSORconfig key). The default returns the_hw_cursor_posattribute, which caret-bearing elements (TextInput, TextArea, CodeEditor) maintain duringrender_toviaPaintContext.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:
FrameorNone
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:
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:
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_keyis 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
ControlItoKeys.TAB.)Return False from
handle_keywhenever 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 -
TextInputclaims 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:
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:
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.
- 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.
- 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:
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.