Widget Lifecycle
This page documents the lifecycle of widgets, from creation through activation, updates, rendering, and deactivation. Understanding the lifecycle helps in building widgets that properly manage state and resources.
Lifecycle Overview
┌─────────────────────────────────────────────────────────────────┐
│ Widget Lifecycle │
├─────────────────────────────────────────────────────────────────┤
│ 1. Creation - Widget constructed with new() │
│ 2. Registration - Added to App.widgets map │
│ 3. Activation - is_active() returns true │
│ 4. Event Loop - handle_key() called per event │
│ 5. Rendering - render() called each frame │
│ 6. Deactivation - is_active() returns false │
│ 7. (Optional) - Widget removed from map │
└─────────────────────────────────────────────────────────────────┘
Phase 1: Creation
Widgets are created using constructors:
impl StatusBar {
pub fn new() -> Self {
Self {
active: true,
data: StatusBarData::default(),
config: StatusBarConfig::default(),
}
}
pub fn with_config(config: StatusBarConfig) -> Self {
Self {
active: true,
data: StatusBarData::default(),
config,
}
}
}
At creation:
- Widget is not yet registered with App
- State is initialized to defaults
- Widget may or may not be active initially
Phase 2: Registration
Widgets are registered with the App:
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();
}
}
After registration:
- Widget is stored in
App.widgetsHashMap - Priority order is recalculated
- Widget participates in event loop
Registration Points
// During App initialization
pub fn with_config(config: AppConfig) -> Self {
let mut app = Self { /* ... */ };
app.register_widget(StatusBar::new());
app
}
// At runtime
app.register_widget(QuestionPanel::new());
app.register_widget(PermissionPanel::new());
Phase 3: Activation
Widgets are activated when they need to be visible/interactive:
impl QuestionPanel {
pub fn activate(
&mut self,
tool_use_id: String,
session_id: i64,
request: AskUserQuestionsRequest,
turn_id: Option<TurnId>,
) {
self.active = true;
self.tool_use_id = tool_use_id;
self.session_id = session_id;
self.request = request;
self.turn_id = turn_id;
self.selected_option = 0;
self.text_input.clear();
}
}
Activation Triggers
| Trigger | Example |
|---|---|
| UiMessage received | UserInteractionRequired activates QuestionPanel |
| User action | /themes activates ThemePicker |
| Startup | StatusBar active by default |
| Text input | / activates SlashPopup |
Activation from App
fn handle_ui_message(&mut self, msg: UiMessage) {
match msg {
UiMessage::UserInteractionRequired { tool_use_id, request, .. } => {
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());
}
}
}
// ...
}
}
Phase 4: Event Handling
Active widgets receive key events:
// Event loop iteration
fn handle_key(&mut self, key_event: KeyEvent) {
for widget_id in &self.widget_priority_order.clone() {
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,
}
}
}
}
}
Event Handling Order
- Check if widget is active
- Call
handle_key()with event and context - Process result (Handled, Action, or NotHandled)
- Continue to next widget if NotHandled
Phase 5: Rendering
Active widgets are rendered each frame:
fn render_widget(&mut self, frame: &mut Frame, widget_id: &str, layout: &LayoutResult, theme: &Theme) {
let area = match layout.widget_areas.get(widget_id) {
Some(area) => *area,
None => return,
};
if let Some(widget) = self.widgets.get_mut(widget_id) {
if widget.is_active() {
widget.render(frame, area, theme);
}
}
}
Rendering Cycle
For each frame:
1. Compute layout (allocate areas)
2. For each widget in render_order:
a. Check if active
b. Get allocated area
c. Call render()
3. Render overlays last
Phase 6: Deactivation
Widgets are deactivated when no longer needed:
impl QuestionPanel {
pub fn deactivate(&mut self) {
self.active = false;
self.request = AskUserQuestionsRequest::default();
self.text_input.clear();
}
}
Deactivation Triggers
| Trigger | Example |
|---|---|
| User action | Submit or cancel |
| External event | Session switch |
| Timeout | (if implemented) |
Deactivation from App
fn submit_question_response(&mut self, tool_use_id: String, response: AskUserQuestionsResponse) {
// Send response to controller
self.send_question_response(tool_use_id.clone(), response);
// Deactivate widget
if let Some(widget) = self.widgets.get_mut(QUESTION_PANEL) {
if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
panel.deactivate();
}
}
}
State Persistence
Some widgets persist state across sessions:
Conversation State
pub trait ConversationView {
fn save_state(&self) -> Box<dyn Any + Send>;
fn restore_state(&mut self, state: Box<dyn Any + Send>);
}
// Save when switching sessions
let state = self.conversation_view.save_state();
self.session_states.insert(old_session_id, state);
// Restore when returning
if let Some(state) = self.session_states.remove(&new_session_id) {
self.conversation_view.restore_state(state);
}
Widget Internal State
Widgets maintain their own state:
struct ThemePickerState {
active: bool,
selected_index: usize, // Persists while widget exists
themes: Vec<String>,
}
Widget Removal
Widgets can be removed at runtime:
impl App {
pub fn unregister_widget(&mut self, id: &str) -> Option<Box<dyn Widget>> {
let widget = self.widgets.remove(id);
self.rebuild_priority_order();
widget
}
}
This is rarely needed since most widgets persist for the App lifetime.
Data Updates
Widgets receive data updates through direct mutation:
// Update StatusBar data each frame
fn update_status_bar_data(&mut self) {
let data = StatusBarData {
cwd: self.get_cwd(),
model_name: self.model_name.clone(),
context_used: self.context_used,
context_limit: self.context_limit,
help_hint: self.key_handler.status_hint(),
};
if let Some(status_bar) = self.widget_mut::<StatusBar>(STATUS_BAR) {
status_bar.update_data(data);
}
}
Lifecycle Diagram
┌─────────────┐
│ new() │
└──────┬──────┘
│
▼
┌─────────────┐
│ register │
└──────┬──────┘
│
▼
┌────────────────────────┐
│ is_active() = false │◀─────────┐
└────────────┬───────────┘ │
│ activate() │
▼ │
┌────────────────────────┐ │
┌───▶│ is_active() = true │──────────┤
│ └────────────┬───────────┘ │
│ │ │
│ ┌────────────┴───────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌───────────┐ │
│ │handle_key│ │ render │ │
│ └────┬─────┘ └───────────┘ │
│ │ │
│ │ Action/Handled │
│ └──────────────┐ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
└──────────────│ update │ │
└──────┬──────┘ │
│ deactivate() │
└──────────────────┘
Best Practices
State Initialization
pub fn new() -> Self {
Self {
active: false, // Start inactive unless always visible
data: Default::default(),
}
}
Clean Activation
pub fn activate(&mut self, params: ActivationParams) {
self.active = true;
self.params = params;
self.reset_selection(); // Reset transient state
}
Clean Deactivation
pub fn deactivate(&mut self) {
self.active = false;
self.clear_sensitive_data(); // Clear any sensitive state
}
Graceful Rendering
fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) {
// Handle zero-size area gracefully
if area.width < 3 || area.height < 3 {
return;
}
// ... normal rendering
}
Next Steps
- Widget Trait - Trait interface
- Widget System - Widget management
- App Structure - App state management
