1use crate::buffer::ScreenBuffer;
9use crate::compositor::Compositor;
10use crate::error::Result;
11use crate::geometry::Size;
12use crate::renderer::Renderer;
13use crate::terminal::Terminal;
14
15pub struct RenderContext {
22 current: ScreenBuffer,
23 previous: ScreenBuffer,
24 renderer: Renderer,
25 size: Size,
26 compositor: Option<Compositor>,
27}
28
29impl RenderContext {
30 pub fn new(terminal: &dyn Terminal) -> Result<Self> {
32 let size = terminal.size()?;
33 let caps = terminal.capabilities();
34 let renderer = Renderer::new(caps.color, caps.synchronized_output);
35 Ok(Self {
36 current: ScreenBuffer::new(size),
37 previous: ScreenBuffer::new(size),
38 renderer,
39 size,
40 compositor: None,
41 })
42 }
43
44 pub fn with_size(size: Size, renderer: Renderer) -> Self {
46 Self {
47 current: ScreenBuffer::new(size),
48 previous: ScreenBuffer::new(size),
49 renderer,
50 size,
51 compositor: None,
52 }
53 }
54
55 #[must_use]
60 pub fn with_compositor(mut self, compositor: Compositor) -> Self {
61 self.compositor = Some(compositor);
62 self
63 }
64
65 pub fn compositor(&self) -> Option<&Compositor> {
67 self.compositor.as_ref()
68 }
69
70 pub fn compositor_mut(&mut self) -> Option<&mut Compositor> {
72 self.compositor.as_mut()
73 }
74
75 pub fn size(&self) -> Size {
77 self.size
78 }
79
80 pub fn buffer_mut(&mut self) -> &mut ScreenBuffer {
82 &mut self.current
83 }
84
85 pub fn buffer(&self) -> &ScreenBuffer {
87 &self.current
88 }
89
90 pub fn begin_frame(&mut self) {
92 std::mem::swap(&mut self.current, &mut self.previous);
93 self.current.clear();
94 }
95
96 pub fn end_frame(&mut self, terminal: &mut dyn Terminal) -> Result<()> {
102 if let Some(ref compositor) = self.compositor {
104 compositor.compose(&mut self.current);
105 }
106
107 let changes = self.current.diff(&self.previous);
108 let output = self.renderer.render(&changes);
109 if !output.is_empty() {
110 terminal.write_raw(output.as_bytes())?;
111 terminal.flush()?;
112 }
113 Ok(())
114 }
115
116 pub fn handle_resize(&mut self, new_size: Size) {
118 self.size = new_size;
119 self.current.resize(new_size);
120 self.previous.resize(new_size);
121 if let Some(ref mut compositor) = self.compositor {
122 compositor.resize(new_size.width, new_size.height);
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::cell::Cell;
131 use crate::color::{Color, NamedColor};
132 use crate::compositor::Compositor;
133 use crate::geometry::Rect;
134 use crate::segment::Segment;
135 use crate::style::Style;
136 use crate::terminal::{ColorSupport, TestBackend};
137
138 #[test]
139 fn create_from_test_backend() {
140 let backend = TestBackend::new(80, 24);
141 let ctx = RenderContext::new(&backend);
142 assert!(ctx.is_ok());
143 let ctx = ctx.ok();
144 assert!(ctx.is_some());
145 let ctx = ctx.as_ref();
146 assert_eq!(ctx.map(|c| c.size()), Some(Size::new(80, 24)));
147 }
148
149 #[test]
150 fn begin_frame_clears_current() {
151 let renderer = Renderer::new(ColorSupport::TrueColor, false);
152 let mut ctx = RenderContext::with_size(Size::new(10, 5), renderer);
153
154 ctx.buffer_mut().set(0, 0, Cell::new("A", Style::default()));
156 assert_eq!(
157 ctx.buffer().get(0, 0).map(|c| c.grapheme.as_str()),
158 Some("A")
159 );
160
161 ctx.begin_frame();
163 assert!(ctx.buffer().get(0, 0).is_some_and(|c| c.is_blank()));
164 }
165
166 #[test]
167 fn end_frame_writes_to_terminal() {
168 let mut backend = TestBackend::new(10, 5);
169 let renderer = Renderer::new(ColorSupport::TrueColor, false);
170 let mut ctx = RenderContext::with_size(Size::new(10, 5), renderer);
171
172 ctx.buffer_mut().set(0, 0, Cell::new("A", Style::default()));
174
175 let result = ctx.end_frame(&mut backend);
177 assert!(result.is_ok());
178
179 let output = backend.buffer();
180 assert!(!output.is_empty());
181 let output_str = String::from_utf8_lossy(output);
183 assert!(output_str.contains('A'));
184 }
185
186 #[test]
187 fn second_frame_only_diffs() {
188 let mut backend = TestBackend::new(10, 5);
189 let renderer = Renderer::new(ColorSupport::TrueColor, false);
190 let mut ctx = RenderContext::with_size(Size::new(10, 5), renderer);
191
192 ctx.buffer_mut().set(0, 0, Cell::new("A", Style::default()));
194 let _ = ctx.end_frame(&mut backend);
195 backend.clear_buffer();
196
197 ctx.begin_frame();
199 ctx.buffer_mut().set(0, 0, Cell::new("A", Style::default()));
200 ctx.buffer_mut().set(1, 0, Cell::new("B", Style::default()));
201 let _ = ctx.end_frame(&mut backend);
202
203 let output = backend.buffer();
204 let output_str = String::from_utf8_lossy(output);
205 assert!(output_str.contains('B'));
209 }
210
211 #[test]
212 fn handle_resize() {
213 let renderer = Renderer::new(ColorSupport::TrueColor, false);
214 let mut ctx = RenderContext::with_size(Size::new(10, 5), renderer);
215 assert_eq!(ctx.size(), Size::new(10, 5));
216
217 ctx.handle_resize(Size::new(20, 10));
218 assert_eq!(ctx.size(), Size::new(20, 10));
219 assert_eq!(ctx.buffer().size(), Size::new(20, 10));
220 }
221
222 #[test]
223 fn styled_cell_rendering() {
224 let mut backend = TestBackend::new(10, 5);
225 let renderer = Renderer::new(ColorSupport::TrueColor, false);
226 let mut ctx = RenderContext::with_size(Size::new(10, 5), renderer);
227
228 let style = Style::new().fg(Color::Named(NamedColor::Red)).bold(true);
229 ctx.buffer_mut().set(0, 0, Cell::new("X", style));
230 let _ = ctx.end_frame(&mut backend);
231
232 let output = backend.buffer();
233 let output_str = String::from_utf8_lossy(output);
234 assert!(output_str.contains("\x1b[31m")); assert!(output_str.contains("\x1b[1m")); assert!(output_str.contains('X'));
237 }
238
239 #[test]
242 fn compositor_none_by_default() {
243 let renderer = Renderer::new(ColorSupport::TrueColor, false);
244 let ctx = RenderContext::with_size(Size::new(10, 5), renderer);
245 assert!(ctx.compositor().is_none());
246 }
247
248 #[test]
249 fn compositor_none_from_new() {
250 let backend = TestBackend::new(80, 24);
251 let result = RenderContext::new(&backend);
252 assert!(result.is_ok());
253 match result {
254 Ok(ctx) => assert!(ctx.compositor().is_none()),
255 Err(_) => unreachable!(),
256 }
257 }
258
259 #[test]
260 fn with_compositor_sets_compositor() {
261 let renderer = Renderer::new(ColorSupport::TrueColor, false);
262 let compositor = Compositor::new(10, 5);
263 let ctx = RenderContext::with_size(Size::new(10, 5), renderer).with_compositor(compositor);
264 assert!(ctx.compositor().is_some());
265 }
266
267 #[test]
268 fn compositor_accessor_returns_reference() {
269 let renderer = Renderer::new(ColorSupport::TrueColor, false);
270 let compositor = Compositor::new(80, 24);
271 let ctx = RenderContext::with_size(Size::new(80, 24), renderer).with_compositor(compositor);
272 match ctx.compositor() {
273 Some(c) => {
274 assert_eq!(c.screen_size(), Size::new(80, 24));
275 }
276 None => unreachable!(),
277 }
278 }
279
280 #[test]
281 fn compositor_mut_allows_mutation() {
282 let renderer = Renderer::new(ColorSupport::TrueColor, false);
283 let compositor = Compositor::new(80, 24);
284 let mut ctx =
285 RenderContext::with_size(Size::new(80, 24), renderer).with_compositor(compositor);
286
287 match ctx.compositor_mut() {
289 Some(c) => {
290 let region = Rect::new(0, 0, 10, 5);
291 c.add_widget(1, region, 0, vec![vec![Segment::new("test")]]);
292 assert_eq!(c.layer_count(), 1);
293 }
294 None => unreachable!(),
295 }
296 }
297
298 #[test]
299 fn end_frame_with_compositor_composes_before_diff() {
300 let mut backend = TestBackend::new(20, 5);
301 let renderer = Renderer::new(ColorSupport::TrueColor, false);
302 let mut compositor = Compositor::new(20, 5);
303
304 let region = Rect::new(0, 0, 20, 5);
306 compositor.add_widget(1, region, 0, vec![vec![Segment::new("Hello")]]);
307
308 let mut ctx =
309 RenderContext::with_size(Size::new(20, 5), renderer).with_compositor(compositor);
310
311 let result = ctx.end_frame(&mut backend);
313 assert!(result.is_ok());
314
315 let output = backend.buffer();
316 let output_str = String::from_utf8_lossy(output);
317 assert!(output_str.contains('H'));
319 assert!(output_str.contains("ello"));
320 }
321
322 #[test]
323 fn compositor_z_ordering_in_render_context() {
324 let mut backend = TestBackend::new(20, 5);
325 let renderer = Renderer::new(ColorSupport::TrueColor, false);
326 let mut compositor = Compositor::new(20, 5);
327
328 let bg_region = Rect::new(0, 0, 20, 5);
330 compositor.add_widget(1, bg_region, 0, vec![vec![Segment::new("BACKGROUND")]]);
331
332 let fg_region = Rect::new(0, 0, 20, 5);
334 compositor.add_widget(2, fg_region, 10, vec![vec![Segment::new("TOP")]]);
335
336 let mut ctx =
337 RenderContext::with_size(Size::new(20, 5), renderer).with_compositor(compositor);
338
339 let result = ctx.end_frame(&mut backend);
340 assert!(result.is_ok());
341
342 match ctx.buffer().get(0, 0) {
344 Some(cell) => {
345 assert_eq!(cell.grapheme, "T");
346 }
347 None => unreachable!(),
348 }
349 }
350
351 #[test]
352 fn handle_resize_updates_compositor() {
353 let renderer = Renderer::new(ColorSupport::TrueColor, false);
354 let compositor = Compositor::new(10, 5);
355 let mut ctx =
356 RenderContext::with_size(Size::new(10, 5), renderer).with_compositor(compositor);
357
358 ctx.handle_resize(Size::new(40, 20));
359
360 assert_eq!(ctx.size(), Size::new(40, 20));
361 match ctx.compositor() {
362 Some(c) => {
363 assert_eq!(c.screen_size(), Size::new(40, 20));
364 }
365 None => unreachable!(),
366 }
367 }
368
369 #[test]
370 fn integration_widget_segments_through_compositor() {
371 let mut backend = TestBackend::new(40, 10);
372 let renderer = Renderer::new(ColorSupport::TrueColor, false);
373 let mut compositor = Compositor::new(40, 10);
374
375 let title_region = Rect::new(0, 0, 40, 1);
377 let title_style = Style::new().fg(Color::Named(NamedColor::Green)).bold(true);
378 let title_seg = Segment::styled("Title Bar", title_style);
379 compositor.add_widget(1, title_region, 0, vec![vec![title_seg]]);
380
381 let content_region = Rect::new(0, 1, 40, 9);
383 let content_seg = Segment::new("Content here");
384 compositor.add_widget(2, content_region, 0, vec![vec![content_seg]]);
385
386 let mut ctx =
387 RenderContext::with_size(Size::new(40, 10), renderer).with_compositor(compositor);
388
389 let result = ctx.end_frame(&mut backend);
390 assert!(result.is_ok());
391
392 match ctx.buffer().get(0, 0) {
394 Some(cell) => {
395 assert_eq!(cell.grapheme, "T");
396 assert!(cell.style.bold);
397 }
398 None => unreachable!(),
399 }
400
401 match ctx.buffer().get(0, 1) {
403 Some(cell) => {
404 assert_eq!(cell.grapheme, "C");
405 }
406 None => unreachable!(),
407 }
408 }
409}