1use super::scroll_buffer::ScrollBuffer;
20use crate::actor::Engine;
21use crate::buffer::{Buffer, Cell, Rgb};
22use crate::layout::Rect;
23use std::io::Write;
24use unicode_segmentation::UnicodeSegmentation;
25use unicode_width::UnicodeWidthStr;
26
27#[derive(Debug, Clone)]
29pub struct StreamConfig {
30 pub max_scrollback: usize,
32 pub default_fg: Rgb,
34 pub default_bg: Rgb,
36 pub auto_scroll: bool,
38 pub word_wrap: bool,
40}
41
42impl Default for StreamConfig {
43 fn default() -> Self {
44 Self {
45 max_scrollback: 10000,
46 default_fg: Rgb::new(220, 220, 220),
47 default_bg: Rgb::DEFAULT_BG,
48 auto_scroll: true,
49 word_wrap: true,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum AppendResult {
57 FastPath {
59 chars: usize,
61 start_col: u16,
63 row: u16,
65 },
66 SlowPath {
68 dirty_rect: Rect,
70 },
71 Empty,
73}
74
75pub struct StreamWidget {
83 bounds: Rect,
85 config: StreamConfig,
87 content: ScrollBuffer,
89 cursor_col: u16,
91 cursor_row: u16,
93 current_fg: Rgb,
95 current_bg: Rgb,
97 needs_full_redraw: bool,
99 dirty_rects: Vec<Rect>,
101}
102
103impl StreamWidget {
104 pub fn new(bounds: Rect) -> Self {
106 Self::with_config(bounds, StreamConfig::default())
107 }
108
109 pub fn with_config(bounds: Rect, config: StreamConfig) -> Self {
111 Self {
112 bounds,
113 current_fg: config.default_fg,
114 current_bg: config.default_bg,
115 content: ScrollBuffer::new(config.max_scrollback),
116 config,
117 cursor_col: 0,
118 cursor_row: 0,
119 needs_full_redraw: true,
120 dirty_rects: Vec::new(),
121 }
122 }
123
124 pub const fn bounds(&self) -> Rect {
126 self.bounds
127 }
128
129 pub fn set_bounds(&mut self, bounds: Rect) {
133 if bounds != self.bounds {
134 let width_changed = bounds.width != self.bounds.width;
135 self.bounds = bounds;
136 self.needs_full_redraw = true;
137
138 if width_changed && bounds.width > 0 {
140 self.content.rewrap(bounds.width as usize);
141 }
142 }
143 }
144
145 pub const fn set_fg(&mut self, fg: Rgb) {
147 self.current_fg = fg;
148 }
149
150 pub const fn set_bg(&mut self, bg: Rgb) {
152 self.current_bg = bg;
153 }
154
155 pub const fn reset_colors(&mut self) {
157 self.current_fg = self.config.default_fg;
158 self.current_bg = self.config.default_bg;
159 }
160
161 fn can_fast_path(&self, text: &str) -> bool {
169 if !self.content.at_bottom() {
171 return false;
172 }
173
174 if text.contains('\n') {
176 return false;
177 }
178
179 let text_width = UnicodeWidthStr::width(text);
181 let available = (self.bounds.width as usize).saturating_sub(self.cursor_col as usize);
182
183 text_width <= available
184 }
185
186 fn append_fast_path(&mut self, text: &str) -> AppendResult {
191 let start_col = self.cursor_col;
192 let row = self.cursor_row;
193 let mut char_count = 0;
194
195 let cells = text.graphemes(true).filter_map(|g| {
197 Cell::from_grapheme(g).map(|mut c| {
198 c.set_fg(self.current_fg);
199 c.set_bg(self.current_bg);
200 c
201 })
202 });
203 self.content.append(cells);
204
205 for grapheme in text.graphemes(true) {
207 let width = UnicodeWidthStr::width(grapheme);
208 self.cursor_col += u16::try_from(width).unwrap_or(0);
210 char_count += 1;
211 }
212
213 AppendResult::FastPath {
214 chars: char_count,
215 start_col,
216 row,
217 }
218 }
219
220 fn append_slow_path(&mut self, text: &str) -> AppendResult {
225 let initial_row = self.cursor_row;
226 let mut max_row = self.cursor_row;
227 let initial_col = self.cursor_col;
228 let mut min_touched_col = self.cursor_col;
229 let mut max_col = self.cursor_col;
230
231 for ch in text.chars() {
232 match ch {
233 '\n' => {
234 let was_at_bottom = self.content.at_bottom();
236 self.content.newline(false);
237 if !was_at_bottom {
238 self.content.scroll_up(1);
239 }
240
241 max_col = max_col.max(self.cursor_col);
242 self.cursor_col = 0;
243 min_touched_col = 0; self.cursor_row += 1;
245
246 if self.cursor_row >= self.bounds.height {
248 self.handle_scroll(was_at_bottom);
249 }
250 }
251 '\r' => {
252 self.cursor_col = 0;
254 min_touched_col = 0;
255 }
256 '\t' => {
257 let spaces = 4 - (self.cursor_col % 4);
259 for _ in 0..spaces {
260 self.append_char(' ');
261 }
262 }
263 _ => {
264 self.append_char(ch);
265 }
266 }
267
268 max_row = max_row.max(self.cursor_row);
269 max_col = max_col.max(self.cursor_col);
270
271 if self.cursor_col < initial_col && self.cursor_row > initial_row {
273 min_touched_col = 0;
274 }
275 }
276
277 let dirty_rect = Rect {
279 x: self.bounds.x + min_touched_col,
280 y: self.bounds.y + initial_row,
281 width: self.bounds.width,
282 height: (max_row - initial_row + 1).max(1),
283 };
284
285 if !self.needs_full_redraw {
286 self.dirty_rects.push(dirty_rect);
287 }
288
289 AppendResult::SlowPath { dirty_rect }
290 }
291
292 #[allow(clippy::cast_possible_truncation)]
294 fn append_char(&mut self, ch: char) {
295 let char_width = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
296
297 if self.cursor_col + char_width > self.bounds.width {
299 if self.config.word_wrap {
300 let was_at_bottom = self.content.at_bottom();
301 self.content.newline(true);
302 if !was_at_bottom {
303 self.content.scroll_up(1);
304 }
305
306 self.cursor_col = 0;
307 self.cursor_row += 1;
308
309 if self.cursor_row >= self.bounds.height {
310 self.handle_scroll(was_at_bottom);
311 }
312 } else {
313 return;
315 }
316 }
317
318 let mut cell = Cell::from_char(ch);
320 cell.set_fg(self.current_fg);
321 cell.set_bg(self.current_bg);
322
323 self.content.append(std::iter::once(cell));
324 self.cursor_col += char_width;
325 }
326
327 const fn handle_scroll(&mut self, was_at_bottom: bool) {
329 self.cursor_row = self.bounds.height - 1;
331
332 if self.config.auto_scroll && was_at_bottom {
335 self.content.scroll_to_bottom();
336 }
337
338 self.needs_full_redraw = true;
340 }
341
342 pub fn append(&mut self, text: &str) -> AppendResult {
347 if text.is_empty() {
348 return AppendResult::Empty;
349 }
350
351 if self.can_fast_path(text) {
352 self.append_fast_path(text)
353 } else {
354 self.append_slow_path(text)
355 }
356 }
357
358 #[allow(clippy::cast_possible_truncation)]
362 pub fn render(&mut self, buffer: &mut Buffer) {
363 let viewport_height = self.bounds.height as usize;
364
365 let visible_lines: Vec<_> = self.content.visible_lines(viewport_height).collect();
367
368 for (row, line) in visible_lines.iter().enumerate() {
370 let y = self.bounds.y + row as u16;
371 if y >= self.bounds.y + self.bounds.height {
372 break;
373 }
374
375 let mut col = 0u16;
376 for cell in &line.content {
377 if col >= self.bounds.width {
378 break;
379 }
380
381 let x = self.bounds.x + col;
382 buffer.set(x, y, *cell);
385
386 col += u16::from(cell.display_width());
387 }
388
389 while col < self.bounds.width {
391 let x = self.bounds.x + col;
392 buffer.set(x, y, Cell::new(' ').with_fg(self.current_fg).with_bg(self.current_bg));
393 col += 1;
394 }
395 }
396
397 for row in visible_lines.len()..viewport_height {
399 let y = self.bounds.y + row as u16;
400 for col in 0..self.bounds.width {
401 let x = self.bounds.x + col;
402 buffer.set(x, y, Cell::new(' ').with_fg(self.current_fg).with_bg(self.current_bg));
403 }
404 }
405
406 self.needs_full_redraw = false;
407 self.dirty_rects.clear();
408 }
409
410 pub fn write_fast_path(
415 &self,
416 result: AppendResult,
417 text: &str,
418 output: &mut Vec<u8>,
419 ) {
420 if let AppendResult::FastPath { start_col, row, .. } = result {
421 let abs_x = self.bounds.x + start_col + 1; let abs_y = self.bounds.y + row + 1; let _ = write!(output, "\x1b[{abs_y};{abs_x}H");
426
427 let _ = write!(
429 output,
430 "\x1b[38;2;{};{};{}m\x1b[48;2;{};{};{}m",
431 self.current_fg.r, self.current_fg.g, self.current_fg.b,
432 self.current_bg.r, self.current_bg.g, self.current_bg.b
433 );
434
435 output.extend_from_slice(text.as_bytes());
437 }
438 }
439
440 pub fn append_fast_into(&mut self, text: &str, output: &mut Vec<u8>) -> bool {
446 let result = self.append(text);
447 if let AppendResult::FastPath { .. } = result {
448 self.write_fast_path(result, text, output);
449 true
450 } else {
451 false
452 }
453 }
454
455 pub fn push(&mut self, engine: &Engine, text: &str) {
474 let result = self.append(text);
475
476 if let AppendResult::FastPath { .. } = result {
477 let mut output = Vec::with_capacity(64);
479 self.write_fast_path(result, text, &mut output);
480 engine.write_raw(output);
481 }
482 }
485
486 pub const fn needs_redraw(&self) -> bool {
488 self.needs_full_redraw || !self.dirty_rects.is_empty()
489 }
490
491 pub fn dirty_rects(&self) -> &[Rect] {
493 &self.dirty_rects
494 }
495
496 pub const fn invalidate(&mut self) {
498 self.needs_full_redraw = true;
499 }
500
501 pub fn clear(&mut self) {
503 self.content.clear();
504 self.cursor_col = 0;
505 self.cursor_row = 0;
506 self.needs_full_redraw = true;
507 }
508
509 pub fn scroll_up(&mut self, lines: usize) {
511 self.content.scroll_up(lines);
512 self.needs_full_redraw = true;
513 }
514
515 pub const fn scroll_down(&mut self, lines: usize) {
517 self.content.scroll_down(lines);
518 self.needs_full_redraw = true;
519 }
520
521 pub const fn cursor_position(&self) -> (u16, u16) {
523 (self.cursor_col, self.cursor_row)
524 }
525
526 pub fn line_count(&self) -> usize {
528 self.content.len()
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn test_stream_widget_new() {
538 let widget = StreamWidget::new(Rect::new(0, 0, 80, 24));
539 assert_eq!(widget.bounds().width, 80);
540 assert_eq!(widget.bounds().height, 24);
541 assert_eq!(widget.cursor_position(), (0, 0));
542 }
543
544 #[test]
545 fn test_stream_widget_append_fast_path() {
546 let mut widget = StreamWidget::new(Rect::new(0, 0, 80, 24));
547 let result = widget.append("Hello");
548
549 match result {
550 AppendResult::FastPath { chars, start_col, row } => {
551 assert_eq!(chars, 5);
552 assert_eq!(start_col, 0);
553 assert_eq!(row, 0);
554 }
555 _ => panic!("Expected fast path"),
556 }
557
558 assert_eq!(widget.cursor_position(), (5, 0));
559 }
560
561 #[test]
562 fn test_stream_widget_append_slow_path_newline() {
563 let mut widget = StreamWidget::new(Rect::new(0, 0, 80, 24));
564 let result = widget.append("Hello\nWorld");
565
566 match result {
567 AppendResult::SlowPath { .. } => {}
568 _ => panic!("Expected slow path due to newline"),
569 }
570
571 assert_eq!(widget.cursor_position(), (5, 1));
572 }
573
574 #[test]
575 fn test_stream_widget_wrap() {
576 let mut widget = StreamWidget::new(Rect::new(0, 0, 10, 24));
577
578 widget.append("12345678901234567890");
580
581 assert!(widget.cursor_row > 0);
583 }
584
585 #[test]
586 fn test_stream_widget_render() {
587 let mut widget = StreamWidget::new(Rect::new(0, 0, 10, 3));
588 widget.append("Line 1\nLine 2\nLine 3");
589
590 let mut buffer = Buffer::new(10, 3);
591 widget.render(&mut buffer);
592
593 let cell = buffer.get(0, 0).unwrap();
595 assert_eq!(cell.grapheme(), Some("L"));
596 }
597}