Rendering Pipeline
This page documents how the TUI renders frames to the terminal. The rendering pipeline computes layouts, renders widgets, and manages the terminal buffer.
Pipeline Overview
┌─────────────────────────────────────────────────────────────────┐
│ Frame Rendering │
├─────────────────────────────────────────────────────────────────┤
│ 1. Build layout context │
│ 2. Compute widget sizes │
│ 3. Apply layout template │
│ 4. Update widget data │
│ 5. Render widgets in order │
│ 6. Render input/throbber │
│ 7. Render overlays │
│ 8. Set cursor position │
└─────────────────────────────────────────────────────────────────┘
Terminal Drawing
The ratatui Terminal::draw() method handles buffer management:
terminal.draw(|frame| {
self.render_frame(frame, show_throbber, prompt_len, indent_len);
})?;
The Frame object represents the current screen state. All rendering calls update a buffer that is flushed when the closure completes.
Frame Rendering
The main render function:
fn render_frame(
&mut self,
frame: &mut ratatui::Frame,
show_throbber: bool,
prompt_len: usize,
indent_len: usize,
) {
let frame_area = frame.area();
let theme = theme();
// 1. Build layout context
let ctx = LayoutContext {
frame_area,
show_throbber,
input_visual_lines: self.calculate_input_lines(),
theme: &theme,
active_widgets: self.active_widget_set(),
};
// 2. Compute widget sizes
let sizes = self.compute_widget_sizes(frame_area.height);
// 3. Apply layout template
let layout = self.layout_template.compute(&ctx, &sizes);
// 4. Update status bar data
self.update_status_bar_data();
// 5. Render widgets in order
for widget_id in &layout.render_order {
self.render_widget(frame, widget_id, &layout, &theme);
}
// 6. Render input or throbber
if let Some(input_area) = layout.input_area {
if show_throbber {
self.render_throbber(frame, input_area, &theme);
} else {
self.render_input(frame, input_area, &theme);
}
}
// 7. Render overlays
self.render_overlays(frame, frame_area, &theme);
// 8. Set cursor position
if !show_throbber && !self.overlay_active() {
let (x, y) = self.calculate_cursor_position(&layout);
frame.set_cursor_position((x, y));
}
}
Layout Context
Context passed to the layout system:
pub struct LayoutContext<'a> {
pub frame_area: Rect,
pub show_throbber: bool,
pub input_visual_lines: usize,
pub theme: &'a Theme,
pub active_widgets: HashSet<&'static str>,
}
Widget Sizes
Computed sizes for layout:
pub struct WidgetSizes {
pub heights: HashMap<&'static str, u16>,
pub is_active: HashMap<&'static str, bool>,
}
fn compute_widget_sizes(&self, available_height: u16) -> WidgetSizes {
let mut heights = HashMap::new();
let mut is_active = HashMap::new();
for (id, widget) in &self.widgets {
is_active.insert(*id, widget.is_active());
if widget.is_active() {
heights.insert(*id, widget.required_height(available_height));
}
}
WidgetSizes { heights, is_active }
}
Layout Template
The layout template computes widget positions:
pub trait LayoutProvider: Send + Sync + 'static {
fn compute(&self, ctx: &LayoutContext, sizes: &WidgetSizes) -> LayoutResult;
}
pub struct LayoutResult {
pub widget_areas: HashMap<&'static str, Rect>,
pub render_order: Vec<&'static str>,
pub input_area: Option<Rect>,
}
Standard Layout
The default layout arranges widgets vertically:
pub fn compute(ctx: &LayoutContext, sizes: &WidgetSizes) -> LayoutResult {
// Calculate heights
let input_height = if ctx.show_throbber { 3 } else { ctx.input_visual_lines + 2 };
let panel_height = calculate_panel_height(sizes);
let popup_height = calculate_popup_height(sizes);
let status_bar_height = sizes.height(STATUS_BAR);
// Build constraints
let constraints = vec![
Constraint::Min(MIN_MAIN_HEIGHT), // Chat view
Constraint::Length(panel_height), // Panels
Constraint::Length(popup_height), // Popups
Constraint::Length(input_height), // Input
Constraint::Length(status_bar_height), // Status bar
];
// Apply layout
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(ctx.frame_area);
// Map chunks to widget areas
let mut widget_areas = HashMap::new();
widget_areas.insert(CHAT_VIEW, chunks[0]);
// ... assign other areas
LayoutResult {
widget_areas,
render_order: vec![CHAT_VIEW, PERMISSION_PANEL, QUESTION_PANEL, SLASH_POPUP, STATUS_BAR],
input_area: Some(chunks[3]),
}
}
Layout Structure
┌─────────────────────────────────────────┐
│ Chat View │ Min height, flexible
├─────────────────────────────────────────┤
│ Permission/Question Panel │ Fixed height when active
├─────────────────────────────────────────┤
│ Slash Popup │ Fixed height when active
├─────────────────────────────────────────┤
│ Input / Throbber │ Dynamic height
├─────────────────────────────────────────┤
│ Status Bar │ Fixed height (2 lines)
└─────────────────────────────────────────┘
Widget Rendering
Widgets are rendered based on their type:
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,
};
match widget_id {
CHAT_VIEW => {
let pending = self.get_pending_status();
self.conversation_view.render(frame, area, theme, pending);
}
SLASH_POPUP => {
if let Some(popup) = self.widget::<SlashPopupState>(SLASH_POPUP) {
if popup.is_active() {
render_slash_popup(popup, &self.commands, frame, area, theme);
}
}
}
_ => {
if let Some(widget) = self.widgets.get_mut(widget_id) {
if widget.is_active() {
widget.render(frame, area, theme);
}
}
}
}
}
Render Order
Widgets are rendered in a specific order:
- Main content (ChatView) - Bottom layer
- Panels (PermissionPanel, QuestionPanel) - Above chat
- Popups (SlashPopup) - Above panels
- Input/Throbber - Above popups
- Status bar - Bottom of screen
- Overlays (ThemePicker, SessionPicker) - Always on top
Overlay Rendering
Overlays are rendered last, covering everything:
fn render_overlays(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) {
if let Some(picker) = self.widget::<ThemePickerState>(THEME_PICKER) {
if picker.is_active() {
render_theme_picker(picker, frame, area);
}
}
if let Some(picker) = self.widget::<SessionPickerState>(SESSION_PICKER) {
if picker.is_active() {
render_session_picker(picker, frame, area, theme);
}
}
}
Input Rendering
The input widget handles text display and cursor:
fn render_input(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
let block = Block::default()
.borders(Borders::ALL)
.border_style(theme.input_border)
.title(" Input ");
let inner = block.inner(area);
frame.render_widget(block, area);
let lines = self.wrap_input_text(inner.width as usize);
let paragraph = Paragraph::new(lines)
.style(theme.input_text);
frame.render_widget(paragraph, inner);
}
Throbber Rendering
The throbber shows processing state:
fn render_throbber(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
let message = self.get_throbber_message();
let spinner = self.throbber_state.current_frame();
let block = Block::default()
.borders(Borders::ALL)
.border_style(theme.throbber_border);
let inner = block.inner(area);
frame.render_widget(block, area);
let text = format!("{} {}", spinner, message);
let paragraph = Paragraph::new(text)
.style(theme.throbber_text);
frame.render_widget(paragraph, inner);
}
Cursor Positioning
The cursor is positioned after all rendering:
fn calculate_cursor_position(&self, layout: &LayoutResult) -> (u16, u16) {
let input_area = layout.input_area.unwrap();
let inner = Block::default().borders(Borders::ALL).inner(input_area);
// Calculate cursor position within input
let (line, col) = self.input_cursor_position(inner.width as usize);
(inner.x + col as u16, inner.y + line as u16)
}
Theme Integration
All widgets receive the current theme:
let theme = theme(); // Global theme accessor
// Pass to widgets
widget.render(frame, area, &theme);
// Use in styling
let style = Style::default()
.fg(theme.text_color)
.bg(theme.background);
Performance Optimization
Minimal Redraws
Ratatui uses differential updates - only changed cells are written to the terminal.
Area Clipping
Widgets only render within their assigned area:
frame.render_widget(paragraph, area); // Auto-clipped to area
Lazy Computation
Layout is computed once per frame:
let layout = self.layout_template.compute(&ctx, &sizes);
// Reused for all widget rendering
Next Steps
- Widget System - Widget management
- Widget Trait - Widget interface
- Event Loop - Event handling
