Widget Trait
This page documents the Widget trait, which defines the interface that all TUI widgets must implement. Understanding this trait is essential for creating custom widgets.
Trait Definition
pub trait Widget: Send + 'static {
/// Unique identifier for the widget
fn id(&self) -> &'static str;
/// Priority for key event handling (higher = first)
fn priority(&self) -> u8 {
100
}
/// Whether the widget is currently active/visible
fn is_active(&self) -> bool;
/// Handle key events
fn handle_key(&mut self, key: KeyEvent, ctx: &WidgetKeyContext) -> WidgetKeyResult;
/// Render the widget to the frame
fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme);
/// Required height for layout computation
fn required_height(&self, available: u16) -> u16 {
0
}
/// Whether this widget blocks text input
fn blocks_input(&self) -> bool {
false
}
/// Whether this is a full-screen overlay
fn is_overlay(&self) -> bool {
false
}
/// Downcasting support
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn into_any(self: Box<Self>) -> Box<dyn Any>;
}
Required Methods
id()
Returns the unique identifier for the widget:
fn id(&self) -> &'static str;
- Must be a
&'static str(compile-time constant) - Used as HashMap key for widget storage
- Should be unique across all widgets
// Example
pub const MY_WIDGET: &str = "my_widget";
fn id(&self) -> &'static str {
MY_WIDGET
}
is_active()
Returns whether the widget should participate in rendering and key handling:
fn is_active(&self) -> bool;
- Inactive widgets are skipped during rendering
- Inactive widgets do not receive key events
- Controlled by widget internal state
fn is_active(&self) -> bool {
self.active
}
handle_key()
Handles key events for the widget:
fn handle_key(&mut self, key: KeyEvent, ctx: &WidgetKeyContext) -> WidgetKeyResult;
Parameters:
key: The key event (code + modifiers)ctx: Context with theme and navigation helpers
Returns:
NotHandled: Key not consumed, continue dispatchHandled: Key consumed, stop dispatchAction(WidgetAction): Request app-level action
fn handle_key(&mut self, key: KeyEvent, ctx: &WidgetKeyContext) -> WidgetKeyResult {
if ctx.nav.is_up(key) {
self.move_selection_up();
return WidgetKeyResult::Handled;
}
if ctx.nav.is_select(key) {
return WidgetKeyResult::Action(WidgetAction::Close);
}
WidgetKeyResult::NotHandled
}
render()
Renders the widget to the given area:
fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme);
Parameters:
frame: Ratatui frame for renderingarea: Allocated area for this widgettheme: Current theme for styling
fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) {
let block = Block::default()
.title(self.title)
.borders(Borders::ALL)
.border_style(theme.border);
let inner = block.inner(area);
frame.render_widget(block, area);
let content = Paragraph::new(&self.content)
.style(theme.text);
frame.render_widget(content, inner);
}
Downcasting Methods
Required for type-erased widget storage:
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn into_any(self: Box<Self>) -> Box<dyn Any>;
Standard Implementation:
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn into_any(self: Box<Self>) -> Box<dyn Any> {
self
}
These enable downcasting from Box<dyn Widget> to concrete types.
Optional Methods
priority()
Determines key event handling order:
fn priority(&self) -> u8 {
100 // Default
}
Higher priority widgets receive key events first:
| Priority | Use Case |
|---|---|
| 120+ | Overlays (theme picker, session picker) |
| 100-119 | Modal widgets (permission, question panels) |
| 50-99 | Interactive content |
| 10-49 | Display-only widgets |
| 0-9 | Background widgets |
required_height()
Returns the height needed for layout:
fn required_height(&self, available: u16) -> u16 {
0 // Default: takes no space
}
Parameters:
available: Total available height
Returns:
- Fixed height for the widget
- 0 for flexible widgets (filled by layout)
// Fixed height widget
fn required_height(&self, _available: u16) -> u16 {
self.config.height // e.g., 2 for status bar
}
// Dynamic height widget
fn required_height(&self, available: u16) -> u16 {
(self.items.len() as u16).min(available / 2)
}
blocks_input()
Whether this widget blocks text input:
fn blocks_input(&self) -> bool {
false // Default
}
When true:
- Text input widget is disabled
- Keys go to this widget instead
- Used by modal widgets
// Modal widget blocks input
fn blocks_input(&self) -> bool {
self.is_active()
}
is_overlay()
Whether this is a full-screen overlay:
fn is_overlay(&self) -> bool {
false // Default
}
Overlays:
- Rendered last (on top of everything)
- Typically cover the entire screen
- Block all other interactions
// Full-screen picker
fn is_overlay(&self) -> bool {
true
}
Key Event Context
The context passed to handle_key():
pub struct WidgetKeyContext<'a> {
pub theme: &'a Theme,
pub nav: NavigationHelper<'a>,
}
NavigationHelper
Respects configured key bindings:
impl NavigationHelper<'_> {
pub fn is_up(&self, key: KeyEvent) -> bool;
pub fn is_down(&self, key: KeyEvent) -> bool;
pub fn is_left(&self, key: KeyEvent) -> bool;
pub fn is_right(&self, key: KeyEvent) -> bool;
pub fn is_select(&self, key: KeyEvent) -> bool;
pub fn is_cancel(&self, key: KeyEvent) -> bool;
pub fn is_tab(&self, key: KeyEvent) -> bool;
}
Using these helpers ensures widgets work with vim-style and arrow key bindings.
Widget Key Result
Return values from handle_key():
pub enum WidgetKeyResult {
NotHandled,
Handled,
Action(WidgetAction),
}
| Result | Effect |
|---|---|
NotHandled | Continue to next widget |
Handled | Stop dispatch, key consumed |
Action(action) | Request app-level action |
Widget Actions
Actions that widgets can request:
pub enum WidgetAction {
SubmitQuestion {
tool_use_id: String,
response: AskUserQuestionsResponse,
},
CancelQuestion {
tool_use_id: String,
},
SubmitPermission {
tool_use_id: String,
response: PermissionResponse,
},
CancelPermission {
tool_use_id: String,
},
SwitchSession {
session_id: i64,
},
ExecuteCommand {
command: String,
},
Close,
}
Trait Bounds
pub trait Widget: Send + 'static
| Bound | Purpose |
|---|---|
Send | Widget can be sent between threads |
'static | Widget has no borrowed references |
Complete Example
pub struct SelectionWidget {
id: &'static str,
active: bool,
items: Vec<String>,
selected: usize,
}
impl SelectionWidget {
pub fn new(id: &'static str, items: Vec<String>) -> Self {
Self {
id,
active: false,
items,
selected: 0,
}
}
pub fn activate(&mut self) {
self.active = true;
self.selected = 0;
}
pub fn deactivate(&mut self) {
self.active = false;
}
}
impl Widget for SelectionWidget {
fn id(&self) -> &'static str {
self.id
}
fn priority(&self) -> u8 {
105 // Above content, below overlays
}
fn is_active(&self) -> bool {
self.active
}
fn handle_key(&mut self, key: KeyEvent, ctx: &WidgetKeyContext) -> WidgetKeyResult {
if ctx.nav.is_up(key) {
if self.selected > 0 {
self.selected -= 1;
}
return WidgetKeyResult::Handled;
}
if ctx.nav.is_down(key) {
if self.selected < self.items.len() - 1 {
self.selected += 1;
}
return WidgetKeyResult::Handled;
}
if ctx.nav.is_select(key) {
let item = self.items[self.selected].clone();
return WidgetKeyResult::Action(WidgetAction::ExecuteCommand {
command: item,
});
}
if ctx.nav.is_cancel(key) {
self.deactivate();
return WidgetKeyResult::Action(WidgetAction::Close);
}
WidgetKeyResult::NotHandled
}
fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) {
let block = Block::default()
.title(" Select ")
.borders(Borders::ALL);
let inner = block.inner(area);
frame.render_widget(block, area);
let items: Vec<Line> = self.items.iter().enumerate()
.map(|(i, item)| {
let style = if i == self.selected {
theme.selected
} else {
theme.normal
};
Line::from(item.as_str()).style(style)
})
.collect();
let list = Paragraph::new(items);
frame.render_widget(list, inner);
}
fn required_height(&self, _available: u16) -> u16 {
(self.items.len() + 2) as u16 // Items + border
}
fn blocks_input(&self) -> bool {
self.active
}
fn as_any(&self) -> &dyn Any { self }
fn as_any_mut(&mut self) -> &mut dyn Any { self }
fn into_any(self: Box<Self>) -> Box<dyn Any> { self }
}
Next Steps
- Widget Lifecycle - Lifecycle management
- Widget System - Widget composition
- Rendering Pipeline - Rendering details
