From 815757fcbbc147050f8ce9418a4e91fd871d011f Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Wed, 24 Jan 2024 10:34:10 -0800 Subject: [PATCH] feat(widgets): implement Widget for Widget refs (#833) Many widgets can be rendered without changing their state. This commit implements The `Widget` trait for references to widgets and changes their implementations to be immutable. This allows us to render widgets without consuming them by passing a ref to the widget when calling `Frame::render_widget()`. ```rust // this might be stored in a struct let paragraph = Paragraph::new("Hello world!"); let [left, right] = area.split(&Layout::horizontal([20, 20])); frame.render_widget(¶graph, left); frame.render_widget(¶graph, right); // we can reuse the widget ``` Implemented for all widgets except BarChart (which has an implementation that modifies the internal state and requires a rewrite to fix. Other widgets will be implemented in follow up commits. Fixes: https://github.com/ratatui-org/ratatui/discussions/164 Replaces PRs: https://github.com/ratatui-org/ratatui/pull/122 and https://github.com/ratatui-org/ratatui/pull/16 Enables: https://github.com/ratatui-org/ratatui/issues/132 Validated as a viable working solution by: https://github.com/ratatui-org/ratatui/pull/836 --- examples/colors_rgb.rs | 2 +- examples/demo2/colors.rs | 2 +- src/layout/layout.rs | 2 +- src/prelude.rs | 1 + src/terminal/frame.rs | 10 +--- src/text/line.rs | 18 ++++++- src/text/span.rs | 16 ++++++ src/text/text.rs | 20 +++++++- src/widgets.rs | 54 +++++++++++++++++++- src/widgets/barchart.rs | 29 ++++------- src/widgets/barchart/bar.rs | 11 ++-- src/widgets/barchart/bar_group.rs | 6 +-- src/widgets/block.rs | 55 +++++++++++++++----- src/widgets/calendar.rs | 58 +++++++++++---------- src/widgets/canvas.rs | 35 +++++++------ src/widgets/canvas/line.rs | 6 +-- src/widgets/canvas/map.rs | 6 +-- src/widgets/canvas/rectangle.rs | 6 +-- src/widgets/chart.rs | 43 ++++++++-------- src/widgets/clear.rs | 8 ++- src/widgets/gauge.rs | 84 ++++++++++++++----------------- src/widgets/list.rs | 51 +++++++++++-------- src/widgets/paragraph.rs | 33 ++++++------ src/widgets/sparkline.rs | 32 ++++++------ src/widgets/table/cell.rs | 2 +- src/widgets/table/table.rs | 34 +++++++------ src/widgets/tabs.rs | 36 ++++++------- tests/widgets_calendar.rs | 5 +- tests/widgets_chart.rs | 63 ++++++++++++----------- 29 files changed, 426 insertions(+), 302 deletions(-) diff --git a/examples/colors_rgb.rs b/examples/colors_rgb.rs index 25dd299df..76a78420a 100644 --- a/examples/colors_rgb.rs +++ b/examples/colors_rgb.rs @@ -23,7 +23,7 @@ use crossterm::{ ExecutableCommand, }; use palette::{convert::FromColorUnclamped, Okhsv, Srgb}; -use ratatui::{prelude::*, widgets::*}; +use ratatui::prelude::*; #[derive(Debug, Default)] struct App { diff --git a/examples/demo2/colors.rs b/examples/demo2/colors.rs index 902ffa3fc..3e32a374d 100644 --- a/examples/demo2/colors.rs +++ b/examples/demo2/colors.rs @@ -1,5 +1,5 @@ use palette::{IntoColor, Okhsv, Srgb}; -use ratatui::{prelude::*, widgets::*}; +use ratatui::prelude::*; /// A widget that renders a color swatch of RGB colors. /// diff --git a/src/layout/layout.rs b/src/layout/layout.rs index 6df8e281d..78e1acf0e 100644 --- a/src/layout/layout.rs +++ b/src/layout/layout.rs @@ -1172,7 +1172,7 @@ mod tests { assert_buffer_eq, layout::flex::Flex, prelude::{Constraint::*, *}, - widgets::{Paragraph, Widget}, + widgets::Paragraph, }; /// Test that the given constraints applied to the given area result in the expected layout. diff --git a/src/prelude.rs b/src/prelude.rs index 55df36ef1..182c17743 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -31,4 +31,5 @@ pub use crate::{ symbols::{self, Marker}, terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport}, text::{self, Line, Masked, Span, Text}, + widgets::{block::BlockExt, StatefulWidget, Widget}, }; diff --git a/src/terminal/frame.rs b/src/terminal/frame.rs index 0480788f9..2c85b8b8e 100644 --- a/src/terminal/frame.rs +++ b/src/terminal/frame.rs @@ -1,7 +1,4 @@ -use crate::{ - prelude::*, - widgets::{StatefulWidget, Widget}, -}; +use crate::prelude::*; /// A consistent view into the terminal state for rendering a single frame. /// @@ -74,10 +71,7 @@ impl Frame<'_> { /// ``` /// /// [`Layout`]: crate::layout::Layout - pub fn render_widget(&mut self, widget: W, area: Rect) - where - W: Widget, - { + pub fn render_widget(&mut self, widget: W, area: Rect) { widget.render(area, self.buffer); } diff --git a/src/text/line.rs b/src/text/line.rs index 1469f4d81..08e68f428 100644 --- a/src/text/line.rs +++ b/src/text/line.rs @@ -407,6 +407,22 @@ impl<'a> From> for String { } impl Widget for Line<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + Widget::render(&self, area, buf); + } +} + +/// Implement [`Widget`] for [`Option`] to simplify the common case of having an optional +/// [`Line`] field in a widget. +impl Widget for &Option> { + fn render(self, area: Rect, buf: &mut Buffer) { + if let Some(line) = self { + line.render(area, buf); + } + } +} + +impl Widget for &Line<'_> { fn render(self, area: Rect, buf: &mut Buffer) { let area = area.intersection(buf.area); buf.set_style(area, self.style); @@ -418,7 +434,7 @@ impl Widget for Line<'_> { None => 0, }; let mut x = area.left().saturating_add(offset); - for span in self.spans { + for span in self.spans.iter() { let span_width = span.width() as u16; let span_area = Rect { x, diff --git a/src/text/span.rs b/src/text/span.rs index e98dcd62a..9223e98a5 100644 --- a/src/text/span.rs +++ b/src/text/span.rs @@ -299,6 +299,22 @@ impl<'a> Styled for Span<'a> { } impl Widget for Span<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + Widget::render(&self, area, buf); + } +} + +/// Implement [`Widget`] for [`Option`] to simplify the common case of having an optional +/// [`Span`] field in a widget. +impl Widget for &Option> { + fn render(self, area: Rect, buf: &mut Buffer) { + if let Some(span) = self { + span.render(area, buf); + } + } +} + +impl Widget for &Span<'_> { fn render(self, area: Rect, buf: &mut Buffer) { let Rect { x: mut current_x, diff --git a/src/text/text.rs b/src/text/text.rs index c98015a91..d3f1105f7 100644 --- a/src/text/text.rs +++ b/src/text/text.rs @@ -414,10 +414,26 @@ impl std::fmt::Display for Text<'_> { } } -impl<'a> Widget for Text<'a> { +impl Widget for Text<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + Widget::render(&self, area, buf); + } +} + +/// Implement [`Widget`] for [`Option`] to simplify the common case of having an optional +/// [`Text`] field in a widget. +impl Widget for &Option> { + fn render(self, area: Rect, buf: &mut Buffer) { + if let Some(text) = self { + text.render(area, buf); + } + } +} + +impl Widget for &Text<'_> { fn render(self, area: Rect, buf: &mut Buffer) { buf.set_style(area, self.style); - for (line, row) in self.lines.into_iter().zip(area.rows()) { + for (line, row) in self.lines.iter().zip(area.rows()) { let line_width = line.width() as u16; let x_offset = match (self.alignment, line.alignment) { diff --git a/src/widgets.rs b/src/widgets.rs index fed25903f..6932af15d 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -53,7 +53,59 @@ pub use self::{ }; use crate::{buffer::Buffer, layout::Rect}; -/// Base requirements for a Widget +/// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`]. +/// +/// Prior to Ratatui 0.26.0, widgets generally were created for each frame as they were consumed +/// during rendering. This meant that they were not meant to be stored but used as *commands* to +/// draw common figures in the UI. +/// +/// Starting with Ratatui 0.26.0, the `Widget` trait was more universally implemented on &T instead +/// of just T. This means that widgets can be stored and reused across frames without having to +/// clone or recreate them. +/// +/// # Examples +/// +/// ```rust,no_run +/// use ratatui::{backend::TestBackend, prelude::*, widgets::*}; +/// # let backend = TestBackend::new(5, 5); +/// # let mut terminal = Terminal::new(backend).unwrap(); +/// +/// terminal.draw(|frame| { +/// frame.render_widget(Clear, frame.size()); +/// }); +/// ``` +/// +/// Rendering a widget by reference: +/// +/// ```rust +/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*}; +/// # let backend = TestBackend::new(5, 5); +/// # let mut terminal = Terminal::new(backend).unwrap(); +/// // this variable could instead be a value stored in a struct and reused across frames +/// let paragraph = Paragraph::new("Hello world!"); +/// +/// terminal.draw(|frame| { +/// frame.render_widget(¶graph, frame.size()); +/// }); +/// ``` +/// +/// It's common to render widgets inside other widgets: +/// +/// ```rust +/// use ratatui::{prelude::*, widgets::*}; +/// +/// struct MyWidget; +/// +/// impl Widget for &MyWidget { +/// fn render(self, area: Rect, buf: &mut Buffer) { +/// Block::default() +/// .title("My Widget") +/// .borders(Borders::ALL) +/// .render(area, buf); +/// // ... +/// } +/// } +/// ``` pub trait Widget { /// Draws the current state of the widget in the given buffer. That is the only method required /// to implement a custom widget. diff --git a/src/widgets/barchart.rs b/src/widgets/barchart.rs index f2b09982c..101ca781d 100644 --- a/src/widgets/barchart.rs +++ b/src/widgets/barchart.rs @@ -1,5 +1,5 @@ #![warn(missing_docs)] -use crate::prelude::*; +use crate::{prelude::*, widgets::Block}; mod bar; mod bar_group; @@ -7,8 +7,6 @@ mod bar_group; pub use bar::Bar; pub use bar_group::BarGroup; -use super::{Block, Widget}; - /// A chart showing values as [bars](Bar). /// /// Here is a possible `BarChart` output. @@ -36,6 +34,9 @@ use super::{Block, Widget}; /// The chart can have a [`Direction`] (by default the bars are [`Vertical`](Direction::Vertical)). /// This is set using [`BarChart::direction`]. /// +/// Note: this is the only widget that doesn't implement `Widget` for `&T` because the current +/// implementation modifies the internal state of self. This will be fixed in the future. +/// /// # Examples /// /// The following example creates a `BarChart` with two groups of bars. @@ -391,15 +392,6 @@ impl<'a> BarChart<'a> { } } - /// renders the block if there is one and updates the area to the inner area - fn render_block(&mut self, area: &mut Rect, buf: &mut Buffer) { - if let Some(block) = self.block.take() { - let inner_area = block.inner(*area); - block.render(*area, buf); - *area = inner_area - } - } - fn render_horizontal(self, buf: &mut Buffer, area: Rect) { // get the longest label let label_size = self @@ -586,19 +578,20 @@ impl<'a> BarChart<'a> { } } -impl<'a> Widget for BarChart<'a> { - fn render(mut self, mut area: Rect, buf: &mut Buffer) { +impl Widget for BarChart<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { buf.set_style(area, self.style); - self.render_block(&mut area, buf); + self.block.render(area, buf); + let inner = self.block.inner_if_some(area); - if area.is_empty() || self.data.is_empty() || self.bar_width == 0 { + if inner.is_empty() || self.data.is_empty() || self.bar_width == 0 { return; } match self.direction { - Direction::Horizontal => self.render_horizontal(buf, area), - Direction::Vertical => self.render_vertical(buf, area), + Direction::Horizontal => self.render_horizontal(buf, inner), + Direction::Vertical => self.render_vertical(buf, inner), } } } diff --git a/src/widgets/barchart/bar.rs b/src/widgets/barchart/bar.rs index dc32589a7..a8ef0cc79 100644 --- a/src/widgets/barchart/bar.rs +++ b/src/widgets/barchart/bar.rs @@ -115,24 +115,21 @@ impl<'a> Bar<'a> { /// bar width, then the value is split into 2 parts. the first part is rendered in the bar /// using value_style. The second part is rendered outside the bar using bar_style pub(super) fn render_value_with_different_styles( - self, + &self, buf: &mut Buffer, area: Rect, bar_length: usize, default_value_style: Style, bar_style: Style, ) { - let text = if let Some(text) = self.text_value { - text - } else { - self.value.to_string() - }; + let value = self.value.to_string(); + let text = self.text_value.as_ref().unwrap_or(&value); if !text.is_empty() { let style = default_value_style.patch(self.value_style); // Since the value may be longer than the bar itself, we need to use 2 different styles // while rendering. Render the first part with the default value style - buf.set_stringn(area.x, area.y, &text, bar_length, style); + buf.set_stringn(area.x, area.y, text, bar_length, style); // render the second part with the bar_style if text.len() > bar_length { let (first, second) = text.split_at(bar_length); diff --git a/src/widgets/barchart/bar_group.rs b/src/widgets/barchart/bar_group.rs index 5ed907502..f51a76835 100644 --- a/src/widgets/barchart/bar_group.rs +++ b/src/widgets/barchart/bar_group.rs @@ -1,9 +1,5 @@ use super::Bar; -use crate::{ - prelude::{Alignment, Buffer, Rect}, - style::Style, - text::Line, -}; +use crate::prelude::*; /// A group of bars to be shown by the Barchart. /// diff --git a/src/widgets/block.rs b/src/widgets/block.rs index a6df2fe26..1614cd06a 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -8,11 +8,7 @@ use strum::{Display, EnumString}; -use crate::{ - prelude::*, - symbols::border, - widgets::{Borders, Widget}, -}; +use crate::{prelude::*, symbols::border, widgets::Borders}; mod padding; pub mod title; @@ -520,7 +516,35 @@ impl<'a> Block<'a> { self.padding = padding; self } +} + +impl Widget for Block<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + Widget::render(&self, area, buf); + } +} + +/// Implement [`Widget`] for [`Option`] to simplify the common case of having an optional +/// [`Block`] field in a widget. +impl Widget for &Option> { + fn render(self, area: Rect, buf: &mut Buffer) { + if let Some(block) = self { + block.render(area, buf); + } + } +} +impl Widget for &Block<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + if area.is_empty() { + return; + } + self.render_borders(area, buf); + self.render_titles(area, buf); + } +} + +impl Block<'_> { fn render_borders(&self, area: Rect, buf: &mut Buffer) { buf.set_style(area, self.style); let symbols = self.border_set; @@ -703,13 +727,20 @@ impl<'a> Block<'a> { } } -impl<'a> Widget for Block<'a> { - fn render(self, area: Rect, buf: &mut Buffer) { - if area.area() == 0 { - return; - } - self.render_borders(area, buf); - self.render_titles(area, buf); +/// An extension trait for [`Block`] that provides some convenience methods. +/// +/// This is implemented for [`Option`](Option) to simplify the common case of having a +/// widget with an optional block. +pub trait BlockExt { + /// Return the inner area of the block if it is `Some`. Otherwise, returns `area`. + /// + /// This is a useful convenience method for widgets that have an `Option` field + fn inner_if_some(&self, area: Rect) -> Rect; +} + +impl BlockExt for Option> { + fn inner_if_some(&self, area: Rect) -> Rect { + self.as_ref().map_or(area, |block| block.inner(area)) } } diff --git a/src/widgets/calendar.rs b/src/widgets/calendar.rs index 4f82e5cdb..fc99b787c 100644 --- a/src/widgets/calendar.rs +++ b/src/widgets/calendar.rs @@ -12,10 +12,7 @@ use std::collections::HashMap; use time::{Date, Duration, OffsetDateTime}; -use crate::{ - prelude::*, - widgets::{Block, Widget}, -}; +use crate::{prelude::*, widgets::Block}; /// Display a month calendar for the month containing `display_date` #[derive(Debug, Clone, Eq, PartialEq, Hash)] @@ -117,36 +114,42 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> { } } -impl<'a, DS: DateStyler> Widget for Monthly<'a, DS> { - fn render(mut self, area: Rect, buf: &mut Buffer) { - // Block is used for borders and such - // Draw that first, and use the blank area inside the block for our own purposes - let mut area = match self.block.take() { - None => area, - Some(b) => { - let inner = b.inner(area); - b.render(area, buf); - inner - } - }; +impl Widget for Monthly<'_, DS> { + fn render(self, area: Rect, buf: &mut Buffer) { + Widget::render(&self, area, buf); + } +} + +impl Widget for &Monthly<'_, DS> { + fn render(self, area: Rect, buf: &mut Buffer) { + self.block.render(area, buf); + let inner = self.block.inner_if_some(area); + self.render_monthly(inner, buf); + } +} + +impl Monthly<'_, DS> { + fn render_monthly(&self, area: Rect, buf: &mut Buffer) { + let layout = Layout::vertical([ + Constraint::Length(self.show_month.is_some().into()), + Constraint::Length(self.show_weekday.is_some().into()), + Constraint::Proportional(1), + ]); + let [month_header, days_header, days_area] = area.split(&layout); // Draw the month name and year if let Some(style) = self.show_month { - let line = Span::styled( + Line::styled( format!("{} {}", self.display_date.month(), self.display_date.year()), style, - ); - // cal is 21 cells wide, so hard code the 11 - let x_off = 11_u16.saturating_sub(line.width() as u16 / 2); - buf.set_line(area.x + x_off, area.y, &line.into(), area.width); - area.y += 1 + ) + .alignment(Alignment::Center) + .render(month_header, buf); } // Draw days of week if let Some(style) = self.show_weekday { - let days = String::from(" Su Mo Tu We Th Fr Sa"); - buf.set_string(area.x, area.y, days, style); - area.y += 1; + Span::styled(" Su Mo Tu We Th Fr Sa", style).render(days_header, buf); } // Set the start of the calendar to the Sunday before the 1st (or the sunday of the first) @@ -154,6 +157,7 @@ impl<'a, DS: DateStyler> Widget for Monthly<'a, DS> { let offset = Duration::days(first_of_month.weekday().number_days_from_sunday().into()); let mut curr_day = first_of_month - offset; + let mut y = days_area.y; // go through all the weeks containing a day in the target month. while curr_day.month() as u8 != self.display_date.month().next() as u8 { let mut spans = Vec::with_capacity(14); @@ -168,8 +172,8 @@ impl<'a, DS: DateStyler> Widget for Monthly<'a, DS> { spans.push(self.format_date(curr_day)); curr_day += Duration::DAY; } - buf.set_line(area.x, area.y, &spans.into(), area.width); - area.y += 1; + buf.set_line(days_area.x, y, &spans.into(), area.width); + y += 1; } } } diff --git a/src/widgets/canvas.rs b/src/widgets/canvas.rs index 1ff9e8f51..79c1f55f7 100644 --- a/src/widgets/canvas.rs +++ b/src/widgets/canvas.rs @@ -16,14 +16,7 @@ pub use self::{ points::Points, rectangle::Rectangle, }; -use crate::{ - buffer::Buffer, - layout::Rect, - style::{Color, Style}, - symbols, - text::Line as TextLine, - widgets::{Block, Widget}, -}; +use crate::{prelude::*, symbols, text::Line as TextLine, widgets::Block}; /// Interface for all shapes that may be drawn on a Canvas widget. pub trait Shape { @@ -694,19 +687,25 @@ where } } -impl<'a, F> Widget for Canvas<'a, F> +impl Widget for Canvas<'_, F> where F: Fn(&mut Context), { - fn render(mut self, area: Rect, buf: &mut Buffer) { - let canvas_area = match self.block.take() { - Some(b) => { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - } - None => area, - }; + fn render(self, area: Rect, buf: &mut Buffer) { + Widget::render(&self, area, buf); + } +} + +impl Widget for &Canvas<'_, F> +where + F: Fn(&mut Context), +{ + fn render(self, area: Rect, buf: &mut Buffer) { + self.block.render(area, buf); + let canvas_area = self.block.inner_if_some(area); + if canvas_area.is_empty() { + return; + } buf.set_style(canvas_area, Style::default().bg(self.background_color)); diff --git a/src/widgets/canvas/line.rs b/src/widgets/canvas/line.rs index a3542b96b..b4725442d 100644 --- a/src/widgets/canvas/line.rs +++ b/src/widgets/canvas/line.rs @@ -108,11 +108,7 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us #[cfg(test)] mod tests { use super::Line; - use crate::{ - assert_buffer_eq, - prelude::*, - widgets::{canvas::Canvas, Widget}, - }; + use crate::{assert_buffer_eq, prelude::*, widgets::canvas::Canvas}; #[track_caller] fn test(line: Line, expected_lines: Vec<&str>) { diff --git a/src/widgets/canvas/map.rs b/src/widgets/canvas/map.rs index 2dfe727a0..e733d4f9c 100644 --- a/src/widgets/canvas/map.rs +++ b/src/widgets/canvas/map.rs @@ -46,11 +46,7 @@ mod tests { use strum::ParseError; use super::*; - use crate::{ - assert_buffer_eq, - prelude::*, - widgets::{canvas::Canvas, Widget}, - }; + use crate::{assert_buffer_eq, prelude::*, widgets::canvas::Canvas}; #[test] fn map_resolution_to_string() { diff --git a/src/widgets/canvas/rectangle.rs b/src/widgets/canvas/rectangle.rs index 542ae8203..9e2aa5187 100644 --- a/src/widgets/canvas/rectangle.rs +++ b/src/widgets/canvas/rectangle.rs @@ -54,11 +54,7 @@ impl Shape for Rectangle { #[cfg(test)] mod tests { use super::*; - use crate::{ - assert_buffer_eq, - prelude::*, - widgets::{canvas::Canvas, Widget}, - }; + use crate::{assert_buffer_eq, prelude::*, widgets::canvas::Canvas}; #[test] fn draw_block_lines() { diff --git a/src/widgets/chart.rs b/src/widgets/chart.rs index dd9b70c1b..6f8f00f5b 100644 --- a/src/widgets/chart.rs +++ b/src/widgets/chart.rs @@ -4,14 +4,14 @@ use std::cmp::max; use strum::{Display, EnumString}; use unicode_width::UnicodeWidthStr; +use super::block::BlockExt; use crate::{ - buffer::Buffer, layout::Flex, prelude::*, symbols, widgets::{ canvas::{Canvas, Line as CanvasLine, Points}, - Block, Borders, Widget, + Block, Borders, }, }; @@ -809,7 +809,7 @@ impl<'a> Chart<'a> { } fn render_x_labels( - &mut self, + &self, buf: &mut Buffer, layout: &ChartLayout, chart_area: Rect, @@ -892,7 +892,7 @@ impl<'a> Chart<'a> { } fn render_y_labels( - &mut self, + &self, buf: &mut Buffer, layout: &ChartLayout, chart_area: Rect, @@ -916,26 +916,27 @@ impl<'a> Chart<'a> { } } -impl<'a> Widget for Chart<'a> { - fn render(mut self, area: Rect, buf: &mut Buffer) { - if area.area() == 0 { +impl Widget for Chart<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + Widget::render(&self, area, buf); + } +} + +impl Widget for &Chart<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + buf.set_style(area, self.style); + + self.block.render(area, buf); + let chart_area = self.block.inner_if_some(area); + if chart_area.is_empty() { return; } - buf.set_style(area, self.style); + // Sample the style of the entire widget. This sample will be used to reset the style of // the cells that are part of the components put on top of the grah area (i.e legend and // axis names). let original_style = buf.get(area.left(), area.top()).style(); - let chart_area = match self.block.take() { - Some(b) => { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - } - None => area, - }; - let layout = self.layout(chart_area); let graph_area = layout.graph_area; if graph_area.width < 1 || graph_area.height < 1 { @@ -996,7 +997,7 @@ impl<'a> Widget for Chart<'a> { } if let Some((x, y)) = layout.title_x { - let title = self.x_axis.title.unwrap(); + let title = self.x_axis.title.as_ref().unwrap(); let width = graph_area .right() .saturating_sub(x) @@ -1010,11 +1011,11 @@ impl<'a> Widget for Chart<'a> { }, original_style, ); - buf.set_line(x, y, &title, width); + buf.set_line(x, y, title, width); } if let Some((x, y)) = layout.title_y { - let title = self.y_axis.title.unwrap(); + let title = self.y_axis.title.as_ref().unwrap(); let width = graph_area .right() .saturating_sub(x) @@ -1028,7 +1029,7 @@ impl<'a> Widget for Chart<'a> { }, original_style, ); - buf.set_line(x, y, &title, width); + buf.set_line(x, y, title, width); } if let Some(legend_area) = layout.legend_area { diff --git a/src/widgets/clear.rs b/src/widgets/clear.rs index 2b3667559..b9b2cccf8 100644 --- a/src/widgets/clear.rs +++ b/src/widgets/clear.rs @@ -1,4 +1,4 @@ -use crate::{buffer::Buffer, layout::Rect, widgets::Widget}; +use crate::prelude::*; /// A widget to clear/reset a certain area to allow overdrawing (e.g. for popups). /// @@ -25,6 +25,12 @@ use crate::{buffer::Buffer, layout::Rect, widgets::Widget}; pub struct Clear; impl Widget for Clear { + fn render(self, area: Rect, buf: &mut Buffer) { + Widget::render(&self, area, buf); + } +} + +impl Widget for &Clear { fn render(self, area: Rect, buf: &mut Buffer) { for x in area.left()..area.right() { for y in area.top()..area.bottom() { diff --git a/src/widgets/gauge.rs b/src/widgets/gauge.rs index dc85327e1..8c6fe8438 100644 --- a/src/widgets/gauge.rs +++ b/src/widgets/gauge.rs @@ -1,12 +1,6 @@ #![deny(missing_docs)] -use crate::{ - buffer::Buffer, - layout::Rect, - style::{Color, Style, Styled}, - symbols, - text::{Line, Span}, - widgets::{Block, Widget}, -}; + +use crate::{prelude::*, widgets::Block}; /// A widget to display a progress bar. /// @@ -161,28 +155,33 @@ impl<'a> Gauge<'a> { } } -impl<'a> Widget for Gauge<'a> { - fn render(mut self, area: Rect, buf: &mut Buffer) { +impl Widget for Gauge<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + Widget::render(&self, area, buf); + } +} + +impl Widget for &Gauge<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { buf.set_style(area, self.style); - let gauge_area = match self.block.take() { - Some(b) => { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - } - None => area, - }; - buf.set_style(gauge_area, self.gauge_style); - if gauge_area.height < 1 { + self.block.render(area, buf); + let inner = self.block.inner_if_some(area); + self.render_gague(inner, buf); + } +} + +impl Gauge<'_> { + fn render_gague(&self, gauge_area: Rect, buf: &mut Buffer) { + if gauge_area.is_empty() { return; } + buf.set_style(gauge_area, self.gauge_style); + // compute label value and its position // label is put at the center of the gauge_area - let label = { - let pct = f64::round(self.ratio * 100.0); - self.label.unwrap_or_else(|| Span::from(format!("{pct}%"))) - }; + let default_label = Span::raw(format!("{}%", f64::round(self.ratio * 100.0))); + let label = self.label.as_ref().unwrap_or(&default_label); let clamped_label_width = gauge_area.width.min(label.width() as u16); let label_col = gauge_area.left() + (gauge_area.width - clamped_label_width) / 2; let label_row = gauge_area.top() + gauge_area.height / 2; @@ -217,7 +216,7 @@ impl<'a> Widget for Gauge<'a> { } } // render the label - buf.set_span(label_col, label_row, &label, clamped_label_width); + buf.set_span(label_col, label_row, label, clamped_label_width); } } @@ -351,32 +350,25 @@ impl<'a> LineGauge<'a> { } } -impl<'a> Widget for LineGauge<'a> { - fn render(mut self, area: Rect, buf: &mut Buffer) { - buf.set_style(area, self.style); - let gauge_area = match self.block.take() { - Some(b) => { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - } - None => area, - }; +impl Widget for LineGauge<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + Widget::render(&self, area, buf); + } +} - if gauge_area.height < 1 { +impl Widget for &LineGauge<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + buf.set_style(area, self.style); + self.block.render(area, buf); + let gauge_area = self.block.inner_if_some(area); + if gauge_area.is_empty() { return; } let ratio = self.ratio; - let label = self - .label - .unwrap_or_else(move || Line::from(format!("{:.0}%", ratio * 100.0))); - let (col, row) = buf.set_line( - gauge_area.left(), - gauge_area.top(), - &label, - gauge_area.width, - ); + let default_label = Line::from(format!("{:.0}%", ratio * 100.0)); + let label = self.label.as_ref().unwrap_or(&default_label); + let (col, row) = buf.set_line(gauge_area.left(), gauge_area.top(), label, gauge_area.width); let start = col + 1; if start >= gauge_area.right() { return; diff --git a/src/widgets/list.rs b/src/widgets/list.rs index 8e6a792c9..1d97070c4 100755 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -4,7 +4,7 @@ use unicode_width::UnicodeWidthStr; use crate::{ prelude::*, - widgets::{Block, HighlightSpacing, StatefulWidget, Widget}, + widgets::{Block, HighlightSpacing}, }; /// State of the [`List`] widget @@ -812,21 +812,37 @@ impl<'a> List<'a> { } } -impl<'a> StatefulWidget for List<'a> { +impl Widget for List<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + let mut state = ListState::default(); + StatefulWidget::render(&self, area, buf, &mut state); + } +} + +impl Widget for &List<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + let mut state = ListState::default(); + StatefulWidget::render(self, area, buf, &mut state); + } +} + +impl StatefulWidget for List<'_> { type State = ListState; - fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + StatefulWidget::render(&self, area, buf, state); + } +} + +impl StatefulWidget for &List<'_> { + type State = ListState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { buf.set_style(area, self.style); - let list_area = match self.block.take() { - Some(b) => { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - } - None => area, - }; + self.block.render(area, buf); + let list_area = self.block.inner_if_some(area); - if self.items.is_empty() || list_area.is_empty() { + if list_area.is_empty() || self.items.is_empty() { return; } @@ -846,7 +862,7 @@ impl<'a> StatefulWidget for List<'a> { let selection_spacing = self.highlight_spacing.should_add(state.selected.is_some()); for (i, item) in self .items - .iter_mut() + .iter() .enumerate() .skip(state.offset) .take(last_visible_index - first_visible_index) @@ -911,13 +927,6 @@ impl<'a> StatefulWidget for List<'a> { } } -impl<'a> Widget for List<'a> { - fn render(self, area: Rect, buf: &mut Buffer) { - let mut state = ListState::default(); - StatefulWidget::render(self, area, buf, &mut state); - } -} - impl<'a> Styled for List<'a> { type Item = List<'a>; @@ -961,7 +970,7 @@ mod tests { prelude::Alignment, style::{Color, Modifier, Stylize}, text::{Line, Span}, - widgets::{Borders, StatefulWidget, Widget}, + widgets::Borders, }; #[test] diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index 299c6b984..497b812f6 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -1,12 +1,10 @@ use unicode_width::UnicodeWidthStr; +use super::block::BlockExt; use crate::{ prelude::*, text::StyledGrapheme, - widgets::{ - reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine}, - Block, Widget, - }, + widgets::{reflow::*, Block}, }; fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 { @@ -325,19 +323,24 @@ impl<'a> Paragraph<'a> { } } -impl<'a> Widget for Paragraph<'a> { - fn render(mut self, area: Rect, buf: &mut Buffer) { +impl Widget for Paragraph<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + Widget::render(&self, area, buf); + } +} + +impl Widget for &Paragraph<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { buf.set_style(area, self.style); - let text_area = match self.block.take() { - Some(b) => { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - } - None => area, - }; + self.block.render(area, buf); + let inner = self.block.inner_if_some(area); + self.render_paragraph(inner, buf); + } +} - if text_area.height < 1 { +impl Paragraph<'_> { + fn render_paragraph(&self, text_area: Rect, buf: &mut Buffer) { + if text_area.is_empty() { return; } diff --git a/src/widgets/sparkline.rs b/src/widgets/sparkline.rs index 0c9ca88df..a12ce1ab5 100644 --- a/src/widgets/sparkline.rs +++ b/src/widgets/sparkline.rs @@ -3,10 +3,7 @@ use std::cmp::min; use strum::{Display, EnumString}; -use crate::{ - prelude::*, - widgets::{Block, Widget}, -}; +use crate::{prelude::*, widgets::Block}; /// Widget to render a sparkline over one or more lines. /// @@ -156,18 +153,23 @@ impl<'a> Styled for Sparkline<'a> { } } -impl<'a> Widget for Sparkline<'a> { - fn render(mut self, area: Rect, buf: &mut Buffer) { - let spark_area = match self.block.take() { - Some(b) => { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - } - None => area, - }; +impl Widget for Sparkline<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + Widget::render(&self, area, buf); + } +} + +impl Widget for &Sparkline<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + self.block.render(area, buf); + let inner = self.block.inner_if_some(area); + self.render_sparkline(inner, buf); + } +} - if spark_area.height < 1 { +impl Sparkline<'_> { + fn render_sparkline(&self, spark_area: Rect, buf: &mut Buffer) { + if spark_area.is_empty() { return; } diff --git a/src/widgets/table/cell.rs b/src/widgets/table/cell.rs index 3cc3a7612..0d05cf68d 100644 --- a/src/widgets/table/cell.rs +++ b/src/widgets/table/cell.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, widgets::Widget}; +use crate::prelude::*; /// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`]. /// diff --git a/src/widgets/table/table.rs b/src/widgets/table/table.rs index ce21e4ac1..7dd198a99 100644 --- a/src/widgets/table/table.rs +++ b/src/widgets/table/table.rs @@ -4,7 +4,7 @@ use super::*; use crate::{ layout::{Flex, SegmentSize}, prelude::*, - widgets::{Block, StatefulWidget, Widget}, + widgets::Block, }; /// A widget to display data in formatted columns. @@ -610,16 +610,32 @@ impl Widget for Table<'_> { } } +impl Widget for &Table<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + let mut state = TableState::default(); + StatefulWidget::render(self, area, buf, &mut state); + } +} + impl StatefulWidget for Table<'_> { type State = TableState; - fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { - buf.set_style(area, self.style); + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + StatefulWidget::render(&self, area, buf, state); + } +} - let table_area = self.render_block(area, buf); +impl StatefulWidget for &Table<'_> { + type State = TableState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + buf.set_style(area, self.style); + self.block.render(area, buf); + let table_area = self.block.inner_if_some(area); if table_area.is_empty() { return; } + let selection_width = self.selection_width(state); let columns_widths = self.get_columns_widths(table_area.width, selection_width); let (header_area, rows_area, footer_area) = self.layout(table_area); @@ -663,16 +679,6 @@ impl Table<'_> { (header_area, rows_area, footer_area) } - fn render_block(&mut self, area: Rect, buf: &mut Buffer) -> Rect { - if let Some(block) = self.block.take() { - let inner_area = block.inner(area); - block.render(area, buf); - inner_area - } else { - area - } - } - fn render_header(&self, area: Rect, buf: &mut Buffer, column_widths: &[(u16, u16)]) { if let Some(ref header) = self.header { buf.set_style(area, header.style); diff --git a/src/widgets/tabs.rs b/src/widgets/tabs.rs index 4546a1123..812422d00 100755 --- a/src/widgets/tabs.rs +++ b/src/widgets/tabs.rs @@ -1,8 +1,5 @@ #![deny(missing_docs)] -use crate::{ - prelude::*, - widgets::{Block, Widget}, -}; +use crate::{prelude::*, widgets::Block}; const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED); @@ -249,25 +246,30 @@ impl<'a> Styled for Tabs<'a> { } } -impl<'a> Widget for Tabs<'a> { - fn render(mut self, area: Rect, buf: &mut Buffer) { +impl Widget for Tabs<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + Widget::render(&self, area, buf); + } +} + +impl Widget for &Tabs<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { buf.set_style(area, self.style); - let tabs_area = match self.block.take() { - Some(b) => { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - } - None => area, - }; + self.block.render(area, buf); + let inner = self.block.inner_if_some(area); + self.render_tabs(inner, buf); + } +} - if tabs_area.height < 1 { +impl Tabs<'_> { + fn render_tabs(&self, tabs_area: Rect, buf: &mut Buffer) { + if tabs_area.is_empty() { return; } let mut x = tabs_area.left(); let titles_length = self.titles.len(); - for (i, title) in self.titles.into_iter().enumerate() { + for (i, title) in self.titles.iter().enumerate() { let last_title = titles_length - 1 == i; let remaining_width = tabs_area.right().saturating_sub(x); @@ -284,7 +286,7 @@ impl<'a> Widget for Tabs<'a> { } // Title - let pos = buf.set_line(x, tabs_area.top(), &title, remaining_width); + let pos = buf.set_line(x, tabs_area.top(), title, remaining_width); if i == self.selected { buf.set_style( Rect { diff --git a/tests/widgets_calendar.rs b/tests/widgets_calendar.rs index b1ba6479e..93179c027 100644 --- a/tests/widgets_calendar.rs +++ b/tests/widgets_calendar.rs @@ -11,6 +11,7 @@ use ratatui::{ }; use time::{Date, Month}; +#[track_caller] fn test_render(widget: W, expected: Buffer, size: (u16, u16)) { let backend = TestBackend::new(size.0, size.1); let mut terminal = Terminal::new(backend).unwrap(); @@ -63,7 +64,7 @@ fn show_month_header() { ) .show_month_header(Style::default()); let expected = Buffer::with_lines(vec![ - " January 2023 ", + " January 2023 ", " 1 2 3 4 5 6 7", " 8 9 10 11 12 13 14", " 15 16 17 18 19 20 21", @@ -101,7 +102,7 @@ fn show_combo() { .show_month_header(Style::default()) .show_surrounding(Style::default()); let expected = Buffer::with_lines(vec![ - " January 2023 ", + " January 2023 ", " Su Mo Tu We Th Fr Sa", " 1 2 3 4 5 6 7", " 8 9 10 11 12 13 14", diff --git a/tests/widgets_chart.rs b/tests/widgets_chart.rs index f65671e45..3ce37c429 100644 --- a/tests/widgets_chart.rs +++ b/tests/widgets_chart.rs @@ -8,6 +8,7 @@ use ratatui::{ widgets::{Axis, Block, Borders, Chart, Dataset, GraphType::Line}, Terminal, }; +use rstest::rstest; fn create_labels<'a>(labels: &'a [&'a str]) -> Vec> { labels.iter().map(|l| Span::from(*l)).collect() @@ -30,38 +31,36 @@ where terminal.backend().assert_buffer(&expected); } -#[test] -fn widgets_chart_can_render_on_small_areas() { - let test_case = |width, height| { - let backend = TestBackend::new(width, height); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|f| { - let datasets = vec![Dataset::default() - .marker(symbols::Marker::Braille) - .style(Style::default().fg(Color::Magenta)) - .data(&[(0.0, 0.0)])]; - let chart = Chart::new(datasets) - .block(Block::default().title("Plot").borders(Borders::ALL)) - .x_axis( - Axis::default() - .bounds([0.0, 0.0]) - .labels(create_labels(&["0.0", "1.0"])), - ) - .y_axis( - Axis::default() - .bounds([0.0, 0.0]) - .labels(create_labels(&["0.0", "1.0"])), - ); - f.render_widget(chart, f.size()); - }) - .unwrap(); - }; - test_case(0, 0); - test_case(0, 1); - test_case(1, 0); - test_case(1, 1); - test_case(2, 2); +#[rstest] +#[case(0, 0)] +#[case(0, 1)] +#[case(1, 0)] +#[case(1, 1)] +#[case(2, 2)] +fn widgets_chart_can_render_on_small_areas(#[case] width: u16, #[case] height: u16) { + let backend = TestBackend::new(width, height); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + let datasets = vec![Dataset::default() + .marker(symbols::Marker::Braille) + .style(Style::default().fg(Color::Magenta)) + .data(&[(0.0, 0.0)])]; + let chart = Chart::new(datasets) + .block(Block::default().title("Plot").borders(Borders::ALL)) + .x_axis( + Axis::default() + .bounds([0.0, 0.0]) + .labels(create_labels(&["0.0", "1.0"])), + ) + .y_axis( + Axis::default() + .bounds([0.0, 0.0]) + .labels(create_labels(&["0.0", "1.0"])), + ); + f.render_widget(chart, f.size()); + }) + .unwrap(); } #[test]