1#![forbid(unsafe_code)]
2
3use crate::{Widget, clear_text_area, draw_text_span};
21use ftui_core::geometry::Rect;
22use ftui_render::frame::Frame;
23use ftui_style::Style;
24use ftui_text::wrap::display_width;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct HistoryEntry {
29 pub description: String,
31 pub is_redo: bool,
33}
34
35impl HistoryEntry {
36 #[must_use]
38 pub fn new(description: impl Into<String>, is_redo: bool) -> Self {
39 Self {
40 description: description.into(),
41 is_redo,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum HistoryPanelMode {
49 #[default]
51 Compact,
52 Full,
54}
55
56#[derive(Debug, Clone)]
61pub struct HistoryPanel {
62 title: String,
64 undo_items: Vec<String>,
66 redo_items: Vec<String>,
68 mode: HistoryPanelMode,
70 compact_limit: usize,
72 title_style: Style,
74 undo_style: Style,
76 redo_style: Style,
78 marker_style: Style,
80 bg_style: Style,
82 marker_text: String,
84 undo_icon: String,
86 redo_icon: String,
88}
89
90impl Default for HistoryPanel {
91 fn default() -> Self {
92 Self::new()
93 }
94}
95
96impl HistoryPanel {
97 #[must_use]
99 pub fn new() -> Self {
100 Self {
101 title: "History".to_string(),
102 undo_items: Vec::new(),
103 redo_items: Vec::new(),
104 mode: HistoryPanelMode::Compact,
105 compact_limit: 5,
106 title_style: Style::new().bold(),
107 undo_style: Style::default(),
108 redo_style: Style::new().dim(),
109 marker_style: Style::new().bold(),
110 bg_style: Style::default(),
111 marker_text: "─── current ───".to_string(),
112 undo_icon: "↶ ".to_string(),
113 redo_icon: "↷ ".to_string(),
114 }
115 }
116
117 #[must_use]
119 pub fn with_title(mut self, title: impl Into<String>) -> Self {
120 self.title = title.into();
121 self
122 }
123
124 #[must_use]
126 pub fn with_undo_items(mut self, items: &[impl AsRef<str>]) -> Self {
127 self.undo_items = items.iter().map(|s| s.as_ref().to_string()).collect();
128 self
129 }
130
131 #[must_use]
133 pub fn with_redo_items(mut self, items: &[impl AsRef<str>]) -> Self {
134 self.redo_items = items.iter().map(|s| s.as_ref().to_string()).collect();
135 self
136 }
137
138 #[must_use]
140 pub fn with_mode(mut self, mode: HistoryPanelMode) -> Self {
141 self.mode = mode;
142 self
143 }
144
145 #[must_use]
147 pub fn with_compact_limit(mut self, limit: usize) -> Self {
148 self.compact_limit = limit;
149 self
150 }
151
152 #[must_use]
154 pub fn with_title_style(mut self, style: Style) -> Self {
155 self.title_style = style;
156 self
157 }
158
159 #[must_use]
161 pub fn with_undo_style(mut self, style: Style) -> Self {
162 self.undo_style = style;
163 self
164 }
165
166 #[must_use]
168 pub fn with_redo_style(mut self, style: Style) -> Self {
169 self.redo_style = style;
170 self
171 }
172
173 #[must_use]
175 pub fn with_marker_style(mut self, style: Style) -> Self {
176 self.marker_style = style;
177 self
178 }
179
180 #[must_use]
182 pub fn with_bg_style(mut self, style: Style) -> Self {
183 self.bg_style = style;
184 self
185 }
186
187 #[must_use]
189 pub fn with_marker_text(mut self, text: impl Into<String>) -> Self {
190 self.marker_text = text.into();
191 self
192 }
193
194 #[must_use]
196 pub fn with_undo_icon(mut self, icon: impl Into<String>) -> Self {
197 self.undo_icon = icon.into();
198 self
199 }
200
201 #[must_use]
203 pub fn with_redo_icon(mut self, icon: impl Into<String>) -> Self {
204 self.redo_icon = icon.into();
205 self
206 }
207
208 #[inline]
210 #[must_use]
211 pub fn is_empty(&self) -> bool {
212 self.undo_items.is_empty() && self.redo_items.is_empty()
213 }
214
215 #[inline]
217 #[must_use]
218 pub fn len(&self) -> usize {
219 self.undo_items.len() + self.redo_items.len()
220 }
221
222 #[must_use]
224 pub fn undo_items(&self) -> &[String] {
225 &self.undo_items
226 }
227
228 #[must_use]
230 pub fn redo_items(&self) -> &[String] {
231 &self.redo_items
232 }
233
234 fn render_content(&self, area: Rect, frame: &mut Frame) {
236 if area.width == 0 || area.height == 0 {
237 return;
238 }
239
240 let deg = frame.buffer.degradation;
241 if !deg.render_content() {
242 return;
243 }
244
245 let title_style = if deg.apply_styling() {
246 self.title_style
247 } else {
248 Style::default()
249 };
250 let undo_style = if deg.apply_styling() {
251 self.undo_style
252 } else {
253 Style::default()
254 };
255 let redo_style = if deg.apply_styling() {
256 self.redo_style
257 } else {
258 Style::default()
259 };
260 let marker_style = if deg.apply_styling() {
261 self.marker_style
262 } else {
263 Style::default()
264 };
265
266 let max_x = area.right();
267 let mut row: u16 = 0;
268
269 if row < area.height && !self.title.is_empty() {
271 let y = area.y.saturating_add(row);
272 draw_text_span(frame, area.x, y, &self.title, title_style, max_x);
273 row += 1;
274
275 if row < area.height {
277 row += 1;
278 }
279 }
280
281 let (undo_to_show, redo_to_show) = match self.mode {
283 HistoryPanelMode::Compact => {
284 let half_limit = self.compact_limit / 2;
285 let undo_start = self.undo_items.len().saturating_sub(half_limit);
286 let redo_end = half_limit.min(self.redo_items.len());
287 (&self.undo_items[undo_start..], &self.redo_items[..redo_end])
288 }
289 HistoryPanelMode::Full => (&self.undo_items[..], &self.redo_items[..]),
290 };
291
292 if self.mode == HistoryPanelMode::Compact
294 && undo_to_show.len() < self.undo_items.len()
295 && row < area.height
296 {
297 let y = area.y.saturating_add(row);
298 let hidden = self.undo_items.len() - undo_to_show.len();
299 let text = format!("... ({} more)", hidden);
300 draw_text_span(frame, area.x, y, &text, redo_style, max_x);
301 row += 1;
302 }
303
304 for desc in undo_to_show {
306 if row >= area.height {
307 break;
308 }
309 let y = area.y.saturating_add(row);
310 let icon_end = draw_text_span(frame, area.x, y, &self.undo_icon, undo_style, max_x);
311 draw_text_span(frame, icon_end, y, desc, undo_style, max_x);
312 row += 1;
313 }
314
315 if row < area.height {
317 let y = area.y.saturating_add(row);
318 let marker_width = display_width(&self.marker_text);
320 let available = area.width as usize;
321 let pad_left = available.saturating_sub(marker_width) / 2;
322 let x = area.x.saturating_add(pad_left as u16);
323 draw_text_span(frame, x, y, &self.marker_text, marker_style, max_x);
324 row += 1;
325 }
326
327 for desc in redo_to_show {
329 if row >= area.height {
330 break;
331 }
332 let y = area.y.saturating_add(row);
333 let icon_end = draw_text_span(frame, area.x, y, &self.redo_icon, redo_style, max_x);
334 draw_text_span(frame, icon_end, y, desc, redo_style, max_x);
335 row += 1;
336 }
337
338 if self.mode == HistoryPanelMode::Compact
340 && redo_to_show.len() < self.redo_items.len()
341 && row < area.height
342 {
343 let y = area.y.saturating_add(row);
344 let hidden = self.redo_items.len() - redo_to_show.len();
345 let text = format!("... ({} more)", hidden);
346 draw_text_span(frame, area.x, y, &text, redo_style, max_x);
347 }
348 }
349}
350
351impl Widget for HistoryPanel {
352 fn render(&self, area: Rect, frame: &mut Frame) {
353 let deg = frame.buffer.degradation;
354 if !deg.render_content() {
355 clear_text_area(frame, area, Style::default());
356 return;
357 }
358
359 let mut bg_cell = ftui_render::cell::Cell::from_char(' ');
361 crate::apply_style(
362 &mut bg_cell,
363 if deg.apply_styling() {
364 self.bg_style
365 } else {
366 Style::default()
367 },
368 );
369 frame.buffer.fill(area, bg_cell);
370
371 self.render_content(area, frame);
372 }
373
374 fn is_essential(&self) -> bool {
375 false
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382 use ftui_render::budget::DegradationLevel;
383 use ftui_render::frame::Frame;
384 use ftui_render::grapheme_pool::GraphemePool;
385
386 #[test]
387 fn new_panel_is_empty() {
388 let panel = HistoryPanel::new();
389 assert!(panel.is_empty());
390 assert_eq!(panel.len(), 0);
391 }
392
393 #[test]
394 fn with_undo_items() {
395 let panel = HistoryPanel::new().with_undo_items(&["Insert text", "Delete word"]);
396 assert_eq!(panel.undo_items().len(), 2);
397 assert_eq!(panel.undo_items()[0], "Insert text");
398 assert_eq!(panel.len(), 2);
399 }
400
401 #[test]
402 fn with_redo_items() {
403 let panel = HistoryPanel::new().with_redo_items(&["Paste"]);
404 assert_eq!(panel.redo_items().len(), 1);
405 assert_eq!(panel.len(), 1);
406 }
407
408 #[test]
409 fn with_both_stacks() {
410 let panel = HistoryPanel::new()
411 .with_undo_items(&["A", "B"])
412 .with_redo_items(&["C"]);
413 assert!(!panel.is_empty());
414 assert_eq!(panel.len(), 3);
415 }
416
417 #[test]
418 fn with_title() {
419 let panel = HistoryPanel::new().with_title("My History");
420 assert_eq!(panel.title, "My History");
421 }
422
423 #[test]
424 fn with_mode() {
425 let panel = HistoryPanel::new().with_mode(HistoryPanelMode::Full);
426 assert_eq!(panel.mode, HistoryPanelMode::Full);
427 }
428
429 #[test]
430 fn render_empty() {
431 let panel = HistoryPanel::new();
432 let mut pool = GraphemePool::new();
433 let mut frame = Frame::new(30, 10, &mut pool);
434 let area = Rect::new(0, 0, 30, 10);
435 panel.render(area, &mut frame); }
437
438 #[test]
439 fn render_with_items() {
440 let panel = HistoryPanel::new()
441 .with_undo_items(&["Insert text"])
442 .with_redo_items(&["Delete word"]);
443
444 let mut pool = GraphemePool::new();
445 let mut frame = Frame::new(30, 10, &mut pool);
446 let area = Rect::new(0, 0, 30, 10);
447 panel.render(area, &mut frame);
448
449 let cell = frame.buffer.get(0, 0).unwrap();
451 assert_eq!(cell.content.as_char(), Some('H')); }
453
454 #[test]
455 fn render_zero_area() {
456 let panel = HistoryPanel::new().with_undo_items(&["Test"]);
457 let mut pool = GraphemePool::new();
458 let mut frame = Frame::new(30, 10, &mut pool);
459 let area = Rect::new(0, 0, 0, 0);
460 panel.render(area, &mut frame); }
462
463 #[test]
464 fn compact_limit() {
465 let items: Vec<_> = (0..10).map(|i| format!("Item {}", i)).collect();
466 let panel = HistoryPanel::new()
467 .with_mode(HistoryPanelMode::Compact)
468 .with_compact_limit(4)
469 .with_undo_items(&items);
470
471 let mut pool = GraphemePool::new();
472 let mut frame = Frame::new(30, 20, &mut pool);
473 let area = Rect::new(0, 0, 30, 20);
474 panel.render(area, &mut frame); }
476
477 #[test]
478 fn is_not_essential() {
479 let panel = HistoryPanel::new();
480 assert!(!panel.is_essential());
481 }
482
483 #[test]
484 fn render_no_styling_drops_configured_styles() {
485 let fg = ftui_render::cell::PackedRgba::rgb(255, 0, 0);
486 let bg = ftui_render::cell::PackedRgba::rgb(0, 40, 80);
487 let panel = HistoryPanel::new()
488 .with_undo_items(&["Insert text"])
489 .with_title_style(Style::new().fg(fg).bold())
490 .with_undo_style(Style::new().fg(fg))
491 .with_marker_style(Style::new().fg(fg).italic())
492 .with_bg_style(Style::new().bg(bg));
493 let mut pool = GraphemePool::new();
494 let mut frame = Frame::new(30, 10, &mut pool);
495 frame.buffer.degradation = DegradationLevel::NoStyling;
496 let area = Rect::new(0, 0, 30, 10);
497
498 panel.render(area, &mut frame);
499
500 let title_cell = frame.buffer.get(0, 0).unwrap();
501 let background_cell = frame.buffer.get(15, 5).unwrap();
502 let default_text = ftui_render::cell::Cell::from_char('H');
503 let default_bg = ftui_render::cell::Cell::from_char(' ');
504
505 assert_eq!(title_cell.content.as_char(), Some('H'));
506 assert_eq!(title_cell.fg, default_text.fg);
507 assert_eq!(title_cell.bg, default_text.bg);
508 assert_eq!(title_cell.attrs, default_text.attrs);
509 assert_eq!(background_cell.fg, default_bg.fg);
510 assert_eq!(background_cell.bg, default_bg.bg);
511 assert_eq!(background_cell.attrs, default_bg.attrs);
512 }
513
514 #[test]
515 fn render_skeleton_is_noop() {
516 let panel = HistoryPanel::new().with_undo_items(&["Insert text"]);
517 let mut pool = GraphemePool::new();
518 let mut frame = Frame::new(30, 10, &mut pool);
519 let area = Rect::new(0, 0, 30, 10);
520 panel.render(area, &mut frame);
521
522 frame.buffer.degradation = DegradationLevel::Skeleton;
523 panel.render(area, &mut frame);
524
525 let cell = frame.buffer.get(0, 0).unwrap();
526 assert_eq!(cell.content.as_char(), Some(' '));
527 let default_cell = ftui_render::cell::Cell::from_char(' ');
528 assert_eq!(cell.fg, default_cell.fg);
529 assert_eq!(cell.bg, default_cell.bg);
530 assert_eq!(cell.attrs, default_cell.attrs);
531 }
532
533 #[test]
534 fn render_shorter_history_clears_stale_rows() {
535 let long = HistoryPanel::new()
536 .with_undo_items(&["Insert text", "Delete line", "Paste block"])
537 .with_redo_items(&["Redo thing"]);
538 let short = HistoryPanel::new().with_undo_items(&["Insert"]);
539 let mut pool = GraphemePool::new();
540 let mut frame = Frame::new(30, 10, &mut pool);
541 let area = Rect::new(0, 0, 30, 10);
542
543 long.render(area, &mut frame);
544 short.render(area, &mut frame);
545
546 for y in 6..10u16 {
547 for x in 0..30u16 {
548 assert_eq!(frame.buffer.get(x, y).unwrap().content.as_char(), Some(' '));
549 }
550 }
551 }
552
553 #[test]
554 fn default_impl() {
555 let panel = HistoryPanel::default();
556 assert!(panel.is_empty());
557 }
558
559 #[test]
560 fn with_icons() {
561 let panel = HistoryPanel::new()
562 .with_undo_icon("<< ")
563 .with_redo_icon(">> ");
564 assert_eq!(panel.undo_icon, "<< ");
565 assert_eq!(panel.redo_icon, ">> ");
566 }
567
568 #[test]
569 fn with_marker_text() {
570 let panel = HistoryPanel::new().with_marker_text("=== NOW ===");
571 assert_eq!(panel.marker_text, "=== NOW ===");
572 }
573
574 #[test]
575 fn history_entry_new() {
576 let entry = HistoryEntry::new("Delete line", false);
577 assert_eq!(entry.description, "Delete line");
578 assert!(!entry.is_redo);
579
580 let redo = HistoryEntry::new("Paste", true);
581 assert!(redo.is_redo);
582 }
583
584 #[test]
585 fn history_panel_mode_default_is_compact() {
586 assert_eq!(HistoryPanelMode::default(), HistoryPanelMode::Compact);
587 }
588
589 #[test]
590 fn with_compact_limit_setter() {
591 let panel = HistoryPanel::new().with_compact_limit(10);
592 assert_eq!(panel.compact_limit, 10);
593 }
594
595 #[test]
596 fn history_entry_equality() {
597 let a = HistoryEntry::new("X", false);
598 let b = HistoryEntry::new("X", false);
599 let c = HistoryEntry::new("X", true);
600 assert_eq!(a, b);
601 assert_ne!(a, c);
602 }
603
604 #[test]
605 fn full_mode_renders_all_items() {
606 let items: Vec<_> = (0..10).map(|i| format!("Item {i}")).collect();
607 let panel = HistoryPanel::new()
608 .with_mode(HistoryPanelMode::Full)
609 .with_undo_items(&items);
610
611 let mut pool = GraphemePool::new();
612 let mut frame = Frame::new(30, 30, &mut pool);
613 let area = Rect::new(0, 0, 30, 30);
614 panel.render(area, &mut frame); }
616
617 #[test]
620 fn style_setters_applied() {
621 let style = Style::new().italic();
622 let panel = HistoryPanel::new()
623 .with_title_style(style)
624 .with_undo_style(style)
625 .with_redo_style(style)
626 .with_marker_style(style)
627 .with_bg_style(style);
628 assert_eq!(panel.title_style, style);
629 assert_eq!(panel.undo_style, style);
630 assert_eq!(panel.redo_style, style);
631 assert_eq!(panel.marker_style, style);
632 assert_eq!(panel.bg_style, style);
633 }
634
635 #[test]
636 fn clone_preserves_all_fields() {
637 let panel = HistoryPanel::new()
638 .with_title("T")
639 .with_undo_items(&["A"])
640 .with_redo_items(&["B"])
641 .with_mode(HistoryPanelMode::Full)
642 .with_compact_limit(3)
643 .with_marker_text("NOW")
644 .with_undo_icon("U ")
645 .with_redo_icon("R ");
646 let cloned = panel.clone();
647 assert_eq!(cloned.title, "T");
648 assert_eq!(cloned.undo_items, vec!["A"]);
649 assert_eq!(cloned.redo_items, vec!["B"]);
650 assert_eq!(cloned.mode, HistoryPanelMode::Full);
651 assert_eq!(cloned.compact_limit, 3);
652 assert_eq!(cloned.marker_text, "NOW");
653 assert_eq!(cloned.undo_icon, "U ");
654 assert_eq!(cloned.redo_icon, "R ");
655 }
656
657 #[test]
658 fn debug_format() {
659 let panel = HistoryPanel::new();
660 let dbg = format!("{:?}", panel);
661 assert!(dbg.contains("HistoryPanel"));
662 assert!(dbg.contains("History"));
663
664 let entry = HistoryEntry::new("X", true);
665 let dbg_e = format!("{:?}", entry);
666 assert!(dbg_e.contains("HistoryEntry"));
667 assert!(dbg_e.contains("is_redo: true"));
668
669 let mode = HistoryPanelMode::Compact;
670 assert!(format!("{:?}", mode).contains("Compact"));
671 }
672
673 #[test]
674 fn history_entry_clone() {
675 let a = HistoryEntry::new("Hello", false);
676 let b = a.clone();
677 assert_eq!(a, b);
678 assert_eq!(b.description, "Hello");
679 }
680
681 #[test]
682 fn history_panel_mode_copy_eq() {
683 let a = HistoryPanelMode::Full;
684 let b = a; assert_eq!(a, b);
686 assert_ne!(a, HistoryPanelMode::Compact);
687 }
688
689 #[test]
690 fn render_only_redo_no_undo() {
691 let panel = HistoryPanel::new().with_redo_items(&["Redo1", "Redo2"]);
692 let mut pool = GraphemePool::new();
693 let mut frame = Frame::new(30, 10, &mut pool);
694 let area = Rect::new(0, 0, 30, 10);
695 panel.render(area, &mut frame);
696 let cell = frame.buffer.get(0, 0).unwrap();
698 assert_eq!(cell.content.as_char(), Some('H')); }
700
701 #[test]
702 fn render_empty_title() {
703 let panel = HistoryPanel::new().with_title("").with_undo_items(&["A"]);
704 let mut pool = GraphemePool::new();
705 let mut frame = Frame::new(30, 10, &mut pool);
706 let area = Rect::new(0, 0, 30, 10);
707 panel.render(area, &mut frame);
708 let cell = frame.buffer.get(0, 0).unwrap();
710 assert_ne!(cell.content.as_char(), Some('H'));
712 }
713
714 #[test]
715 fn compact_both_stacks_overflow() {
716 let undo: Vec<_> = (0..8).map(|i| format!("U{i}")).collect();
717 let redo: Vec<_> = (0..8).map(|i| format!("R{i}")).collect();
718 let panel = HistoryPanel::new()
719 .with_mode(HistoryPanelMode::Compact)
720 .with_compact_limit(4)
721 .with_undo_items(&undo)
722 .with_redo_items(&redo);
723 let mut pool = GraphemePool::new();
724 let mut frame = Frame::new(40, 20, &mut pool);
725 let area = Rect::new(0, 0, 40, 20);
726 panel.render(area, &mut frame);
727 }
729
730 #[test]
731 fn compact_limit_zero() {
732 let panel = HistoryPanel::new()
733 .with_compact_limit(0)
734 .with_undo_items(&["A", "B"])
735 .with_redo_items(&["C"]);
736 let mut pool = GraphemePool::new();
737 let mut frame = Frame::new(30, 10, &mut pool);
738 let area = Rect::new(0, 0, 30, 10);
739 panel.render(area, &mut frame); }
741
742 #[test]
743 fn compact_limit_one_odd() {
744 let panel = HistoryPanel::new()
745 .with_compact_limit(1)
746 .with_undo_items(&["A", "B", "C"])
747 .with_redo_items(&["D", "E"]);
748 let mut pool = GraphemePool::new();
749 let mut frame = Frame::new(30, 10, &mut pool);
750 let area = Rect::new(0, 0, 30, 10);
751 panel.render(area, &mut frame);
753 }
754
755 #[test]
756 fn render_width_one() {
757 let panel = HistoryPanel::new()
758 .with_undo_items(&["LongItem"])
759 .with_redo_items(&["AnotherLong"]);
760 let mut pool = GraphemePool::new();
761 let mut frame = Frame::new(30, 10, &mut pool);
762 let area = Rect::new(0, 0, 1, 10);
763 panel.render(area, &mut frame); }
765
766 #[test]
767 fn render_height_one() {
768 let panel = HistoryPanel::new()
769 .with_undo_items(&["A"])
770 .with_redo_items(&["B"]);
771 let mut pool = GraphemePool::new();
772 let mut frame = Frame::new(30, 10, &mut pool);
773 let area = Rect::new(0, 0, 30, 1);
774 panel.render(area, &mut frame); }
776
777 #[test]
778 fn render_height_three_no_room_for_redo() {
779 let panel = HistoryPanel::new()
780 .with_undo_items(&["A"])
781 .with_redo_items(&["B"]);
782 let mut pool = GraphemePool::new();
783 let mut frame = Frame::new(30, 10, &mut pool);
784 let area = Rect::new(0, 0, 30, 3);
786 panel.render(area, &mut frame);
787 }
788
789 #[test]
790 fn bg_style_fills_area() {
791 use ftui_render::cell::PackedRgba;
792 let red = PackedRgba::rgb(255, 0, 0);
793 let panel = HistoryPanel::new().with_bg_style(Style::new().bg(red));
794 let mut pool = GraphemePool::new();
795 let mut frame = Frame::new(10, 5, &mut pool);
796 let area = Rect::new(0, 0, 10, 5);
797 panel.render(area, &mut frame);
798 for y in 0..5u16 {
800 for x in 0..10u16 {
801 let cell = frame.buffer.get(x, y).unwrap();
802 assert_eq!(cell.bg, red);
803 }
804 }
805 }
806
807 #[test]
808 fn bg_style_none_does_not_fill() {
809 use ftui_render::cell::PackedRgba;
810 let panel = HistoryPanel::new();
811 let mut pool = GraphemePool::new();
812 let mut frame = Frame::new(10, 5, &mut pool);
813 let area = Rect::new(0, 0, 10, 5);
814 panel.render(area, &mut frame);
815 let cell = frame.buffer.get(5, 3).unwrap();
817 assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
818 }
819
820 #[test]
821 fn marker_centering_even_width() {
822 let panel = HistoryPanel::new()
823 .with_title("")
824 .with_marker_text("XX")
825 .with_undo_items(&["A"]);
826 let mut pool = GraphemePool::new();
827 let mut frame = Frame::new(20, 10, &mut pool);
828 let area = Rect::new(0, 0, 20, 10);
829 panel.render(area, &mut frame);
830 let cell_before = frame.buffer.get(8, 1).unwrap();
833 assert_ne!(cell_before.content.as_char(), Some('X'));
834 let cell_start = frame.buffer.get(9, 1).unwrap();
835 assert_eq!(cell_start.content.as_char(), Some('X'));
836 }
837
838 #[test]
839 fn marker_wider_than_area() {
840 let panel = HistoryPanel::new()
841 .with_title("")
842 .with_marker_text("VERY LONG MARKER TEXT THAT EXCEEDS");
843 let mut pool = GraphemePool::new();
844 let mut frame = Frame::new(10, 5, &mut pool);
845 let area = Rect::new(0, 0, 10, 5);
846 panel.render(area, &mut frame);
848 }
849
850 #[test]
851 fn overwrite_items_replaces() {
852 let panel = HistoryPanel::new()
853 .with_undo_items(&["Old1", "Old2"])
854 .with_undo_items(&["New1"]);
855 assert_eq!(panel.undo_items().len(), 1);
856 assert_eq!(panel.undo_items()[0], "New1");
857 }
858
859 #[test]
860 fn render_at_offset_area() {
861 let panel = HistoryPanel::new()
862 .with_undo_items(&["A"])
863 .with_redo_items(&["B"]);
864 let mut pool = GraphemePool::new();
865 let mut frame = Frame::new(30, 20, &mut pool);
866 let area = Rect::new(5, 5, 20, 10);
867 panel.render(area, &mut frame);
868 let cell = frame.buffer.get(5, 5).unwrap();
870 assert_eq!(cell.content.as_char(), Some('H'));
871 let origin = frame.buffer.get(0, 0).unwrap();
873 assert_ne!(origin.content.as_char(), Some('H'));
874 }
875
876 #[test]
877 fn empty_undo_icon_and_redo_icon() {
878 let panel = HistoryPanel::new()
879 .with_undo_icon("")
880 .with_redo_icon("")
881 .with_undo_items(&["A"])
882 .with_redo_items(&["B"]);
883 let mut pool = GraphemePool::new();
884 let mut frame = Frame::new(30, 10, &mut pool);
885 let area = Rect::new(0, 0, 30, 10);
886 panel.render(area, &mut frame);
887 }
888
889 #[test]
890 fn full_mode_no_ellipsis() {
891 let undo: Vec<_> = (0..10).map(|i| format!("U{i}")).collect();
892 let redo: Vec<_> = (0..10).map(|i| format!("R{i}")).collect();
893 let panel = HistoryPanel::new()
894 .with_mode(HistoryPanelMode::Full)
895 .with_undo_items(&undo)
896 .with_redo_items(&redo);
897 let mut pool = GraphemePool::new();
898 let mut frame = Frame::new(30, 30, &mut pool);
899 let area = Rect::new(0, 0, 30, 30);
900 panel.render(area, &mut frame);
901 }
903
904 #[test]
905 fn compact_undo_only_with_overflow() {
906 let items: Vec<_> = (0..10).map(|i| format!("Item{i}")).collect();
907 let panel = HistoryPanel::new()
908 .with_mode(HistoryPanelMode::Compact)
909 .with_compact_limit(4)
910 .with_undo_items(&items);
911 let mut pool = GraphemePool::new();
913 let mut frame = Frame::new(30, 15, &mut pool);
914 let area = Rect::new(0, 0, 30, 15);
915 panel.render(area, &mut frame);
916 }
917
918 #[test]
919 fn compact_redo_only_with_overflow() {
920 let items: Vec<_> = (0..10).map(|i| format!("Item{i}")).collect();
921 let panel = HistoryPanel::new()
922 .with_mode(HistoryPanelMode::Compact)
923 .with_compact_limit(4)
924 .with_redo_items(&items);
925 let mut pool = GraphemePool::new();
927 let mut frame = Frame::new(30, 15, &mut pool);
928 let area = Rect::new(0, 0, 30, 15);
929 panel.render(area, &mut frame);
930 }
931
932 #[test]
933 fn history_entry_from_string_type() {
934 let entry = HistoryEntry::new(String::from("Owned"), false);
935 assert_eq!(entry.description, "Owned");
936 }
937}