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 crate::test_env::without_no_color(|| {
225 let mut backend = TestBackend::new(10, 5);
226 let renderer = Renderer::new(ColorSupport::TrueColor, false);
227 let mut ctx = RenderContext::with_size(Size::new(10, 5), renderer);
228
229 let style = Style::new().fg(Color::Named(NamedColor::Red)).bold(true);
230 ctx.buffer_mut().set(0, 0, Cell::new("X", style));
231 let _ = ctx.end_frame(&mut backend);
232
233 let output = backend.buffer();
234 let output_str = String::from_utf8_lossy(output);
235 assert!(output_str.contains("\x1b[31m")); assert!(output_str.contains("\x1b[1m")); assert!(output_str.contains('X'));
238 });
239 }
240
241 #[test]
244 fn compositor_none_by_default() {
245 let renderer = Renderer::new(ColorSupport::TrueColor, false);
246 let ctx = RenderContext::with_size(Size::new(10, 5), renderer);
247 assert!(ctx.compositor().is_none());
248 }
249
250 #[test]
251 fn compositor_none_from_new() {
252 let backend = TestBackend::new(80, 24);
253 let result = RenderContext::new(&backend);
254 assert!(result.is_ok());
255 match result {
256 Ok(ctx) => assert!(ctx.compositor().is_none()),
257 Err(_) => unreachable!(),
258 }
259 }
260
261 #[test]
262 fn with_compositor_sets_compositor() {
263 let renderer = Renderer::new(ColorSupport::TrueColor, false);
264 let compositor = Compositor::new(10, 5);
265 let ctx = RenderContext::with_size(Size::new(10, 5), renderer).with_compositor(compositor);
266 assert!(ctx.compositor().is_some());
267 }
268
269 #[test]
270 fn compositor_accessor_returns_reference() {
271 let renderer = Renderer::new(ColorSupport::TrueColor, false);
272 let compositor = Compositor::new(80, 24);
273 let ctx = RenderContext::with_size(Size::new(80, 24), renderer).with_compositor(compositor);
274 match ctx.compositor() {
275 Some(c) => {
276 assert_eq!(c.screen_size(), Size::new(80, 24));
277 }
278 None => unreachable!(),
279 }
280 }
281
282 #[test]
283 fn compositor_mut_allows_mutation() {
284 let renderer = Renderer::new(ColorSupport::TrueColor, false);
285 let compositor = Compositor::new(80, 24);
286 let mut ctx =
287 RenderContext::with_size(Size::new(80, 24), renderer).with_compositor(compositor);
288
289 match ctx.compositor_mut() {
291 Some(c) => {
292 let region = Rect::new(0, 0, 10, 5);
293 c.add_widget(1, region, 0, vec![vec![Segment::new("test")]]);
294 assert_eq!(c.layer_count(), 1);
295 }
296 None => unreachable!(),
297 }
298 }
299
300 #[test]
301 fn end_frame_with_compositor_composes_before_diff() {
302 let mut backend = TestBackend::new(20, 5);
303 let renderer = Renderer::new(ColorSupport::TrueColor, false);
304 let mut compositor = Compositor::new(20, 5);
305
306 let region = Rect::new(0, 0, 20, 5);
308 compositor.add_widget(1, region, 0, vec![vec![Segment::new("Hello")]]);
309
310 let mut ctx =
311 RenderContext::with_size(Size::new(20, 5), renderer).with_compositor(compositor);
312
313 let result = ctx.end_frame(&mut backend);
315 assert!(result.is_ok());
316
317 let output = backend.buffer();
318 let output_str = String::from_utf8_lossy(output);
319 assert!(output_str.contains('H'));
321 assert!(output_str.contains("ello"));
322 }
323
324 #[test]
325 fn compositor_z_ordering_in_render_context() {
326 let mut backend = TestBackend::new(20, 5);
327 let renderer = Renderer::new(ColorSupport::TrueColor, false);
328 let mut compositor = Compositor::new(20, 5);
329
330 let bg_region = Rect::new(0, 0, 20, 5);
332 compositor.add_widget(1, bg_region, 0, vec![vec![Segment::new("BACKGROUND")]]);
333
334 let fg_region = Rect::new(0, 0, 20, 5);
336 compositor.add_widget(2, fg_region, 10, vec![vec![Segment::new("TOP")]]);
337
338 let mut ctx =
339 RenderContext::with_size(Size::new(20, 5), renderer).with_compositor(compositor);
340
341 let result = ctx.end_frame(&mut backend);
342 assert!(result.is_ok());
343
344 match ctx.buffer().get(0, 0) {
346 Some(cell) => {
347 assert_eq!(cell.grapheme, "T");
348 }
349 None => unreachable!(),
350 }
351 }
352
353 #[test]
354 fn handle_resize_updates_compositor() {
355 let renderer = Renderer::new(ColorSupport::TrueColor, false);
356 let compositor = Compositor::new(10, 5);
357 let mut ctx =
358 RenderContext::with_size(Size::new(10, 5), renderer).with_compositor(compositor);
359
360 ctx.handle_resize(Size::new(40, 20));
361
362 assert_eq!(ctx.size(), Size::new(40, 20));
363 match ctx.compositor() {
364 Some(c) => {
365 assert_eq!(c.screen_size(), Size::new(40, 20));
366 }
367 None => unreachable!(),
368 }
369 }
370
371 #[test]
372 fn integration_widget_segments_through_compositor() {
373 let mut backend = TestBackend::new(40, 10);
374 let renderer = Renderer::new(ColorSupport::TrueColor, false);
375 let mut compositor = Compositor::new(40, 10);
376
377 let title_region = Rect::new(0, 0, 40, 1);
379 let title_style = Style::new().fg(Color::Named(NamedColor::Green)).bold(true);
380 let title_seg = Segment::styled("Title Bar", title_style);
381 compositor.add_widget(1, title_region, 0, vec![vec![title_seg]]);
382
383 let content_region = Rect::new(0, 1, 40, 9);
385 let content_seg = Segment::new("Content here");
386 compositor.add_widget(2, content_region, 0, vec![vec![content_seg]]);
387
388 let mut ctx =
389 RenderContext::with_size(Size::new(40, 10), renderer).with_compositor(compositor);
390
391 let result = ctx.end_frame(&mut backend);
392 assert!(result.is_ok());
393
394 match ctx.buffer().get(0, 0) {
396 Some(cell) => {
397 assert_eq!(cell.grapheme, "T");
398 assert!(cell.style.bold);
399 }
400 None => unreachable!(),
401 }
402
403 match ctx.buffer().get(0, 1) {
405 Some(cell) => {
406 assert_eq!(cell.grapheme, "C");
407 }
408 None => unreachable!(),
409 }
410 }
411}