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