#![forbid(unsafe_code)]
use crate::{Widget, clear_text_area, draw_text_span};
use ftui_core::geometry::Rect;
use ftui_render::frame::Frame;
use ftui_style::Style;
use ftui_text::wrap::display_width;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HistoryEntry {
pub description: String,
pub is_redo: bool,
}
impl HistoryEntry {
#[must_use]
pub fn new(description: impl Into<String>, is_redo: bool) -> Self {
Self {
description: description.into(),
is_redo,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HistoryPanelMode {
#[default]
Compact,
Full,
}
#[derive(Debug, Clone)]
pub struct HistoryPanel {
title: String,
undo_items: Vec<String>,
redo_items: Vec<String>,
mode: HistoryPanelMode,
compact_limit: usize,
title_style: Style,
undo_style: Style,
redo_style: Style,
marker_style: Style,
bg_style: Style,
marker_text: String,
undo_icon: String,
redo_icon: String,
}
impl Default for HistoryPanel {
fn default() -> Self {
Self::new()
}
}
impl HistoryPanel {
#[must_use]
pub fn new() -> Self {
Self {
title: "History".to_string(),
undo_items: Vec::new(),
redo_items: Vec::new(),
mode: HistoryPanelMode::Compact,
compact_limit: 5,
title_style: Style::new().bold(),
undo_style: Style::default(),
redo_style: Style::new().dim(),
marker_style: Style::new().bold(),
bg_style: Style::default(),
marker_text: "─── current ───".to_string(),
undo_icon: "↶ ".to_string(),
redo_icon: "↷ ".to_string(),
}
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
#[must_use]
pub fn with_undo_items(mut self, items: &[impl AsRef<str>]) -> Self {
self.undo_items = items.iter().map(|s| s.as_ref().to_string()).collect();
self
}
#[must_use]
pub fn with_redo_items(mut self, items: &[impl AsRef<str>]) -> Self {
self.redo_items = items.iter().map(|s| s.as_ref().to_string()).collect();
self
}
#[must_use]
pub fn with_mode(mut self, mode: HistoryPanelMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub fn with_compact_limit(mut self, limit: usize) -> Self {
self.compact_limit = limit;
self
}
#[must_use]
pub fn with_title_style(mut self, style: Style) -> Self {
self.title_style = style;
self
}
#[must_use]
pub fn with_undo_style(mut self, style: Style) -> Self {
self.undo_style = style;
self
}
#[must_use]
pub fn with_redo_style(mut self, style: Style) -> Self {
self.redo_style = style;
self
}
#[must_use]
pub fn with_marker_style(mut self, style: Style) -> Self {
self.marker_style = style;
self
}
#[must_use]
pub fn with_bg_style(mut self, style: Style) -> Self {
self.bg_style = style;
self
}
#[must_use]
pub fn with_marker_text(mut self, text: impl Into<String>) -> Self {
self.marker_text = text.into();
self
}
#[must_use]
pub fn with_undo_icon(mut self, icon: impl Into<String>) -> Self {
self.undo_icon = icon.into();
self
}
#[must_use]
pub fn with_redo_icon(mut self, icon: impl Into<String>) -> Self {
self.redo_icon = icon.into();
self
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.undo_items.is_empty() && self.redo_items.is_empty()
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.undo_items.len() + self.redo_items.len()
}
#[must_use]
pub fn undo_items(&self) -> &[String] {
&self.undo_items
}
#[must_use]
pub fn redo_items(&self) -> &[String] {
&self.redo_items
}
fn render_content(&self, area: Rect, frame: &mut Frame) {
if area.width == 0 || area.height == 0 {
return;
}
let deg = frame.buffer.degradation;
if !deg.render_content() {
return;
}
let title_style = if deg.apply_styling() {
self.title_style
} else {
Style::default()
};
let undo_style = if deg.apply_styling() {
self.undo_style
} else {
Style::default()
};
let redo_style = if deg.apply_styling() {
self.redo_style
} else {
Style::default()
};
let marker_style = if deg.apply_styling() {
self.marker_style
} else {
Style::default()
};
let max_x = area.right();
let mut row: u16 = 0;
if row < area.height && !self.title.is_empty() {
let y = area.y.saturating_add(row);
draw_text_span(frame, area.x, y, &self.title, title_style, max_x);
row += 1;
if row < area.height {
row += 1;
}
}
let (undo_to_show, redo_to_show) = match self.mode {
HistoryPanelMode::Compact => {
let half_limit = self.compact_limit / 2;
let undo_start = self.undo_items.len().saturating_sub(half_limit);
let redo_end = half_limit.min(self.redo_items.len());
(&self.undo_items[undo_start..], &self.redo_items[..redo_end])
}
HistoryPanelMode::Full => (&self.undo_items[..], &self.redo_items[..]),
};
if self.mode == HistoryPanelMode::Compact
&& undo_to_show.len() < self.undo_items.len()
&& row < area.height
{
let y = area.y.saturating_add(row);
let hidden = self.undo_items.len() - undo_to_show.len();
let text = format!("... ({} more)", hidden);
draw_text_span(frame, area.x, y, &text, redo_style, max_x);
row += 1;
}
for desc in undo_to_show {
if row >= area.height {
break;
}
let y = area.y.saturating_add(row);
let icon_end = draw_text_span(frame, area.x, y, &self.undo_icon, undo_style, max_x);
draw_text_span(frame, icon_end, y, desc, undo_style, max_x);
row += 1;
}
if row < area.height {
let y = area.y.saturating_add(row);
let marker_width = display_width(&self.marker_text);
let available = area.width as usize;
let pad_left = available.saturating_sub(marker_width) / 2;
let x = area.x.saturating_add(pad_left as u16);
draw_text_span(frame, x, y, &self.marker_text, marker_style, max_x);
row += 1;
}
for desc in redo_to_show {
if row >= area.height {
break;
}
let y = area.y.saturating_add(row);
let icon_end = draw_text_span(frame, area.x, y, &self.redo_icon, redo_style, max_x);
draw_text_span(frame, icon_end, y, desc, redo_style, max_x);
row += 1;
}
if self.mode == HistoryPanelMode::Compact
&& redo_to_show.len() < self.redo_items.len()
&& row < area.height
{
let y = area.y.saturating_add(row);
let hidden = self.redo_items.len() - redo_to_show.len();
let text = format!("... ({} more)", hidden);
draw_text_span(frame, area.x, y, &text, redo_style, max_x);
}
}
}
impl Widget for HistoryPanel {
fn render(&self, area: Rect, frame: &mut Frame) {
let deg = frame.buffer.degradation;
if !deg.render_content() {
clear_text_area(frame, area, Style::default());
return;
}
let mut bg_cell = ftui_render::cell::Cell::from_char(' ');
crate::apply_style(
&mut bg_cell,
if deg.apply_styling() {
self.bg_style
} else {
Style::default()
},
);
frame.buffer.fill(area, bg_cell);
self.render_content(area, frame);
}
fn is_essential(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use ftui_render::budget::DegradationLevel;
use ftui_render::frame::Frame;
use ftui_render::grapheme_pool::GraphemePool;
#[test]
fn new_panel_is_empty() {
let panel = HistoryPanel::new();
assert!(panel.is_empty());
assert_eq!(panel.len(), 0);
}
#[test]
fn with_undo_items() {
let panel = HistoryPanel::new().with_undo_items(&["Insert text", "Delete word"]);
assert_eq!(panel.undo_items().len(), 2);
assert_eq!(panel.undo_items()[0], "Insert text");
assert_eq!(panel.len(), 2);
}
#[test]
fn with_redo_items() {
let panel = HistoryPanel::new().with_redo_items(&["Paste"]);
assert_eq!(panel.redo_items().len(), 1);
assert_eq!(panel.len(), 1);
}
#[test]
fn with_both_stacks() {
let panel = HistoryPanel::new()
.with_undo_items(&["A", "B"])
.with_redo_items(&["C"]);
assert!(!panel.is_empty());
assert_eq!(panel.len(), 3);
}
#[test]
fn with_title() {
let panel = HistoryPanel::new().with_title("My History");
assert_eq!(panel.title, "My History");
}
#[test]
fn with_mode() {
let panel = HistoryPanel::new().with_mode(HistoryPanelMode::Full);
assert_eq!(panel.mode, HistoryPanelMode::Full);
}
#[test]
fn render_empty() {
let panel = HistoryPanel::new();
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
let area = Rect::new(0, 0, 30, 10);
panel.render(area, &mut frame); }
#[test]
fn render_with_items() {
let panel = HistoryPanel::new()
.with_undo_items(&["Insert text"])
.with_redo_items(&["Delete word"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
let area = Rect::new(0, 0, 30, 10);
panel.render(area, &mut frame);
let cell = frame.buffer.get(0, 0).unwrap();
assert_eq!(cell.content.as_char(), Some('H')); }
#[test]
fn render_zero_area() {
let panel = HistoryPanel::new().with_undo_items(&["Test"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
let area = Rect::new(0, 0, 0, 0);
panel.render(area, &mut frame); }
#[test]
fn compact_limit() {
let items: Vec<_> = (0..10).map(|i| format!("Item {}", i)).collect();
let panel = HistoryPanel::new()
.with_mode(HistoryPanelMode::Compact)
.with_compact_limit(4)
.with_undo_items(&items);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 20, &mut pool);
let area = Rect::new(0, 0, 30, 20);
panel.render(area, &mut frame); }
#[test]
fn is_not_essential() {
let panel = HistoryPanel::new();
assert!(!panel.is_essential());
}
#[test]
fn render_no_styling_drops_configured_styles() {
let fg = ftui_render::cell::PackedRgba::rgb(255, 0, 0);
let bg = ftui_render::cell::PackedRgba::rgb(0, 40, 80);
let panel = HistoryPanel::new()
.with_undo_items(&["Insert text"])
.with_title_style(Style::new().fg(fg).bold())
.with_undo_style(Style::new().fg(fg))
.with_marker_style(Style::new().fg(fg).italic())
.with_bg_style(Style::new().bg(bg));
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
frame.buffer.degradation = DegradationLevel::NoStyling;
let area = Rect::new(0, 0, 30, 10);
panel.render(area, &mut frame);
let title_cell = frame.buffer.get(0, 0).unwrap();
let background_cell = frame.buffer.get(15, 5).unwrap();
let default_text = ftui_render::cell::Cell::from_char('H');
let default_bg = ftui_render::cell::Cell::from_char(' ');
assert_eq!(title_cell.content.as_char(), Some('H'));
assert_eq!(title_cell.fg, default_text.fg);
assert_eq!(title_cell.bg, default_text.bg);
assert_eq!(title_cell.attrs, default_text.attrs);
assert_eq!(background_cell.fg, default_bg.fg);
assert_eq!(background_cell.bg, default_bg.bg);
assert_eq!(background_cell.attrs, default_bg.attrs);
}
#[test]
fn render_skeleton_is_noop() {
let panel = HistoryPanel::new().with_undo_items(&["Insert text"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
let area = Rect::new(0, 0, 30, 10);
panel.render(area, &mut frame);
frame.buffer.degradation = DegradationLevel::Skeleton;
panel.render(area, &mut frame);
let cell = frame.buffer.get(0, 0).unwrap();
assert_eq!(cell.content.as_char(), Some(' '));
let default_cell = ftui_render::cell::Cell::from_char(' ');
assert_eq!(cell.fg, default_cell.fg);
assert_eq!(cell.bg, default_cell.bg);
assert_eq!(cell.attrs, default_cell.attrs);
}
#[test]
fn render_shorter_history_clears_stale_rows() {
let long = HistoryPanel::new()
.with_undo_items(&["Insert text", "Delete line", "Paste block"])
.with_redo_items(&["Redo thing"]);
let short = HistoryPanel::new().with_undo_items(&["Insert"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
let area = Rect::new(0, 0, 30, 10);
long.render(area, &mut frame);
short.render(area, &mut frame);
for y in 6..10u16 {
for x in 0..30u16 {
assert_eq!(frame.buffer.get(x, y).unwrap().content.as_char(), Some(' '));
}
}
}
#[test]
fn default_impl() {
let panel = HistoryPanel::default();
assert!(panel.is_empty());
}
#[test]
fn with_icons() {
let panel = HistoryPanel::new()
.with_undo_icon("<< ")
.with_redo_icon(">> ");
assert_eq!(panel.undo_icon, "<< ");
assert_eq!(panel.redo_icon, ">> ");
}
#[test]
fn with_marker_text() {
let panel = HistoryPanel::new().with_marker_text("=== NOW ===");
assert_eq!(panel.marker_text, "=== NOW ===");
}
#[test]
fn history_entry_new() {
let entry = HistoryEntry::new("Delete line", false);
assert_eq!(entry.description, "Delete line");
assert!(!entry.is_redo);
let redo = HistoryEntry::new("Paste", true);
assert!(redo.is_redo);
}
#[test]
fn history_panel_mode_default_is_compact() {
assert_eq!(HistoryPanelMode::default(), HistoryPanelMode::Compact);
}
#[test]
fn with_compact_limit_setter() {
let panel = HistoryPanel::new().with_compact_limit(10);
assert_eq!(panel.compact_limit, 10);
}
#[test]
fn history_entry_equality() {
let a = HistoryEntry::new("X", false);
let b = HistoryEntry::new("X", false);
let c = HistoryEntry::new("X", true);
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn full_mode_renders_all_items() {
let items: Vec<_> = (0..10).map(|i| format!("Item {i}")).collect();
let panel = HistoryPanel::new()
.with_mode(HistoryPanelMode::Full)
.with_undo_items(&items);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 30, &mut pool);
let area = Rect::new(0, 0, 30, 30);
panel.render(area, &mut frame); }
#[test]
fn style_setters_applied() {
let style = Style::new().italic();
let panel = HistoryPanel::new()
.with_title_style(style)
.with_undo_style(style)
.with_redo_style(style)
.with_marker_style(style)
.with_bg_style(style);
assert_eq!(panel.title_style, style);
assert_eq!(panel.undo_style, style);
assert_eq!(panel.redo_style, style);
assert_eq!(panel.marker_style, style);
assert_eq!(panel.bg_style, style);
}
#[test]
fn clone_preserves_all_fields() {
let panel = HistoryPanel::new()
.with_title("T")
.with_undo_items(&["A"])
.with_redo_items(&["B"])
.with_mode(HistoryPanelMode::Full)
.with_compact_limit(3)
.with_marker_text("NOW")
.with_undo_icon("U ")
.with_redo_icon("R ");
let cloned = panel.clone();
assert_eq!(cloned.title, "T");
assert_eq!(cloned.undo_items, vec!["A"]);
assert_eq!(cloned.redo_items, vec!["B"]);
assert_eq!(cloned.mode, HistoryPanelMode::Full);
assert_eq!(cloned.compact_limit, 3);
assert_eq!(cloned.marker_text, "NOW");
assert_eq!(cloned.undo_icon, "U ");
assert_eq!(cloned.redo_icon, "R ");
}
#[test]
fn debug_format() {
let panel = HistoryPanel::new();
let dbg = format!("{:?}", panel);
assert!(dbg.contains("HistoryPanel"));
assert!(dbg.contains("History"));
let entry = HistoryEntry::new("X", true);
let dbg_e = format!("{:?}", entry);
assert!(dbg_e.contains("HistoryEntry"));
assert!(dbg_e.contains("is_redo: true"));
let mode = HistoryPanelMode::Compact;
assert!(format!("{:?}", mode).contains("Compact"));
}
#[test]
fn history_entry_clone() {
let a = HistoryEntry::new("Hello", false);
let b = a.clone();
assert_eq!(a, b);
assert_eq!(b.description, "Hello");
}
#[test]
fn history_panel_mode_copy_eq() {
let a = HistoryPanelMode::Full;
let b = a; assert_eq!(a, b);
assert_ne!(a, HistoryPanelMode::Compact);
}
#[test]
fn render_only_redo_no_undo() {
let panel = HistoryPanel::new().with_redo_items(&["Redo1", "Redo2"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
let area = Rect::new(0, 0, 30, 10);
panel.render(area, &mut frame);
let cell = frame.buffer.get(0, 0).unwrap();
assert_eq!(cell.content.as_char(), Some('H')); }
#[test]
fn render_empty_title() {
let panel = HistoryPanel::new().with_title("").with_undo_items(&["A"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
let area = Rect::new(0, 0, 30, 10);
panel.render(area, &mut frame);
let cell = frame.buffer.get(0, 0).unwrap();
assert_ne!(cell.content.as_char(), Some('H'));
}
#[test]
fn compact_both_stacks_overflow() {
let undo: Vec<_> = (0..8).map(|i| format!("U{i}")).collect();
let redo: Vec<_> = (0..8).map(|i| format!("R{i}")).collect();
let panel = HistoryPanel::new()
.with_mode(HistoryPanelMode::Compact)
.with_compact_limit(4)
.with_undo_items(&undo)
.with_redo_items(&redo);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(40, 20, &mut pool);
let area = Rect::new(0, 0, 40, 20);
panel.render(area, &mut frame);
}
#[test]
fn compact_limit_zero() {
let panel = HistoryPanel::new()
.with_compact_limit(0)
.with_undo_items(&["A", "B"])
.with_redo_items(&["C"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
let area = Rect::new(0, 0, 30, 10);
panel.render(area, &mut frame); }
#[test]
fn compact_limit_one_odd() {
let panel = HistoryPanel::new()
.with_compact_limit(1)
.with_undo_items(&["A", "B", "C"])
.with_redo_items(&["D", "E"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
let area = Rect::new(0, 0, 30, 10);
panel.render(area, &mut frame);
}
#[test]
fn render_width_one() {
let panel = HistoryPanel::new()
.with_undo_items(&["LongItem"])
.with_redo_items(&["AnotherLong"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
let area = Rect::new(0, 0, 1, 10);
panel.render(area, &mut frame); }
#[test]
fn render_height_one() {
let panel = HistoryPanel::new()
.with_undo_items(&["A"])
.with_redo_items(&["B"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
let area = Rect::new(0, 0, 30, 1);
panel.render(area, &mut frame); }
#[test]
fn render_height_three_no_room_for_redo() {
let panel = HistoryPanel::new()
.with_undo_items(&["A"])
.with_redo_items(&["B"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
let area = Rect::new(0, 0, 30, 3);
panel.render(area, &mut frame);
}
#[test]
fn bg_style_fills_area() {
use ftui_render::cell::PackedRgba;
let red = PackedRgba::rgb(255, 0, 0);
let panel = HistoryPanel::new().with_bg_style(Style::new().bg(red));
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 5, &mut pool);
let area = Rect::new(0, 0, 10, 5);
panel.render(area, &mut frame);
for y in 0..5u16 {
for x in 0..10u16 {
let cell = frame.buffer.get(x, y).unwrap();
assert_eq!(cell.bg, red);
}
}
}
#[test]
fn bg_style_none_does_not_fill() {
use ftui_render::cell::PackedRgba;
let panel = HistoryPanel::new();
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 5, &mut pool);
let area = Rect::new(0, 0, 10, 5);
panel.render(area, &mut frame);
let cell = frame.buffer.get(5, 3).unwrap();
assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
}
#[test]
fn marker_centering_even_width() {
let panel = HistoryPanel::new()
.with_title("")
.with_marker_text("XX")
.with_undo_items(&["A"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(20, 10, &mut pool);
let area = Rect::new(0, 0, 20, 10);
panel.render(area, &mut frame);
let cell_before = frame.buffer.get(8, 1).unwrap();
assert_ne!(cell_before.content.as_char(), Some('X'));
let cell_start = frame.buffer.get(9, 1).unwrap();
assert_eq!(cell_start.content.as_char(), Some('X'));
}
#[test]
fn marker_wider_than_area() {
let panel = HistoryPanel::new()
.with_title("")
.with_marker_text("VERY LONG MARKER TEXT THAT EXCEEDS");
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 5, &mut pool);
let area = Rect::new(0, 0, 10, 5);
panel.render(area, &mut frame);
}
#[test]
fn overwrite_items_replaces() {
let panel = HistoryPanel::new()
.with_undo_items(&["Old1", "Old2"])
.with_undo_items(&["New1"]);
assert_eq!(panel.undo_items().len(), 1);
assert_eq!(panel.undo_items()[0], "New1");
}
#[test]
fn render_at_offset_area() {
let panel = HistoryPanel::new()
.with_undo_items(&["A"])
.with_redo_items(&["B"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 20, &mut pool);
let area = Rect::new(5, 5, 20, 10);
panel.render(area, &mut frame);
let cell = frame.buffer.get(5, 5).unwrap();
assert_eq!(cell.content.as_char(), Some('H'));
let origin = frame.buffer.get(0, 0).unwrap();
assert_ne!(origin.content.as_char(), Some('H'));
}
#[test]
fn empty_undo_icon_and_redo_icon() {
let panel = HistoryPanel::new()
.with_undo_icon("")
.with_redo_icon("")
.with_undo_items(&["A"])
.with_redo_items(&["B"]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 10, &mut pool);
let area = Rect::new(0, 0, 30, 10);
panel.render(area, &mut frame);
}
#[test]
fn full_mode_no_ellipsis() {
let undo: Vec<_> = (0..10).map(|i| format!("U{i}")).collect();
let redo: Vec<_> = (0..10).map(|i| format!("R{i}")).collect();
let panel = HistoryPanel::new()
.with_mode(HistoryPanelMode::Full)
.with_undo_items(&undo)
.with_redo_items(&redo);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 30, &mut pool);
let area = Rect::new(0, 0, 30, 30);
panel.render(area, &mut frame);
}
#[test]
fn compact_undo_only_with_overflow() {
let items: Vec<_> = (0..10).map(|i| format!("Item{i}")).collect();
let panel = HistoryPanel::new()
.with_mode(HistoryPanelMode::Compact)
.with_compact_limit(4)
.with_undo_items(&items);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 15, &mut pool);
let area = Rect::new(0, 0, 30, 15);
panel.render(area, &mut frame);
}
#[test]
fn compact_redo_only_with_overflow() {
let items: Vec<_> = (0..10).map(|i| format!("Item{i}")).collect();
let panel = HistoryPanel::new()
.with_mode(HistoryPanelMode::Compact)
.with_compact_limit(4)
.with_redo_items(&items);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(30, 15, &mut pool);
let area = Rect::new(0, 0, 30, 15);
panel.render(area, &mut frame);
}
#[test]
fn history_entry_from_string_type() {
let entry = HistoryEntry::new(String::from("Owned"), false);
assert_eq!(entry.description, "Owned");
}
}