diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6d46ec0..54ff24a 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -11,4 +11,4 @@ jobs: checks: write uses: joshka/github-workflows/.github/workflows/rust-check.yml@main with: - msrv: 1.74.0 + msrv: 1.80.0 diff --git a/Cargo.toml b/Cargo.toml index 98dc99d..387dbb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,14 @@ [workspace] resolver = "2" members = ["tui-*"] +default-members = ["tui-*"] [workspace.package] authors = ["Joshka"] license = "MIT OR Apache-2.0" repository = "https://github.com/joshka/tui-widgets" edition = "2021" -rust-version = "1.74.0" +rust-version = "1.80.0" categories = ["command-line-interface", "gui"] keywords = ["cli", "console", "ratatui", "terminal", "tui"] @@ -23,6 +24,7 @@ futures = "0.3.31" itertools = "0.13.0" indoc = "2.0.5" lipsum = "0.9.1" +pretty_assertions = "1.4.1" ratatui = { version = "0.30.0-alpha.0", default-features = false } ratatui-core = { version = "0.1.0-alpha.0" } ratatui-macros = "0.7.0-alpha.0" diff --git a/bacon.toml b/bacon.toml index a307b66..9a616d5 100644 --- a/bacon.toml +++ b/bacon.toml @@ -8,43 +8,23 @@ default_job = "check" [jobs.check] -command = ["cargo", "check", "--workspace", "--color", "always"] +command = ["cargo", "check", "--workspace"] need_stdout = false [jobs.check-all] -command = [ - "cargo", - "check", - "--workspace", - "--all-targets", - "--color", - "always", -] +command = ["cargo", "check", "--workspace", "--all-targets", "--all-features"] need_stdout = false [jobs.clippy] -command = [ - "cargo", - "clippy", - "--workspace", - "--all-features", - "--all-targets", - "--color", - "always", -] +command = ["cargo", "clippy", "--workspace", "--all-features", "--all-targets"] need_stdout = false [jobs.test] -command = [ - "cargo", - "test", - "--workspace", - "--color", - "always", - "--", - "--color", - "always", # see https://github.com/Canop/bacon/issues/124 -] +command = ["cargo", "test", "--workspace"] +need_stdout = true + +[jobs.test-unit] +command = ["cargo", "test", "--workspace", "--lib"] need_stdout = true [jobs.doc] @@ -55,8 +35,6 @@ command = [ "-Zunstable-options", "-Zrustdoc-scrape-examples", "--all-features", - "--color", - "always", "--no-deps", ] need_stdout = false @@ -71,8 +49,6 @@ command = [ "-Zunstable-options", "-Zrustdoc-scrape-examples", "--all-features", - "--color", - "always", "--no-deps", "--open", ] @@ -84,27 +60,12 @@ on_success = "job:doc" # so that we don't open the browser at each change # way. Don't forget the `--color always` part or the errors won't be # properly parsed. [jobs.run] -command = [ - "cargo", - "run", - "--color", - "always", - # put launch parameters for your program behind a `--` separator -] +command = ["cargo", "run"] need_stdout = true allow_warnings = true [jobs.coverage] -command = [ - "cargo", - "llvm-cov", - "--workspace", - "--lcov", - "--output-path", - "target/lcov.info", - "--color", - "always", -] +command = ["cargo", "llvm-cov", "--workspace", "--lcov", "--output-path", "target/lcov.info"] [jobs.format] command = ["cargo", "+nightly", "fmt", "--", "--check"] @@ -128,3 +89,4 @@ shift-r = "job:rdme" f = "job:format" o = "job:coverage" v = "job:vhs" +u = "job:test-unit" diff --git a/tui-popup/Cargo.toml b/tui-popup/Cargo.toml index 1580091..7c9fe38 100644 --- a/tui-popup/Cargo.toml +++ b/tui-popup/Cargo.toml @@ -31,6 +31,7 @@ derive_setters.workspace = true ratatui = { workspace = true, features = ["unstable-widget-ref"] } [dev-dependencies] +pretty_assertions.workspace = true color-eyre.workspace = true lipsum.workspace = true diff --git a/tui-popup/examples/paragraph.rs b/tui-popup/examples/paragraph.rs index e473762..2e5cb9f 100644 --- a/tui-popup/examples/paragraph.rs +++ b/tui-popup/examples/paragraph.rs @@ -6,39 +6,57 @@ use ratatui::{ widgets::{Paragraph, Wrap}, Frame, }; -use tui_popup::{Popup, SizedWrapper}; - -mod terminal; +use tui_popup::{KnownSizeWrapper, Popup}; fn main() -> Result<()> { - let mut terminal = terminal::init()?; - let mut app = App::default(); - while !app.should_exit { - terminal.draw(|frame| app.render(frame))?; - app.handle_events()?; - } - terminal::restore()?; - Ok(()) + color_eyre::install()?; + let terminal = ratatui::init(); + let result = App::default().run(terminal); + ratatui::restore(); + result } #[derive(Default)] struct App { should_exit: bool, + lorem_ipsum: String, scroll: u16, } impl App { + fn run(&mut self, mut terminal: ratatui::DefaultTerminal) -> Result<()> { + self.lorem_ipsum = lipsum(2000); + while !self.should_exit { + terminal.draw(|frame| self.render(frame))?; + self.handle_events()?; + } + Ok(()) + } + fn render(&self, frame: &mut Frame) { let area = frame.area(); - let background = background(area); + self.render_background(frame, area); + self.render_popup(frame); + } - let paragraph = paragraph(self.scroll); - let popup = Popup::new(paragraph) + fn render_background(&self, frame: &mut Frame, area: Rect) { + let text = Text::raw(&self.lorem_ipsum); + let paragraph = Paragraph::new(text).wrap(Wrap { trim: false }).dark_gray(); + frame.render_widget(paragraph, area); + } + + fn render_popup(&self, frame: &mut Frame) { + let lines: Text = (0..10).map(|i| Span::raw(format!("Line {i}"))).collect(); + let paragraph = Paragraph::new(lines).scroll((self.scroll, 0)); + let wrapper = KnownSizeWrapper { + inner: ¶graph, + width: 21, + height: 5, + }; + let popup = Popup::new(wrapper) .title("scroll: ↑/↓ quit: Esc") .style(Style::new().white().on_blue()); - - frame.render_widget(background, area); - frame.render_widget(&popup, area); + frame.render_widget(popup, frame.area()); } fn handle_events(&mut self) -> Result<()> { @@ -61,20 +79,3 @@ impl App { self.scroll = self.scroll.saturating_add(1); } } - -fn paragraph(scroll: u16) -> SizedWrapper> { - let lines: Text = (0..10).map(|i| Span::raw(format!("Line {i}"))).collect(); - let paragraph = Paragraph::new(lines).scroll((scroll, 0)); - SizedWrapper { - inner: paragraph, - width: 21, - height: 5, - } -} - -fn background(area: Rect) -> Paragraph<'static> { - let lorem_ipsum = lipsum(area.area() as usize / 5); - Paragraph::new(lorem_ipsum) - .wrap(Wrap { trim: false }) - .dark_gray() -} diff --git a/tui-popup/examples/popup.rs b/tui-popup/examples/popup.rs index 83ebc63..a02ab48 100644 --- a/tui-popup/examples/popup.rs +++ b/tui-popup/examples/popup.rs @@ -8,18 +8,23 @@ use ratatui::{ }; use tui_popup::Popup; -mod terminal; - fn main() -> Result<()> { - let mut terminal = terminal::init()?; + color_eyre::install()?; + let mut terminal = ratatui::init(); + let result = run(&mut terminal); + ratatui::restore(); + result +} + +fn run(terminal: &mut ratatui::DefaultTerminal) -> Result<()> { loop { - terminal.draw(render)?; - if read_any_key()? { - break; + terminal.draw(|frame| { + render(frame); + })?; + if matches!(event::read()?, Event::Key(_)) { + break Ok(()); } } - terminal::restore()?; - Ok(()) } fn render(frame: &mut Frame) { @@ -32,11 +37,6 @@ fn render(frame: &mut Frame) { frame.render_widget(&popup, area); } -fn read_any_key() -> Result { - let event = event::read()?; - Ok(matches!(event, Event::Key(_))) -} - fn background(area: Rect) -> Paragraph<'static> { let lorem_ipsum = lipsum(area.area() as usize / 5); Paragraph::new(lorem_ipsum) diff --git a/tui-popup/examples/state.rs b/tui-popup/examples/state.rs index 242d07b..576929e 100644 --- a/tui-popup/examples/state.rs +++ b/tui-popup/examples/state.rs @@ -4,95 +4,90 @@ use ratatui::{ crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}, prelude::{Constraint, Frame, Layout, Rect, Style, Stylize, Text}, widgets::{Paragraph, Wrap}, + DefaultTerminal, }; use tui_popup::{Popup, PopupState}; -mod terminal; - fn main() -> Result<()> { - let mut terminal = terminal::init()?; - let mut app = App::default(); - while !app.should_exit { - terminal.draw(|frame| app.render(frame))?; - app.handle_events()?; - } - terminal::restore()?; - Ok(()) + color_eyre::install()?; + let terminal = ratatui::init(); + let result = run(terminal); + ratatui::restore(); + result } -#[derive(Default)] -struct App { - popup: PopupState, - should_exit: bool, +fn run(mut terminal: DefaultTerminal) -> Result<()> { + let mut state = PopupState::default(); + let mut exit = false; + while !exit { + terminal.draw(|frame| draw(frame, &mut state))?; + handle_events(&mut state, &mut exit)?; + } + Ok(()) } -impl App { - fn render(&mut self, frame: &mut Frame) { - let [background_area, status_area] = - Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).areas(frame.area()); - - let background = Self::background(background_area); - frame.render_widget(background, background_area); +fn draw(frame: &mut Frame, state: &mut PopupState) { + let vertical = Layout::vertical([Constraint::Min(0), Constraint::Length(1)]); + let [background_area, status_area] = vertical.areas(frame.area()); - let popup = Self::popup_widget(); - frame.render_stateful_widget_ref(popup, background_area, &mut self.popup); - - // must be called after rendering the popup widget as it relies on the popup area being set - let status_bar = self.status_bar(); - frame.render_widget(status_bar, status_area); - } + render_background(frame, background_area); + render_popup(frame, background_area, state); + render_status_bar(frame, status_area, state); +} - fn background(area: Rect) -> Paragraph<'static> { - let lorem_ipsum = lipsum(area.area() as usize / 5); - Paragraph::new(lorem_ipsum) - .wrap(Wrap { trim: false }) - .dark_gray() - } +fn render_background(frame: &mut Frame, area: Rect) { + let lorem_ipsum = lipsum(area.area() as usize / 5); + let background = Paragraph::new(lorem_ipsum) + .wrap(Wrap { trim: false }) + .dark_gray(); + frame.render_widget(background, area); +} - fn popup_widget() -> Popup<'static, Text<'static>> { - Popup::new(Text::from_iter([ - "q: exit", - "r: reset", - "j: move down", - "k: move up", - "h: move left", - "l: move right", - ])) +fn render_popup(frame: &mut Frame, area: Rect, state: &mut PopupState) { + let body = Text::from_iter([ + "q: exit", + "r: reset", + "j: move down", + "k: move up", + "h: move left", + "l: move right", + ]); + let popup = Popup::new(&body) .title("Popup") - .style(Style::new().white().on_blue()) - } - - /// Status bar at the bottom of the screen - /// - /// Must be called after rendering the popup widget as it relies on the popup area being set - fn status_bar(&self) -> Paragraph<'static> { - let popup_area = self.popup.area().unwrap_or_default(); - let text = format!("Popup area: {popup_area:?}"); - Paragraph::new(text).style(Style::new().white().on_black()) - } + .style(Style::new().white().on_blue()); + frame.render_stateful_widget(popup, area, state); +} - fn handle_events(&mut self) -> Result<()> { - match event::read()? { - Event::Key(event) => self.handle_key_event(event), - Event::Mouse(event) => self.popup.handle_mouse_event(event), - _ => (), - }; - Ok(()) - } +/// Status bar at the bottom of the screen +/// +/// Must be called after rendering the popup widget as it relies on the popup area being set +fn render_status_bar(frame: &mut Frame, area: Rect, state: &mut PopupState) { + let popup_area = state.area().unwrap_or_default(); + let text = format!("Popup area: {popup_area:?}"); + let paragraph = Paragraph::new(text).style(Style::new().white().on_black()); + frame.render_widget(paragraph, area); +} - fn handle_key_event(&mut self, event: KeyEvent) { - if event.kind != KeyEventKind::Press { - return; - } - match event.code { - KeyCode::Char('q') | KeyCode::Esc => self.should_exit = true, - KeyCode::Char('r') => self.popup = PopupState::default(), - // TODO: move handling to PopupState (e.g. move_up, move_down, etc. or move(Move:Up)) - KeyCode::Char('j') | KeyCode::Down => self.popup.move_by(0, 1), - KeyCode::Char('k') | KeyCode::Up => self.popup.move_by(0, -1), - KeyCode::Char('h') | KeyCode::Left => self.popup.move_by(-1, 0), - KeyCode::Char('l') | KeyCode::Right => self.popup.move_by(1, 0), - _ => {} +fn handle_events(popup: &mut PopupState, exit: &mut bool) -> Result<()> { + match event::read()? { + Event::Key(event) if event.kind == KeyEventKind::Press => { + handle_key_event(event, popup, exit) } + Event::Mouse(event) => popup.handle_mouse_event(event), + _ => (), + }; + Ok(()) +} + +fn handle_key_event(event: KeyEvent, popup: &mut PopupState, exit: &mut bool) { + match event.code { + KeyCode::Char('q') | KeyCode::Esc => *exit = true, + KeyCode::Char('r') => *popup = PopupState::default(), + // TODO: move handling to PopupState (e.g. move_up, move_down, etc. or move(Move:Up)) + KeyCode::Char('j') | KeyCode::Down => popup.move_by(0, 1), + KeyCode::Char('k') | KeyCode::Up => popup.move_by(0, -1), + KeyCode::Char('h') | KeyCode::Left => popup.move_by(-1, 0), + KeyCode::Char('l') | KeyCode::Right => popup.move_by(1, 0), + _ => {} } } diff --git a/tui-popup/examples/terminal/mod.rs b/tui-popup/examples/terminal/mod.rs deleted file mode 100644 index 3c56bae..0000000 --- a/tui-popup/examples/terminal/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::io::{stdout, Stdout}; - -use color_eyre::{config::HookBuilder, Result}; -use ratatui::{ - crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, - prelude::{CrosstermBackend, Terminal}, -}; - -pub fn init() -> Result>> { - install_error_hooks()?; - execute!(stdout(), EnterAlternateScreen, EnableMouseCapture)?; - let terminal = Terminal::new(CrosstermBackend::new(stdout()))?; - enable_raw_mode()?; - Ok(terminal) -} - -pub fn restore() -> Result<()> { - disable_raw_mode()?; - execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture)?; - Ok(()) -} - -fn install_error_hooks() -> Result<()> { - let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks(); - let panic_hook = panic_hook.into_panic_hook(); - std::panic::set_hook(Box::new(move |panic_info| { - let _ = restore(); - panic_hook(panic_info); - })); - let eyre_hook = eyre_hook.into_eyre_hook(); - color_eyre::eyre::set_hook(Box::new(move |error| { - let _ = restore(); - eyre_hook(error) - }))?; - Ok(()) -} diff --git a/tui-popup/src/known_size.rs b/tui-popup/src/known_size.rs new file mode 100644 index 0000000..0293f8e --- /dev/null +++ b/tui-popup/src/known_size.rs @@ -0,0 +1,55 @@ +use ratatui::text::Text; + +/// A trait for widgets that have a fixed size. +/// +/// This trait allows the popup to automatically size itself based on the size of the body widget. +/// Implementing this trait for a widget allows it to be used as the body of a popup. You can also +/// wrap existing widgets in a newtype and implement this trait for the newtype to use them as the +/// body of a popup. +/// +/// This trait was previously called `SizedWidgetRef`, but was renamed to `KnownSize` to avoid +/// confusion with the `WidgetRef` trait from `ratatui`. +pub trait KnownSize { + fn width(&self) -> usize; + fn height(&self) -> usize; +} + +impl KnownSize for Text<'_> { + fn width(&self) -> usize { + self.width() + } + + fn height(&self) -> usize { + self.height() + } +} + +impl KnownSize for &Text<'_> { + fn width(&self) -> usize { + Text::width(self) + } + + fn height(&self) -> usize { + Text::height(self) + } +} + +impl KnownSize for &str { + fn width(&self) -> usize { + Text::from(*self).width() + } + + fn height(&self) -> usize { + Text::from(*self).height() + } +} + +impl KnownSize for String { + fn width(&self) -> usize { + Text::from(self.as_str()).width() + } + + fn height(&self) -> usize { + Text::from(self.as_str()).height() + } +} diff --git a/tui-popup/src/known_size_wrapper.rs b/tui-popup/src/known_size_wrapper.rs new file mode 100644 index 0000000..1528d1d --- /dev/null +++ b/tui-popup/src/known_size_wrapper.rs @@ -0,0 +1,93 @@ +use std::fmt::Debug; + +use derive_setters::Setters; +use ratatui::{buffer::Buffer, layout::Rect, widgets::WidgetRef}; + +use crate::KnownSize; + +/// The `KnownSizeWrapper` struct wraps a widget and provides a fixed size for it. +/// +/// This struct is used to wrap a widget and provide a fixed size for it. This is useful when you +/// want to use a widget that does not implement `SizedWidgetRef` as the body of a popup. +#[derive(Debug, Setters)] +pub struct KnownSizeWrapper { + #[setters(skip)] + pub inner: W, + pub width: usize, + pub height: usize, +} + +impl WidgetRef for KnownSizeWrapper { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + self.inner.render_ref(area, buf); + } +} + +impl KnownSize for KnownSizeWrapper { + fn width(&self) -> usize { + self.width + } + + fn height(&self) -> usize { + self.height + } +} + +impl KnownSize for &KnownSizeWrapper { + fn width(&self) -> usize { + self.width + } + + fn height(&self) -> usize { + self.height + } +} + +impl KnownSizeWrapper { + /// Create a new `KnownSizeWrapper` with the given widget and size. + pub fn new(inner: W, width: usize, height: usize) -> Self { + Self { + inner, + width, + height, + } + } +} +#[cfg(test)] +mod tests { + use ratatui::{buffer::Buffer, layout::Rect}; + + use super::*; + + struct TestWidget; + + impl WidgetRef for TestWidget { + fn render_ref(&self, _area: Rect, _buf: &mut Buffer) { + "Hello".render_ref(_area, _buf); + } + } + + #[test] + fn test_sized_wrapper_new() { + let widget = TestWidget; + let wrapper = KnownSizeWrapper::new(widget, 10, 20); + assert_eq!(wrapper.width, 10); + assert_eq!(wrapper.height, 20); + } + + #[test] + fn test_sized_wrapper_render() { + let widget = TestWidget; + let wrapper = KnownSizeWrapper::new(widget, 20, 5); + let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 5)); + wrapper.render_ref(buffer.area, &mut buffer); + let expected = Buffer::with_lines([ + "Hello ", + " ", + " ", + " ", + " ", + ]); + assert_eq!(buffer, expected); + } +} diff --git a/tui-popup/src/lib.rs b/tui-popup/src/lib.rs index b871171..d193565 100644 --- a/tui-popup/src/lib.rs +++ b/tui-popup/src/lib.rs @@ -21,8 +21,14 @@ //! # Feature flags #![doc = document_features::document_features!()] +mod known_size; +mod known_size_wrapper; mod popup; mod popup_state; -pub use popup::{Popup, SizedWidgetRef, SizedWrapper}; -pub use popup_state::{DragState, PopupState}; +pub use crate::{ + known_size::KnownSize, + known_size_wrapper::KnownSizeWrapper, + popup::Popup, + popup_state::{DragState, PopupState}, +}; diff --git a/tui-popup/src/popup.rs b/tui-popup/src/popup.rs index 97cdd2e..89fe994 100644 --- a/tui-popup/src/popup.rs +++ b/tui-popup/src/popup.rs @@ -1,13 +1,16 @@ -use std::fmt::Debug; +use std::{cmp::min, fmt}; -use crate::PopupState; use derive_setters::Setters; use ratatui::{ - prelude::{Buffer, Line, Rect, Style, Text}, + buffer::Buffer, + layout::Rect, + style::Style, symbols::border::Set, - widgets::{Block, Borders, Clear, StatefulWidgetRef, Widget, WidgetRef}, + text::Line, + widgets::{Block, Borders, Clear, StatefulWidget, Widget, WidgetRef}, }; -use std::cmp::min; + +use crate::{KnownSize, PopupState}; /// Configuration for a popup. /// @@ -29,10 +32,10 @@ use std::cmp::min; /// frame.render_widget(&popup, frame.size()); /// } /// ``` -#[derive(Debug, Setters)] +#[derive(Setters)] #[setters(into)] #[non_exhaustive] -pub struct Popup<'content, W: SizedWidgetRef> { +pub struct Popup<'content, W> { /// The body of the popup. #[setters(skip)] pub body: W, @@ -48,18 +51,32 @@ pub struct Popup<'content, W: SizedWidgetRef> { pub border_style: Style, } -/// A trait for widgets that have a fixed size. -/// -/// This trait allows the popup to automatically size itself based on the size of the body widget. -/// Implementing this trait for a widget allows it to be used as the body of a popup. You can also -/// wrap existing widgets in a newtype and implement this trait for the newtype to use them as the -/// body of a popup. -pub trait SizedWidgetRef: WidgetRef + Debug { - fn width(&self) -> usize; - fn height(&self) -> usize; +impl fmt::Debug for Popup<'_, W> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // body does not implement Debug, so we can't use #[derive(Debug)] + f.debug_struct("Popup") + .field("body", &"...") + .field("title", &self.title) + .field("style", &self.style) + .field("borders", &self.borders) + .field("border_set", &self.border_set) + .field("border_style", &self.border_style) + .finish() + } +} + +impl PartialEq for Popup<'_, W> { + fn eq(&self, other: &Self) -> bool { + self.body == other.body + && self.title == other.title + && self.style == other.style + && self.borders == other.borders + && self.border_set == other.border_set + && self.border_style == other.border_style + } } -impl<'content, W: SizedWidgetRef> Popup<'content, W> { +impl<'content, W> Popup<'content, W> { /// Create a new popup with the given title and body with all the borders. /// /// # Parameters @@ -86,61 +103,33 @@ impl<'content, W: SizedWidgetRef> Popup<'content, W> { } } -impl SizedWidgetRef for &Text<'_> { - fn width(&self) -> usize { - Text::width(self) - } - - fn height(&self) -> usize { - Text::height(self) - } -} - -impl SizedWidgetRef for &str { - fn width(&self) -> usize { - Text::from(*self).width() - } - - fn height(&self) -> usize { - Text::from(*self).height() +impl Widget for Popup<'_, W> { + fn render(self, area: Rect, buf: &mut Buffer) { + let mut state = PopupState::default(); + StatefulWidget::render(&self, area, buf, &mut state); } } -#[derive(Debug)] -pub struct SizedWrapper { - pub inner: W, - pub width: usize, - pub height: usize, -} - -impl WidgetRef for SizedWrapper { - fn render_ref(&self, area: Rect, buf: &mut Buffer) { - self.inner.render_ref(area, buf); +impl Widget for &Popup<'_, W> { + fn render(self, area: Rect, buf: &mut Buffer) { + let mut state = PopupState::default(); + StatefulWidget::render(self, area, buf, &mut state); } } -impl SizedWidgetRef for SizedWrapper { - fn width(&self) -> usize { - self.width - } - - fn height(&self) -> usize { - self.height - } -} +impl StatefulWidget for Popup<'_, W> { + type State = PopupState; -impl WidgetRef for Popup<'_, W> { - fn render_ref(&self, area: Rect, buf: &mut Buffer) { - let mut state = PopupState::default(); - StatefulWidgetRef::render_ref(self, area, buf, &mut state); + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + StatefulWidget::render(&self, area, buf, state); } } -impl StatefulWidgetRef for Popup<'_, W> { +impl StatefulWidget for &Popup<'_, W> { type State = PopupState; - fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { - let area = if let Some(next) = state.area.take() { + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let popup_area = if let Some(next) = state.area.take() { // ensure that the popup remains on screen let width = min(next.width, area.width); let height = min(next.height, area.height); @@ -169,17 +158,17 @@ impl StatefulWidgetRef for Popup<'_, W> { centered_rect(width, height, area) }; - state.area.replace(area); + state.area.replace(popup_area); - Clear.render(area, buf); + Clear.render(popup_area, buf); let block = Block::default() .borders(self.borders) .border_set(self.border_set) .border_style(self.border_style) .title(self.title.clone()) .style(self.style); - Widget::render(&block, area, buf); - self.body.render_ref(block.inner(area), buf); + Widget::render(&block, popup_area, buf); + self.body.render_ref(block.inner(popup_area), buf); } } @@ -192,3 +181,79 @@ fn centered_rect(width: u16, height: u16, area: Rect) -> Rect { height: min(height, area.height), } } + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn new() { + let popup = Popup::new("Test Body"); + assert_eq!( + popup, + Popup { + body: "Test Body", // &str is a widget + borders: Borders::ALL, + border_set: Set::default(), + border_style: Style::default(), + title: Line::default(), + style: Style::default(), + } + ) + } + + #[test] + fn render() { + let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 5)); + let mut state = PopupState::default(); + let expected = Buffer::with_lines([ + " ", + " ┌Title──────┐ ", + " │Hello World│ ", + " └───────────┘ ", + " ", + ]); + + // Check that a popup ref can render a widget defined by a ref value (e.g. `&str`). + let popup = Popup::new("Hello World").title("Title"); + StatefulWidget::render(&popup, buffer.area, &mut buffer, &mut state); + assert_eq!(buffer, expected); + + // Check that a popup ref can render a widget defined by a owned value (e.g. `String`). + let popup = Popup::new("Hello World".to_string()).title("Title"); + StatefulWidget::render(&popup, buffer.area, &mut buffer, &mut state); + assert_eq!(buffer, expected); + + // Check that an owned popup can render a widget defined by a ref value (e.g. `&str`). + let popup = Popup::new("Hello World").title("Title"); + StatefulWidget::render(popup, buffer.area, &mut buffer, &mut state); + assert_eq!(buffer, expected); + + // Check that an owned popup can render a widget defined by a owned value (e.g. `String`). + let popup = Popup::new("Hello World".to_string()).title("Title"); + StatefulWidget::render(popup, buffer.area, &mut buffer, &mut state); + assert_eq!(buffer, expected); + + // Check that a popup ref can render a ref value (e.g. `&str`), with default state. + let popup = Popup::new("Hello World").title("Title"); + Widget::render(&popup, buffer.area, &mut buffer); + assert_eq!(buffer, expected); + + // Check that a popup ref can render an owned value (e.g. `String`), with default state. + let popup = Popup::new("Hello World".to_string()).title("Title"); + Widget::render(&popup, buffer.area, &mut buffer); + assert_eq!(buffer, expected); + + // Check that an owned popup can render a ref value (e.g. `&str`), with default state. + let popup = Popup::new("Hello World").title("Title"); + Widget::render(popup, buffer.area, &mut buffer); + assert_eq!(buffer, expected); + + // Check that an owned popup can render an owned value (e.g. `String`), with default state. + let popup = Popup::new("Hello World".to_string()).title("Title"); + Widget::render(popup, buffer.area, &mut buffer); + assert_eq!(buffer, expected); + } +}