Widget System
This page documents how widgets are managed, composed, and interact within the TUI. The widget system provides a flexible architecture for building interactive terminal interfaces.
Widget Architecture
┌─────────────────────────────────────────────────────────────────┐
│ App │
│ widgets: HashMap<&'static str, Box<dyn Widget>> │
│ widget_priority_order: Vec<&'static str> │
└─────────────────────────────────┬───────────────────────────────┘
│
┌─────────────────────────┼─────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ StatusBar │ │ ChatView │ │ QuestionPanel │
│ priority:10 │ │ priority:50 │ │ priority:110 │
└───────────────┘ └───────────────┘ └───────────────┘
Widget Registration
Widgets are registered with unique IDs:
impl App {
pub fn register_widget<W: Widget>(&mut self, widget: W) {
let id = widget.id();
self.widgets.insert(id, Box::new(widget));
self.rebuild_priority_order();
}
fn rebuild_priority_order(&mut self) {
let mut ordered: Vec<_> = self.widgets.iter()
.map(|(id, w)| (*id, w.priority()))
.collect();
ordered.sort_by(|a, b| b.1.cmp(&a.1)); // Descending
self.widget_priority_order = ordered.into_iter()
.map(|(id, _)| id)
.collect();
}
}
Built-in Widgets
Core Widgets
| Widget | ID | Priority | Purpose |
|---|---|---|---|
| StatusBar | status_bar | 10 | Display status information |
| ChatView | chat_view | 50 | Conversation display |
| TextInput | text_input | - | User text input (special handling) |
Interactive Widgets
| Widget | ID | Priority | Purpose |
|---|---|---|---|
| PermissionPanel | permission_panel | 110 | Permission requests |
| QuestionPanel | question_panel | 110 | User questions |
| SlashPopup | slash_popup | 100 | Command completion |
Overlay Widgets
| Widget | ID | Priority | Purpose |
|---|---|---|---|
| ThemePicker | theme_picker | 120 | Theme selection |
| SessionPicker | session_picker | 120 | Session switching |
Widget Storage
Widgets are stored as type-erased trait objects:
pub widgets: HashMap<&'static str, Box<dyn Widget>>
This enables:
- Dynamic widget registration
- Uniform handling in loops
- Runtime polymorphism
Widget Access
Access widgets with type casting:
// Immutable access
pub fn widget<W: Widget + 'static>(&self, id: &str) -> Option<&W> {
self.widgets.get(id)
.and_then(|w| w.as_any().downcast_ref::<W>())
}
// Mutable access
pub fn widget_mut<W: Widget + 'static>(&mut self, id: &str) -> Option<&mut W> {
self.widgets.get_mut(id)
.and_then(|w| w.as_any_mut().downcast_mut::<W>())
}
Usage Example
// Access StatusBar
if let Some(status_bar) = self.widget_mut::<StatusBar>(STATUS_BAR) {
status_bar.update_data(new_data);
}
// Check if QuestionPanel is active
if let Some(panel) = self.widget::<QuestionPanel>(QUESTION_PANEL) {
if panel.is_active() {
// Handle active state
}
}
Priority System
Priority determines key event handling order:
fn handle_key(&mut self, key_event: KeyEvent) {
// Process widgets in priority order (highest first)
for widget_id in &self.widget_priority_order {
if let Some(widget) = self.widgets.get_mut(widget_id) {
if widget.is_active() {
match widget.handle_key(key_event, &ctx) {
WidgetKeyResult::Handled => return,
WidgetKeyResult::Action(action) => {
self.process_widget_action(action);
return;
}
WidgetKeyResult::NotHandled => continue,
}
}
}
}
}
Priority Guidelines
| Priority Range | Widget Type |
|---|---|
| 120+ | Overlay widgets (always first) |
| 100-119 | Modal widgets (block input) |
| 50-99 | Interactive content widgets |
| 10-49 | Display-only widgets |
| 0-9 | Background widgets |
Focus Management
The active widget system manages focus:
// Check if any modal is active
fn overlay_active(&self) -> bool {
self.widgets.values().any(|w| w.is_overlay() && w.is_active())
}
// Check if input should be blocked
fn input_blocked(&self) -> bool {
self.widgets.values().any(|w| w.blocks_input() && w.is_active())
}
Widget Activation
Widgets are activated programmatically:
// Activate QuestionPanel
fn activate_question_panel(&mut self, tool_use_id: String, request: AskUserQuestionsRequest) {
if let Some(widget) = self.widgets.get_mut(QUESTION_PANEL) {
if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
panel.activate(tool_use_id, self.session_id, request, self.current_turn_id.clone());
}
}
}
// Deactivate
fn deactivate_question_panel(&mut self) {
if let Some(widget) = self.widgets.get_mut(QUESTION_PANEL) {
if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
panel.deactivate();
}
}
}
Widget Actions
Widgets communicate via actions:
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,
}
Action Processing
fn process_widget_action(&mut self, action: WidgetAction) {
match action {
WidgetAction::SubmitQuestion { tool_use_id, response } => {
self.submit_question_response(tool_use_id, response);
}
WidgetAction::CancelQuestion { tool_use_id } => {
self.cancel_question(tool_use_id);
}
WidgetAction::SwitchSession { session_id } => {
self.switch_session(session_id);
}
WidgetAction::ExecuteCommand { command } => {
self.execute_slash_command(&command);
}
WidgetAction::Close => {
self.close_active_overlay();
}
// ... other actions
}
}
Widget Key Context
Context passed to widgets for key handling:
pub struct WidgetKeyContext<'a> {
pub theme: &'a Theme,
pub nav: NavigationHelper<'a>,
}
pub struct NavigationHelper<'a> {
bindings: &'a KeyBindings,
}
impl NavigationHelper<'_> {
pub fn is_up(&self, key: KeyEvent) -> bool { /* ... */ }
pub fn is_down(&self, key: KeyEvent) -> bool { /* ... */ }
pub fn is_select(&self, key: KeyEvent) -> bool { /* ... */ }
pub fn is_cancel(&self, key: KeyEvent) -> bool { /* ... */ }
}
ConversationView
The conversation display uses a separate trait:
pub trait ConversationView: Send + 'static {
fn add_user_message(&mut self, content: String);
fn add_assistant_message(&mut self, content: String);
fn append_streaming(&mut self, text: &str);
fn complete_streaming(&mut self);
fn is_streaming(&self) -> bool;
fn add_tool_message(&mut self, tool_use_id: &str, name: &str, title: &str);
fn update_tool_status(&mut self, tool_use_id: &str, status: ToolStatus);
fn scroll_up(&mut self);
fn scroll_down(&mut self);
fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme, pending: Option<&str>);
fn save_state(&self) -> Box<dyn Any + Send>;
fn restore_state(&mut self, state: Box<dyn Any + Send>);
fn clear(&mut self);
}
Conversation Factory
Custom conversation views can be provided:
pub type ConversationViewFactory = Box<dyn Fn() -> Box<dyn ConversationView> + Send>;
// Default factory
let factory: ConversationViewFactory = Box::new(|| {
Box::new(ChatView::new())
});
Widget Composition
Widgets can be composed in the layout:
// Register multiple widgets
app.register_widget(StatusBar::new());
app.register_widget(PermissionPanel::new());
app.register_widget(QuestionPanel::new());
app.register_widget(SlashPopupState::new());
app.register_widget(ThemePickerState::new());
app.register_widget(SessionPickerState::new());
All registered widgets participate in:
- Key event dispatch (by priority)
- Rendering (if active)
- Layout computation
Custom Widgets
Create custom widgets by implementing the trait:
pub struct MyWidget {
active: bool,
data: String,
}
impl Widget for MyWidget {
fn id(&self) -> &'static str {
"my_widget"
}
fn priority(&self) -> u8 {
75
}
fn is_active(&self) -> bool {
self.active
}
fn handle_key(&mut self, key: KeyEvent, ctx: &WidgetKeyContext) -> WidgetKeyResult {
if ctx.nav.is_cancel(key) {
self.active = false;
return WidgetKeyResult::Action(WidgetAction::Close);
}
WidgetKeyResult::NotHandled
}
fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) {
let block = Block::default()
.title("My Widget")
.borders(Borders::ALL);
frame.render_widget(block, area);
}
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 Trait - Trait interface details
- Widget Lifecycle - Widget lifecycle management
- Rendering Pipeline - How widgets are rendered
