renderable — the trait and node model
Every element that can appear on screen implements the Renderable trait.
This page is the contract those widgets live by: what a renderable is, the
methods the framework calls, and the two ready-made implementations —
RenderableNode and RootRenderable —
that most widgets build on.
pub trait Renderable {
fn id(&self) -> &str;
fn num(&self) -> u64;
fn is_visible(&self) -> bool;
fn is_focusable(&self) -> bool;
fn is_destroyed(&self) -> bool;
fn parent_num(&self) -> Option<u64>;
fn set_parent_num(&mut self, parent_num: Option<u64>);
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn render_self(&self, buf: &mut Buffer, area: Rect);
fn children(&self) -> &[Box<dyn Renderable>];
fn children_mut(&mut self) -> &mut [Box<dyn Renderable>]; // default: &mut []
fn add_child(&mut self, child: Box<dyn Renderable>) -> usize;
fn remove_child(&mut self, id: &str);
fn insert_child_before(&mut self, child: Box<dyn Renderable>, anchor_id: &str) -> Option<usize>;
// … defaulted hooks below …
}
Identity: id, num, parent_num
id()— a stable string identifier, unique among siblings, used byremove_child/insert_child_beforeto find a child. Widgets mint ids like"box-1","text-2","md-3"from an atomic counter.num()— a globally uniqueu64assigned at construction (also from an atomic counter).numis the parent link currency:set_parent_num/parent_numtrack which renderable adopted this one, so a widget can find its ancestors without holding references.parent_num()—Some(num)once adopted by another renderable,Nonefor the root.
Lifecycle flags
| Method | Meaning |
|---|---|
is_visible() |
Whether the widget should be drawn. The framework skips invisible subtrees. |
is_focusable() |
Whether the widget can receive focus. Interactive widgets (input, select, textarea) return true. |
is_destroyed() |
Whether the widget has been torn down. |
Type erasure: as_any / as_any_mut
Because children are stored as Box<dyn Renderable>, callers that know a
widget’s concrete type downcast through Any:
Why type erasure is needed: The renderable tree needs to hold heterogeneous widget types (boxes, text, markdown, inputs, etc.) in a single collection. Rust’s trait objects (
Box<dyn Renderable>) provide this polymorphism while maintaining type safety. Theas_anymechanism allows downcasting back to concrete types when needed (e.g., accessing widget-specific methods), balancing the flexibility of a mixed-type tree with the safety of Rust’s type system.
let box_widget = widget.as_any().downcast_ref::<BoxRenderable>();
Every widget returns self for both.
Drawing: render_self / render
render_self(&self, buf, area) is the core drawing method: write the widget’s
visuals into the ratatui Buffer within area (a Rect). It takes &self
— the widget must be internally consistent at draw time.
render(buf, area, delta_time) is the per-frame wrapper the renderer calls:
by default it just forwards to render_self, but a widget can override it to
animate using delta_time.
Children
fn children(&self) -> &[Box<dyn Renderable>];
fn children_mut(&mut self) -> &mut [Box<dyn Renderable>]; // default: &mut []
fn add_child(&mut self, child: Box<dyn Renderable>) -> usize; // returns child index
fn remove_child(&mut self, id: &str);
fn insert_child_before(&mut self, child: Box<dyn Renderable>, anchor_id: &str) -> Option<usize>;
add_child returns the index the child was inserted at. remove_child removes
by id. insert_child_before inserts before the child whose id() is
anchor_id, returning the insertion index, or None if the anchor isn’t a
child. The default children_mut returns an empty slice, so widgets that
don’t hold children (like TextRenderable) need not override it — but any
widget that does hold children must, or layout and traversal will never see
them.
Layout hooks
The managed renderer drives layout through these defaulted methods:
| Method | Purpose |
|---|---|
build_style() -> Option<taffy::Style> |
The widget’s flexbox style. None means “no style contribution”. BoxRenderable returns its border + gap style. |
layout_node() -> Option<taffy::NodeId> / set_layout_node(..) |
The widget’s node id in the shared LayoutTree. |
apply_layout(&mut self, &taffy::Layout) |
Called with the computed position/size after layout, so the widget can store it. |
Focus & interaction
focus()/blur()— called by the framework when focus moves.process_mouse_event(&mut self, event: &MouseEvent) -> bool— handle a mouse event; returntrueif consumed (stops propagation).request_render()— ask the renderer to redraw.opacity()/set_opacity(v)— blend factor, default1.0.z_index()/set_z_index(v)— stacking order, default0.live()/set_live(v)— whether the widget wants continuous redraws.on_update(delta_time)— per-frame update hook.on_resize(width, height)— resize notification.destroy()— teardown.
The free helpers: adopt_child / adopt_child_before
Widgets typically implement add_child by delegating to the shared helpers,
which set the parent link and push the child:
pub fn adopt_child(
parent_num: u64,
children: &mut Vec<Box<dyn Renderable>>,
mut child: Box<dyn Renderable>,
) -> usize {
child.set_parent_num(Some(parent_num));
let idx = children.len();
children.push(child);
idx
}
adopt_child_before does the same but inserts before the child matching
anchor_id, returning Option<usize>.
RenderableNode
A concrete, reusable implementation of the trait’s bookkeeping. It stores the
identity, flags, parent link, children, and layout node — everything except
the drawing itself. Widgets can either embed a RenderableNode and delegate,
or replicate the pattern directly (most widgets in this crate replicate it,
since they add their own fields).
RenderableNode::new(id: Option<String>) -> Self // auto-id "renderable-{n}" if None
node.add_child(child) -> usize
node.remove_child(id)
node.insert_child_before(child, anchor_id) -> Option<usize>
node.children_ref() -> &[Box<dyn Renderable>]
node.children_mut() -> &mut Vec<Box<dyn Renderable>>
node.layout_node() -> Option<taffy::NodeId>
node.set_layout_node(node)
RootRenderable
The top of the tree. It’s a Renderable whose render_self does nothing —
its job is to be the parent every top-level widget is attached to, so the
renderer has a single root to walk:
RootRenderable::new() // id "__root__"
root.add_child(boxed_widget) -> usize // returns child index
root.remove_child(id)
root.insert_child_before(child, anchor_id) -> Option<usize>
root.set_id(String)
root.set_visible(bool) // const
root.set_focusable(bool) // const
Usage with the managed renderer:
let mut renderer = Renderer::new(terminal, RendererConfig::default());
renderer.root_mut().add_child(Box::new(my_panel));
renderer.render_frame(0.0)?;
Writing your own renderable
The minimal widget needs: an identity, the lifecycle flags, as_any/as_any_mut,
render_self, and child management. Everything else has a sensible default.
A tiny example — a label that draws “hello” at the area’s top-left:
struct Label { id: String, num: u64 }
impl Renderable for Label {
fn id(&self) -> &str { &self.id }
fn num(&self) -> u64 { self.num }
fn is_visible(&self) -> bool { true }
fn is_focusable(&self) -> bool { false }
fn is_destroyed(&self) -> bool { false }
fn parent_num(&self) -> Option<u64> { None }
fn set_parent_num(&mut self, _p: Option<u64>) {}
fn as_any(&self) -> &dyn Any { self }
fn as_any_mut(&mut self) -> &mut dyn Any { self }
fn render_self(&self, buf: &mut Buffer, area: Rect) {
for (i, ch) in "hello".chars().enumerate() {
if let Some(cell) = buf.cell_mut((area.x + i as u16, area.y)) {
cell.set_char(ch);
}
}
}
fn children(&self) -> &[Box<dyn Renderable>] { &[] }
fn add_child(&mut self, _c: Box<dyn Renderable>) -> usize { 0 }
fn remove_child(&mut self, _id: &str) {}
fn insert_child_before(&mut self, _c: Box<dyn Renderable>, _a: &str) -> Option<usize> { None }
}
In practice you rarely write a renderable from scratch — the
renderables library already covers text, boxes,
markdown, inputs, selects, scrollbars, tables, and more, and RenderableNode
handles the bookkeeping if you do.
Next: types — attributes, mouse events, selection.
Summary
Renderabletrait: every screen element implements it with identity (id,num,parent_num), lifecycle flags (is_visible,is_focusable,is_destroyed), type erasure (as_any/as_any_mut), drawing (render_self), and child management.- Identity:
id()is stable string identifier unique among siblings,num()is globally uniqueu64for parent-link currency,parent_num()tracks adoption. - Lifecycle flags:
is_visible()(whether to draw),is_focusable()(interactive widgets),is_destroyed()(torn down). - Type erasure: children stored as
Box<dyn Renderable>, downcast viaas_any().downcast_ref::<ConcreteType>(). - Drawing:
render_self(&self, buf, area)writes visuals into ratatuiBufferwithinarea;render(buf, area, delta_time)is per-frame wrapper for animation. - Children:
add_childreturns insertion index,remove_childremoves by id,insert_child_beforeinserts before anchor id; defaultchildren_mutreturns empty slice. - Layout hooks:
build_style()(widget’s flexbox style),layout_node()/set_layout_node()(taffy node id),apply_layout()(store computed position/size). - Focus & interaction:
focus()/blur(),process_mouse_event()(return true if consumed),request_render(), opacity/z-index/live flags,on_update/on_resize/destroyhooks. - Free helpers:
adopt_child/adopt_child_beforeset parent link and push child; widgets typically delegate to these. RenderableNode: concrete reusable implementation of bookkeeping (identity, flags, parent link, children, layout node); widgets can embed it or replicate the pattern.RootRenderable: top of tree withrender_selfthat does nothing — parent for all top-level widgets so renderer has single root to walk.