1#![forbid(unsafe_code)]
2
3use crate::{Widget, 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 max_x = area.right();
241 let mut row: u16 = 0;
242
243 if row < area.height && !self.title.is_empty() {
245 let y = area.y.saturating_add(row);
246 draw_text_span(frame, area.x, y, &self.title, self.title_style, max_x);
247 row += 1;
248
249 if row < area.height {
251 row += 1;
252 }
253 }
254
255 let (undo_to_show, redo_to_show) = match self.mode {
257 HistoryPanelMode::Compact => {
258 let half_limit = self.compact_limit / 2;
259 let undo_start = self.undo_items.len().saturating_sub(half_limit);
260 let redo_end = half_limit.min(self.redo_items.len());
261 (&self.undo_items[undo_start..], &self.redo_items[..redo_end])
262 }
263 HistoryPanelMode::Full => (&self.undo_items[..], &self.redo_items[..]),
264 };
265
266 if self.mode == HistoryPanelMode::Compact
268 && undo_to_show.len() < self.undo_items.len()
269 && row < area.height
270 {
271 let y = area.y.saturating_add(row);
272 let hidden = self.undo_items.len() - undo_to_show.len();
273 let text = format!("... ({} more)", hidden);
274 draw_text_span(frame, area.x, y, &text, self.redo_style, max_x);
275 row += 1;
276 }
277
278 for desc in undo_to_show {
280 if row >= area.height {
281 break;
282 }
283 let y = area.y.saturating_add(row);
284 let icon_end =
285 draw_text_span(frame, area.x, y, &self.undo_icon, self.undo_style, max_x);
286 draw_text_span(frame, icon_end, y, desc, self.undo_style, max_x);
287 row += 1;
288 }
289
290 if row < area.height {
292 let y = area.y.saturating_add(row);
293 let marker_width = display_width(&self.marker_text);
295 let available = area.width as usize;
296 let pad_left = available.saturating_sub(marker_width) / 2;
297 let x = area.x.saturating_add(pad_left as u16);
298 draw_text_span(frame, x, y, &self.marker_text, self.marker_style, max_x);
299 row += 1;
300 }
301
302 for desc in redo_to_show {
304 if row >= area.height {
305 break;
306 }
307 let y = area.y.saturating_add(row);
308 let icon_end =
309 draw_text_span(frame, area.x, y, &self.redo_icon, self.redo_style, max_x);
310 draw_text_span(frame, icon_end, y, desc, self.redo_style, max_x);
311 row += 1;
312 }
313
314 if self.mode == HistoryPanelMode::Compact
316 && redo_to_show.len() < self.redo_items.len()
317 && row < area.height
318 {
319 let y = area.y.saturating_add(row);
320 let hidden = self.redo_items.len() - redo_to_show.len();
321 let text = format!("... ({} more)", hidden);
322 draw_text_span(frame, area.x, y, &text, self.redo_style, max_x);
323 }
324 }
325}
326
327impl Widget for HistoryPanel {
328 fn render(&self, area: Rect, frame: &mut Frame) {
329 if let Some(bg) = self.bg_style.bg {
331 for y in area.y..area.bottom() {
332 for x in area.x..area.right() {
333 if let Some(cell) = frame.buffer.get_mut(x, y) {
334 cell.bg = bg;
335 }
336 }
337 }
338 }
339
340 self.render_content(area, frame);
341 }
342
343 fn is_essential(&self) -> bool {
344 false
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use ftui_render::frame::Frame;
352 use ftui_render::grapheme_pool::GraphemePool;
353
354 #[test]
355 fn new_panel_is_empty() {
356 let panel = HistoryPanel::new();
357 assert!(panel.is_empty());
358 assert_eq!(panel.len(), 0);
359 }
360
361 #[test]
362 fn with_undo_items() {
363 let panel = HistoryPanel::new().with_undo_items(&["Insert text", "Delete word"]);
364 assert_eq!(panel.undo_items().len(), 2);
365 assert_eq!(panel.undo_items()[0], "Insert text");
366 assert_eq!(panel.len(), 2);
367 }
368
369 #[test]
370 fn with_redo_items() {
371 let panel = HistoryPanel::new().with_redo_items(&["Paste"]);
372 assert_eq!(panel.redo_items().len(), 1);
373 assert_eq!(panel.len(), 1);
374 }
375
376 #[test]
377 fn with_both_stacks() {
378 let panel = HistoryPanel::new()
379 .with_undo_items(&["A", "B"])
380 .with_redo_items(&["C"]);
381 assert!(!panel.is_empty());
382 assert_eq!(panel.len(), 3);
383 }
384
385 #[test]
386 fn with_title() {
387 let panel = HistoryPanel::new().with_title("My History");
388 assert_eq!(panel.title, "My History");
389 }
390
391 #[test]
392 fn with_mode() {
393 let panel = HistoryPanel::new().with_mode(HistoryPanelMode::Full);
394 assert_eq!(panel.mode, HistoryPanelMode::Full);
395 }
396
397 #[test]
398 fn render_empty() {
399 let panel = HistoryPanel::new();
400 let mut pool = GraphemePool::new();
401 let mut frame = Frame::new(30, 10, &mut pool);
402 let area = Rect::new(0, 0, 30, 10);
403 panel.render(area, &mut frame); }
405
406 #[test]
407 fn render_with_items() {
408 let panel = HistoryPanel::new()
409 .with_undo_items(&["Insert text"])
410 .with_redo_items(&["Delete word"]);
411
412 let mut pool = GraphemePool::new();
413 let mut frame = Frame::new(30, 10, &mut pool);
414 let area = Rect::new(0, 0, 30, 10);
415 panel.render(area, &mut frame);
416
417 let cell = frame.buffer.get(0, 0).unwrap();
419 assert_eq!(cell.content.as_char(), Some('H')); }
421
422 #[test]
423 fn render_zero_area() {
424 let panel = HistoryPanel::new().with_undo_items(&["Test"]);
425 let mut pool = GraphemePool::new();
426 let mut frame = Frame::new(30, 10, &mut pool);
427 let area = Rect::new(0, 0, 0, 0);
428 panel.render(area, &mut frame); }
430
431 #[test]
432 fn compact_limit() {
433 let items: Vec<_> = (0..10).map(|i| format!("Item {}", i)).collect();
434 let panel = HistoryPanel::new()
435 .with_mode(HistoryPanelMode::Compact)
436 .with_compact_limit(4)
437 .with_undo_items(&items);
438
439 let mut pool = GraphemePool::new();
440 let mut frame = Frame::new(30, 20, &mut pool);
441 let area = Rect::new(0, 0, 30, 20);
442 panel.render(area, &mut frame); }
444
445 #[test]
446 fn is_not_essential() {
447 let panel = HistoryPanel::new();
448 assert!(!panel.is_essential());
449 }
450
451 #[test]
452 fn default_impl() {
453 let panel = HistoryPanel::default();
454 assert!(panel.is_empty());
455 }
456
457 #[test]
458 fn with_icons() {
459 let panel = HistoryPanel::new()
460 .with_undo_icon("<< ")
461 .with_redo_icon(">> ");
462 assert_eq!(panel.undo_icon, "<< ");
463 assert_eq!(panel.redo_icon, ">> ");
464 }
465
466 #[test]
467 fn with_marker_text() {
468 let panel = HistoryPanel::new().with_marker_text("=== NOW ===");
469 assert_eq!(panel.marker_text, "=== NOW ===");
470 }
471
472 #[test]
473 fn history_entry_new() {
474 let entry = HistoryEntry::new("Delete line", false);
475 assert_eq!(entry.description, "Delete line");
476 assert!(!entry.is_redo);
477
478 let redo = HistoryEntry::new("Paste", true);
479 assert!(redo.is_redo);
480 }
481
482 #[test]
483 fn history_panel_mode_default_is_compact() {
484 assert_eq!(HistoryPanelMode::default(), HistoryPanelMode::Compact);
485 }
486
487 #[test]
488 fn with_compact_limit_setter() {
489 let panel = HistoryPanel::new().with_compact_limit(10);
490 assert_eq!(panel.compact_limit, 10);
491 }
492
493 #[test]
494 fn history_entry_equality() {
495 let a = HistoryEntry::new("X", false);
496 let b = HistoryEntry::new("X", false);
497 let c = HistoryEntry::new("X", true);
498 assert_eq!(a, b);
499 assert_ne!(a, c);
500 }
501
502 #[test]
503 fn full_mode_renders_all_items() {
504 let items: Vec<_> = (0..10).map(|i| format!("Item {i}")).collect();
505 let panel = HistoryPanel::new()
506 .with_mode(HistoryPanelMode::Full)
507 .with_undo_items(&items);
508
509 let mut pool = GraphemePool::new();
510 let mut frame = Frame::new(30, 30, &mut pool);
511 let area = Rect::new(0, 0, 30, 30);
512 panel.render(area, &mut frame); }
514
515 #[test]
518 fn style_setters_applied() {
519 let style = Style::new().italic();
520 let panel = HistoryPanel::new()
521 .with_title_style(style)
522 .with_undo_style(style)
523 .with_redo_style(style)
524 .with_marker_style(style)
525 .with_bg_style(style);
526 assert_eq!(panel.title_style, style);
527 assert_eq!(panel.undo_style, style);
528 assert_eq!(panel.redo_style, style);
529 assert_eq!(panel.marker_style, style);
530 assert_eq!(panel.bg_style, style);
531 }
532
533 #[test]
534 fn clone_preserves_all_fields() {
535 let panel = HistoryPanel::new()
536 .with_title("T")
537 .with_undo_items(&["A"])
538 .with_redo_items(&["B"])
539 .with_mode(HistoryPanelMode::Full)
540 .with_compact_limit(3)
541 .with_marker_text("NOW")
542 .with_undo_icon("U ")
543 .with_redo_icon("R ");
544 let cloned = panel.clone();
545 assert_eq!(cloned.title, "T");
546 assert_eq!(cloned.undo_items, vec!["A"]);
547 assert_eq!(cloned.redo_items, vec!["B"]);
548 assert_eq!(cloned.mode, HistoryPanelMode::Full);
549 assert_eq!(cloned.compact_limit, 3);
550 assert_eq!(cloned.marker_text, "NOW");
551 assert_eq!(cloned.undo_icon, "U ");
552 assert_eq!(cloned.redo_icon, "R ");
553 }
554
555 #[test]
556 fn debug_format() {
557 let panel = HistoryPanel::new();
558 let dbg = format!("{:?}", panel);
559 assert!(dbg.contains("HistoryPanel"));
560 assert!(dbg.contains("History"));
561
562 let entry = HistoryEntry::new("X", true);
563 let dbg_e = format!("{:?}", entry);
564 assert!(dbg_e.contains("HistoryEntry"));
565 assert!(dbg_e.contains("is_redo: true"));
566
567 let mode = HistoryPanelMode::Compact;
568 assert!(format!("{:?}", mode).contains("Compact"));
569 }
570
571 #[test]
572 fn history_entry_clone() {
573 let a = HistoryEntry::new("Hello", false);
574 let b = a.clone();
575 assert_eq!(a, b);
576 assert_eq!(b.description, "Hello");
577 }
578
579 #[test]
580 fn history_panel_mode_copy_eq() {
581 let a = HistoryPanelMode::Full;
582 let b = a; assert_eq!(a, b);
584 assert_ne!(a, HistoryPanelMode::Compact);
585 }
586
587 #[test]
588 fn render_only_redo_no_undo() {
589 let panel = HistoryPanel::new().with_redo_items(&["Redo1", "Redo2"]);
590 let mut pool = GraphemePool::new();
591 let mut frame = Frame::new(30, 10, &mut pool);
592 let area = Rect::new(0, 0, 30, 10);
593 panel.render(area, &mut frame);
594 let cell = frame.buffer.get(0, 0).unwrap();
596 assert_eq!(cell.content.as_char(), Some('H')); }
598
599 #[test]
600 fn render_empty_title() {
601 let panel = HistoryPanel::new().with_title("").with_undo_items(&["A"]);
602 let mut pool = GraphemePool::new();
603 let mut frame = Frame::new(30, 10, &mut pool);
604 let area = Rect::new(0, 0, 30, 10);
605 panel.render(area, &mut frame);
606 let cell = frame.buffer.get(0, 0).unwrap();
608 assert_ne!(cell.content.as_char(), Some('H'));
610 }
611
612 #[test]
613 fn compact_both_stacks_overflow() {
614 let undo: Vec<_> = (0..8).map(|i| format!("U{i}")).collect();
615 let redo: Vec<_> = (0..8).map(|i| format!("R{i}")).collect();
616 let panel = HistoryPanel::new()
617 .with_mode(HistoryPanelMode::Compact)
618 .with_compact_limit(4)
619 .with_undo_items(&undo)
620 .with_redo_items(&redo);
621 let mut pool = GraphemePool::new();
622 let mut frame = Frame::new(40, 20, &mut pool);
623 let area = Rect::new(0, 0, 40, 20);
624 panel.render(area, &mut frame);
625 }
627
628 #[test]
629 fn compact_limit_zero() {
630 let panel = HistoryPanel::new()
631 .with_compact_limit(0)
632 .with_undo_items(&["A", "B"])
633 .with_redo_items(&["C"]);
634 let mut pool = GraphemePool::new();
635 let mut frame = Frame::new(30, 10, &mut pool);
636 let area = Rect::new(0, 0, 30, 10);
637 panel.render(area, &mut frame); }
639
640 #[test]
641 fn compact_limit_one_odd() {
642 let panel = HistoryPanel::new()
643 .with_compact_limit(1)
644 .with_undo_items(&["A", "B", "C"])
645 .with_redo_items(&["D", "E"]);
646 let mut pool = GraphemePool::new();
647 let mut frame = Frame::new(30, 10, &mut pool);
648 let area = Rect::new(0, 0, 30, 10);
649 panel.render(area, &mut frame);
651 }
652
653 #[test]
654 fn render_width_one() {
655 let panel = HistoryPanel::new()
656 .with_undo_items(&["LongItem"])
657 .with_redo_items(&["AnotherLong"]);
658 let mut pool = GraphemePool::new();
659 let mut frame = Frame::new(30, 10, &mut pool);
660 let area = Rect::new(0, 0, 1, 10);
661 panel.render(area, &mut frame); }
663
664 #[test]
665 fn render_height_one() {
666 let panel = HistoryPanel::new()
667 .with_undo_items(&["A"])
668 .with_redo_items(&["B"]);
669 let mut pool = GraphemePool::new();
670 let mut frame = Frame::new(30, 10, &mut pool);
671 let area = Rect::new(0, 0, 30, 1);
672 panel.render(area, &mut frame); }
674
675 #[test]
676 fn render_height_three_no_room_for_redo() {
677 let panel = HistoryPanel::new()
678 .with_undo_items(&["A"])
679 .with_redo_items(&["B"]);
680 let mut pool = GraphemePool::new();
681 let mut frame = Frame::new(30, 10, &mut pool);
682 let area = Rect::new(0, 0, 30, 3);
684 panel.render(area, &mut frame);
685 }
686
687 #[test]
688 fn bg_style_fills_area() {
689 use ftui_render::cell::PackedRgba;
690 let red = PackedRgba::rgb(255, 0, 0);
691 let panel = HistoryPanel::new().with_bg_style(Style::new().bg(red));
692 let mut pool = GraphemePool::new();
693 let mut frame = Frame::new(10, 5, &mut pool);
694 let area = Rect::new(0, 0, 10, 5);
695 panel.render(area, &mut frame);
696 for y in 0..5u16 {
698 for x in 0..10u16 {
699 let cell = frame.buffer.get(x, y).unwrap();
700 assert_eq!(cell.bg, red);
701 }
702 }
703 }
704
705 #[test]
706 fn bg_style_none_does_not_fill() {
707 use ftui_render::cell::PackedRgba;
708 let panel = HistoryPanel::new();
709 let mut pool = GraphemePool::new();
710 let mut frame = Frame::new(10, 5, &mut pool);
711 let area = Rect::new(0, 0, 10, 5);
712 panel.render(area, &mut frame);
713 let cell = frame.buffer.get(5, 3).unwrap();
715 assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
716 }
717
718 #[test]
719 fn marker_centering_even_width() {
720 let panel = HistoryPanel::new()
721 .with_title("")
722 .with_marker_text("XX")
723 .with_undo_items(&["A"]);
724 let mut pool = GraphemePool::new();
725 let mut frame = Frame::new(20, 10, &mut pool);
726 let area = Rect::new(0, 0, 20, 10);
727 panel.render(area, &mut frame);
728 let cell_before = frame.buffer.get(8, 1).unwrap();
731 assert_ne!(cell_before.content.as_char(), Some('X'));
732 let cell_start = frame.buffer.get(9, 1).unwrap();
733 assert_eq!(cell_start.content.as_char(), Some('X'));
734 }
735
736 #[test]
737 fn marker_wider_than_area() {
738 let panel = HistoryPanel::new()
739 .with_title("")
740 .with_marker_text("VERY LONG MARKER TEXT THAT EXCEEDS");
741 let mut pool = GraphemePool::new();
742 let mut frame = Frame::new(10, 5, &mut pool);
743 let area = Rect::new(0, 0, 10, 5);
744 panel.render(area, &mut frame);
746 }
747
748 #[test]
749 fn overwrite_items_replaces() {
750 let panel = HistoryPanel::new()
751 .with_undo_items(&["Old1", "Old2"])
752 .with_undo_items(&["New1"]);
753 assert_eq!(panel.undo_items().len(), 1);
754 assert_eq!(panel.undo_items()[0], "New1");
755 }
756
757 #[test]
758 fn render_at_offset_area() {
759 let panel = HistoryPanel::new()
760 .with_undo_items(&["A"])
761 .with_redo_items(&["B"]);
762 let mut pool = GraphemePool::new();
763 let mut frame = Frame::new(30, 20, &mut pool);
764 let area = Rect::new(5, 5, 20, 10);
765 panel.render(area, &mut frame);
766 let cell = frame.buffer.get(5, 5).unwrap();
768 assert_eq!(cell.content.as_char(), Some('H'));
769 let origin = frame.buffer.get(0, 0).unwrap();
771 assert_ne!(origin.content.as_char(), Some('H'));
772 }
773
774 #[test]
775 fn empty_undo_icon_and_redo_icon() {
776 let panel = HistoryPanel::new()
777 .with_undo_icon("")
778 .with_redo_icon("")
779 .with_undo_items(&["A"])
780 .with_redo_items(&["B"]);
781 let mut pool = GraphemePool::new();
782 let mut frame = Frame::new(30, 10, &mut pool);
783 let area = Rect::new(0, 0, 30, 10);
784 panel.render(area, &mut frame);
785 }
786
787 #[test]
788 fn full_mode_no_ellipsis() {
789 let undo: Vec<_> = (0..10).map(|i| format!("U{i}")).collect();
790 let redo: Vec<_> = (0..10).map(|i| format!("R{i}")).collect();
791 let panel = HistoryPanel::new()
792 .with_mode(HistoryPanelMode::Full)
793 .with_undo_items(&undo)
794 .with_redo_items(&redo);
795 let mut pool = GraphemePool::new();
796 let mut frame = Frame::new(30, 30, &mut pool);
797 let area = Rect::new(0, 0, 30, 30);
798 panel.render(area, &mut frame);
799 }
801
802 #[test]
803 fn compact_undo_only_with_overflow() {
804 let items: Vec<_> = (0..10).map(|i| format!("Item{i}")).collect();
805 let panel = HistoryPanel::new()
806 .with_mode(HistoryPanelMode::Compact)
807 .with_compact_limit(4)
808 .with_undo_items(&items);
809 let mut pool = GraphemePool::new();
811 let mut frame = Frame::new(30, 15, &mut pool);
812 let area = Rect::new(0, 0, 30, 15);
813 panel.render(area, &mut frame);
814 }
815
816 #[test]
817 fn compact_redo_only_with_overflow() {
818 let items: Vec<_> = (0..10).map(|i| format!("Item{i}")).collect();
819 let panel = HistoryPanel::new()
820 .with_mode(HistoryPanelMode::Compact)
821 .with_compact_limit(4)
822 .with_redo_items(&items);
823 let mut pool = GraphemePool::new();
825 let mut frame = Frame::new(30, 15, &mut pool);
826 let area = Rect::new(0, 0, 30, 15);
827 panel.render(area, &mut frame);
828 }
829
830 #[test]
831 fn history_entry_from_string_type() {
832 let entry = HistoryEntry::new(String::from("Owned"), false);
833 assert_eq!(entry.description, "Owned");
834 }
835}