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.widgets HashMap
  • 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

TriggerExample
UiMessage receivedUserInteractionRequired activates QuestionPanel
User action/themes activates ThemePicker
StartupStatusBar 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

  1. Check if widget is active
  2. Call handle_key() with event and context
  3. Process result (Handled, Action, or NotHandled)
  4. 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

TriggerExample
User actionSubmit or cancel
External eventSession 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