Skip to main content

wisp/renderer/
mod.rs

1//! Frame rendering.
2//!
3//! [`Renderer`] is the facade: it owns the drawing services (theme, syntax
4//! highlighter) and every cache, and sequences one draw. The rest of this
5//! module layers below it:
6//!
7//! - `layout` measures the frame's bands once per draw;
8//! - `history` moves overflow rows into the terminal's native scrollback;
9//! - `transcript` turns conversation items into rows, through the `cache` of
10//!   sealed items and the `streaming` incremental renderer;
11//! - `frame` places the bands, composer, status line, and overlays.
12//!
13//! The pure item→rows builders live with their models in
14//! `crate::conversation::{item_view, tool_view}`.
15
16mod cache;
17mod frame;
18mod history;
19mod layout;
20mod stats;
21mod streaming;
22mod transcript;
23
24use std::collections::HashMap;
25
26use crate::app::App;
27use crate::conversation::{ConversationId, ConversationItemId};
28use crate::view::generation::Generation;
29use crate::view::syntax::SyntaxHighlighter;
30use crate::theme::Theme;
31use frame::draw_frame;
32use ratatui::Terminal;
33use ratatui::backend::Backend;
34
35pub use stats::RenderStats;
36use cache::RenderCache;
37use history::{CommitPoint, NativeHistoryCursor};
38use layout::FrameLayout;
39use stats::Lap;
40use streaming::StreamEntry;
41
42/// Shared rendering services, borrowed for the duration of one draw.
43///
44/// `theme_generation` lets a route or overlay cache styled text across frames and
45/// rebuild it only when the theme actually changes.
46pub struct DrawContext<'a> {
47    pub theme: &'a Theme,
48    pub highlighter: &'a mut SyntaxHighlighter,
49    pub theme_generation: Generation,
50}
51
52/// Owns everything drawing a frame needs: the theme, the syntax highlighter,
53/// and the caches that keep streaming content stable between frames.
54///
55/// Methods destructure `self` rather than taking `&self.theme` through `&mut
56/// self`, so the theme is borrowed alongside the highlighter instead of cloned
57/// once per item per frame.
58pub struct Renderer {
59    theme: Theme,
60    highlighter: SyntaxHighlighter,
61    theme_generation: Option<Generation>,
62    render_cache: RenderCache,
63    native_history: NativeHistoryCursor,
64    stream_cache: HashMap<ConversationItemId, StreamEntry>,
65    stats: RenderStats,
66}
67
68impl Default for Renderer {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74impl Renderer {
75    pub fn new() -> Self {
76        Self {
77            theme: Theme::default(),
78            highlighter: SyntaxHighlighter::new(),
79            theme_generation: None,
80            render_cache: RenderCache::default(),
81            native_history: NativeHistoryCursor::default(),
82            stream_cache: HashMap::new(),
83            stats: RenderStats::default(),
84        }
85    }
86
87    pub fn take_stats(&mut self) -> RenderStats {
88        let highlight = self.highlighter.take_stats();
89        RenderStats { highlight, ..std::mem::take(&mut self.stats) }
90    }
91
92    pub fn theme(&self) -> &Theme {
93        &self.theme
94    }
95
96    /// The app owns the active theme; adopt it whenever its generation moves,
97    /// dropping every cache styled with the old one.
98    fn sync_theme(&mut self, app: &App) {
99        if self.theme_generation != Some(app.theme_generation()) {
100            self.theme = app.theme().clone();
101            self.theme_generation = Some(app.theme_generation());
102            self.highlighter.clear();
103            self.render_cache.clear();
104            self.stream_cache.clear();
105        }
106    }
107
108    fn generation(&self) -> Generation {
109        self.theme_generation.unwrap_or_default()
110    }
111
112    pub fn draw<B: Backend>(&mut self, terminal: &mut Terminal<B>, app: &mut App) -> Result<(), B::Error> {
113        self.sync_theme(app);
114        terminal.autoresize()?;
115        let area = terminal.get_frame().area();
116        self.sync_conversation(app.conversation_id());
117
118        let lap = Lap::start();
119        let layout = FrameLayout::new(area, app, self);
120        let capacity =
121            usize::from(layout.transcript_height).saturating_sub(usize::from(layout.progress_height));
122        self.stats.ns_layout += lap.ns();
123
124        let lap = Lap::start();
125        let live = self.commit_overflow(terminal, app, area.width, capacity)?;
126        self.stats.ns_live += lap.ns();
127
128        let live = if app.full_screen_active() { Vec::new() } else { live };
129        let lap = Lap::start();
130        terminal.draw(|frame| draw_frame(frame, app, self, &layout, &live))?;
131        self.stats.ns_draw += lap.ns();
132        self.stats.frames += 1;
133        self.render_cache.current = std::mem::take(&mut self.render_cache.frame);
134        Ok(())
135    }
136
137    /// The rendering services a full-screen route or overlay draws with.
138    fn context(&mut self) -> DrawContext<'_> {
139        let theme_generation = self.generation();
140        DrawContext { theme: &self.theme, highlighter: &mut self.highlighter, theme_generation }
141    }
142
143    fn sync_conversation(&mut self, conversation_id: ConversationId) {
144        if self.native_history.conversation_id != Some(conversation_id) {
145            self.native_history =
146                NativeHistoryCursor { conversation_id: Some(conversation_id), commit: CommitPoint::default() };
147            self.render_cache.clear();
148            self.stream_cache.clear();
149        }
150    }
151}