1use std::io::Write;
11
12use crate::console::{ConsoleOptions, DynRenderable, RenderResult, Renderable};
13use crate::segment::Segment;
14use crate::style::Style;
15
16pub struct Screen {
25 pub renderable: DynRenderable,
27 pub style: Option<Style>,
29 pub application_mode: bool,
31}
32
33impl Screen {
34 pub fn new(renderable: impl Renderable + Send + Sync + 'static) -> Self {
36 Self {
37 renderable: DynRenderable::new(renderable),
38 style: None,
39 application_mode: false,
40 }
41 }
42
43 pub fn style(mut self, style: Style) -> Self {
45 self.style = Some(style);
46 self
47 }
48
49 pub fn application_mode(mut self, mode: bool) -> Self {
51 self.application_mode = mode;
52 self
53 }
54
55 pub fn update<T>(&mut self, update: T)
57 where
58 T: Into<ScreenUpdate>,
59 {
60 let update = update.into();
61 self.renderable = update.renderable;
62 }
63}
64
65impl std::fmt::Debug for Screen {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("Screen")
68 .field("style", &self.style)
69 .field("application_mode", &self.application_mode)
70 .finish()
71 }
72}
73
74impl Renderable for Screen {
75 fn render(&self, options: &ConsoleOptions) -> RenderResult {
76 let width = options.size.width.max(1);
77 let height = options.size.height.max(1);
78
79 let render_options = options.update_width(width).update_height(height);
81
82 let result = self.renderable.render(&render_options);
84
85 let mut lines: Vec<Vec<Segment>> = if !result.lines.is_empty() {
87 result.lines
88 } else {
89 let segments = result.flatten(&render_options);
90 if segments.is_empty() {
91 vec![vec![]]
92 } else {
93 let mut grouped: Vec<Vec<Segment>> = Vec::new();
95 let mut current_line: Vec<Segment> = Vec::new();
96 for seg in segments {
97 if seg.text == "\n" || seg.text == "\r\n" {
98 grouped.push(std::mem::take(&mut current_line));
99 } else {
100 current_line.push(seg);
101 }
102 }
103 if !current_line.is_empty() {
104 grouped.push(current_line);
105 }
106 if grouped.is_empty() {
107 grouped.push(vec![]);
108 }
109 grouped
110 }
111 };
112
113 if let Some(ref screen_style) = self.style {
117 for line in &mut lines {
118 for seg in line.iter_mut() {
119 if let Some(ref existing) = seg.style {
120 seg.style = Some(existing.combine(screen_style));
121 } else {
122 seg.style = Some(screen_style.clone());
123 }
124 }
125 }
126 }
127
128 let blank_seg = if let Some(ref style) = self.style {
130 Segment::styled(" ".repeat(width), style.clone())
131 } else {
132 Segment::new(" ".repeat(width))
133 };
134
135 for line in &mut lines {
136 let line_len: usize = line.iter().map(|s| s.cell_length()).sum();
137 if line_len > width {
138 let mut cropped: Vec<Segment> = Vec::new();
140 let mut accumulated = 0usize;
141 for seg in line.drain(..) {
142 let seg_len = seg.cell_length();
143 if accumulated + seg_len <= width {
144 cropped.push(seg);
145 accumulated += seg_len;
146 } else if accumulated < width {
147 let remaining = width - accumulated;
148 let (left, _) = seg.split(remaining);
149 if left.cell_length() > 0 {
150 cropped.push(left);
151 }
152 break;
153 } else {
154 break;
155 }
156 }
157 *line = cropped;
158 } else if line_len < width {
159 if let Some(ref style) = self.style {
161 line.push(Segment::styled(" ".repeat(width - line_len), style.clone()));
162 } else {
163 line.push(Segment::new(" ".repeat(width - line_len)));
164 }
165 }
166 }
167
168 if lines.len() > height {
170 lines.truncate(height);
171 } else {
172 while lines.len() < height {
173 lines.push(vec![blank_seg.clone()]);
174 }
175 }
176
177 let new_line_char = if self.application_mode { "\n\r" } else { "\n" };
179 let mut final_lines: Vec<Vec<Segment>> = Vec::with_capacity(lines.len() * 2);
180 let last_idx = lines.len().saturating_sub(1);
181 for (i, line) in lines.into_iter().enumerate() {
182 final_lines.push(line);
183 if i < last_idx {
184 final_lines.push(vec![Segment::new(new_line_char)]);
185 }
186 }
187
188 RenderResult {
189 lines: final_lines,
190 items: Vec::new(),
191 }
192 }
193}
194
195pub struct ScreenUpdate {
204 pub renderable: DynRenderable,
206}
207
208impl ScreenUpdate {
209 pub fn new(renderable: impl Renderable + Send + Sync + 'static) -> Self {
211 Self {
212 renderable: DynRenderable::new(renderable),
213 }
214 }
215}
216
217impl std::fmt::Debug for ScreenUpdate {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 f.debug_struct("ScreenUpdate").finish()
220 }
221}
222
223impl<R> From<R> for ScreenUpdate
224where
225 R: Renderable + Send + Sync + 'static,
226{
227 fn from(renderable: R) -> Self {
228 Self::new(renderable)
229 }
230}
231
232pub struct ScreenContext {
253 active: bool,
255 style: Option<Style>,
257}
258
259impl ScreenContext {
260 pub fn new() -> Self {
262 Self {
263 active: false,
264 style: None,
265 }
266 }
267
268 pub fn style(mut self, style: Style) -> Self {
270 self.style = Some(style);
271 self
272 }
273
274 pub fn enter(&mut self) {
276 if !self.active {
277 let _ = write!(std::io::stdout(), "\x1b[?1049h");
278 let _ = std::io::stdout().flush();
279 self.active = true;
280 }
281 }
282
283 pub fn exit(&mut self) {
285 if self.active {
286 let _ = write!(std::io::stdout(), "\x1b[?1049l");
287 let _ = std::io::stdout().flush();
288 self.active = false;
289 }
290 }
291
292 pub fn update(&mut self, update: impl Into<ScreenUpdate>) -> std::io::Result<()> {
294 if !self.active {
295 self.enter();
296 }
297
298 let opts = ConsoleOptions::default();
299 let screen = Screen {
300 renderable: update.into().renderable,
301 style: self.style.clone(),
302 application_mode: false,
303 };
304 let result = screen.render(&opts);
305 let ansi = result.to_ansi();
306 write!(std::io::stdout(), "{ansi}")?;
307 std::io::stdout().flush()
308 }
309
310 pub fn is_active(&self) -> bool {
312 self.active
313 }
314}
315
316impl Default for ScreenContext {
317 fn default() -> Self {
318 Self::new()
319 }
320}
321
322impl Drop for ScreenContext {
323 fn drop(&mut self) {
324 self.exit();
325 }
326}
327
328impl std::fmt::Debug for ScreenContext {
329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 f.debug_struct("ScreenContext")
331 .field("active", &self.active)
332 .finish()
333 }
334}
335
336#[cfg(test)]
341mod tests {
342 use super::*;
343 use crate::console::ConsoleDimensions;
344 use crate::style::Style;
345
346 #[test]
347 fn test_screen_creation() {
348 let screen = Screen::new("Hello");
349 assert!(screen.style.is_none());
350 assert!(!screen.application_mode);
351 }
352
353 #[test]
354 fn test_screen_with_style() {
355 let screen = Screen::new("Hello").style(Style::new().bold(true));
356 assert!(screen.style.is_some());
357 }
358
359 #[test]
360 fn test_screen_application_mode() {
361 let screen = Screen::new("Hello").application_mode(true);
362 assert!(screen.application_mode);
363 }
364
365 #[test]
366 fn test_screen_crops_wide_content() {
367 let screen = Screen::new("Hello World!!!");
368 let opts = ConsoleOptions {
369 size: ConsoleDimensions {
370 width: 5,
371 height: 1,
372 },
373 max_width: 5,
374 max_height: 1,
375 ..Default::default()
376 };
377 let result = screen.render(&opts);
378 let ansi = result.to_ansi();
379 assert!(ansi.contains("Hello"));
381 assert!(!ansi.contains("World"));
382 }
383
384 #[test]
385 fn test_screen_pads_to_height() {
386 let screen = Screen::new("Hi");
387 let opts = ConsoleOptions {
388 size: ConsoleDimensions {
389 width: 10,
390 height: 5,
391 },
392 max_width: 10,
393 max_height: 5,
394 ..Default::default()
395 };
396 let result = screen.render(&opts);
397 let ansi = result.to_ansi();
398 assert!(ansi.contains("Hi"));
400 }
401
402 #[test]
403 fn test_screen_returns_render_result() {
404 let screen = Screen::new("Test content");
405 let opts = ConsoleOptions {
406 size: ConsoleDimensions {
407 width: 80,
408 height: 24,
409 },
410 max_width: 80,
411 max_height: 24,
412 ..Default::default()
413 };
414 let result = screen.render(&opts);
415 assert!(!result.lines.is_empty());
416 }
417
418 #[test]
419 fn test_screen_update_creation() {
420 let update = ScreenUpdate::new("Updated content");
421 let mut screen = Screen::new("Original");
422 screen.update(update);
423 let opts = ConsoleOptions {
424 size: ConsoleDimensions {
425 width: 80,
426 height: 24,
427 },
428 max_width: 80,
429 max_height: 24,
430 ..Default::default()
431 };
432 let result = screen.render(&opts);
433 let ansi = result.to_ansi();
434 assert!(ansi.contains("Updated"));
435 }
436
437 #[test]
438 fn test_screen_update_from_renderable() {
439 let update: ScreenUpdate = "Direct string".into();
441 let _screen = Screen::new(update.renderable);
442 }
443
444 #[test]
445 fn test_screen_context_creation() {
446 let ctx = ScreenContext::new();
447 assert!(!ctx.is_active());
448 }
449
450 #[test]
451 fn test_screen_context_default() {
452 let ctx = ScreenContext::default();
453 assert!(!ctx.is_active());
454 }
455
456 #[test]
457 fn test_screen_context_enter_exit() {
458 let mut ctx = ScreenContext::new();
459 ctx.enter();
461 assert!(ctx.is_active());
462 ctx.exit();
463 assert!(!ctx.is_active());
464 }
465
466 #[test]
467 fn test_screen_context_double_enter() {
468 let mut ctx = ScreenContext::new();
469 ctx.enter();
470 assert!(ctx.is_active());
471 ctx.enter();
473 assert!(ctx.is_active());
474 }
475}