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(&paragraph, left);
frame.render_widget(&paragraph, 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
This commit is contained in:
Josh McKinney 2024-01-24 10:34:10 -08:00 committed by GitHub
parent 736605ec88
commit 815757fcbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 429 additions and 305 deletions

View file

@ -23,7 +23,7 @@ use crossterm::{
ExecutableCommand, ExecutableCommand,
}; };
use palette::{convert::FromColorUnclamped, Okhsv, Srgb}; use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
use ratatui::{prelude::*, widgets::*}; use ratatui::prelude::*;
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct App { struct App {

View file

@ -1,5 +1,5 @@
use palette::{IntoColor, Okhsv, Srgb}; use palette::{IntoColor, Okhsv, Srgb};
use ratatui::{prelude::*, widgets::*}; use ratatui::prelude::*;
/// A widget that renders a color swatch of RGB colors. /// A widget that renders a color swatch of RGB colors.
/// ///

View file

@ -1172,7 +1172,7 @@ mod tests {
assert_buffer_eq, assert_buffer_eq,
layout::flex::Flex, layout::flex::Flex,
prelude::{Constraint::*, *}, prelude::{Constraint::*, *},
widgets::{Paragraph, Widget}, widgets::Paragraph,
}; };
/// Test that the given constraints applied to the given area result in the expected layout. /// Test that the given constraints applied to the given area result in the expected layout.

View file

@ -31,4 +31,5 @@ pub use crate::{
symbols::{self, Marker}, symbols::{self, Marker},
terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport}, terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport},
text::{self, Line, Masked, Span, Text}, text::{self, Line, Masked, Span, Text},
widgets::{block::BlockExt, StatefulWidget, Widget},
}; };

View file

@ -1,7 +1,4 @@
use crate::{ use crate::prelude::*;
prelude::*,
widgets::{StatefulWidget, Widget},
};
/// A consistent view into the terminal state for rendering a single frame. /// A consistent view into the terminal state for rendering a single frame.
/// ///
@ -74,10 +71,7 @@ impl Frame<'_> {
/// ``` /// ```
/// ///
/// [`Layout`]: crate::layout::Layout /// [`Layout`]: crate::layout::Layout
pub fn render_widget<W>(&mut self, widget: W, area: Rect) pub fn render_widget<W: Widget>(&mut self, widget: W, area: Rect) {
where
W: Widget,
{
widget.render(area, self.buffer); widget.render(area, self.buffer);
} }

View file

@ -407,6 +407,22 @@ impl<'a> From<Line<'a>> for String {
} }
impl Widget for Line<'_> { impl Widget for Line<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Widget::render(&self, area, buf);
}
}
/// Implement [`Widget`] for [`Option<Line>`] to simplify the common case of having an optional
/// [`Line`] field in a widget.
impl Widget for &Option<Line<'_>> {
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) { fn render(self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area); let area = area.intersection(buf.area);
buf.set_style(area, self.style); buf.set_style(area, self.style);
@ -418,7 +434,7 @@ impl Widget for Line<'_> {
None => 0, None => 0,
}; };
let mut x = area.left().saturating_add(offset); 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_width = span.width() as u16;
let span_area = Rect { let span_area = Rect {
x, x,

View file

@ -299,6 +299,22 @@ impl<'a> Styled for Span<'a> {
} }
impl Widget for Span<'_> { impl Widget for Span<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Widget::render(&self, area, buf);
}
}
/// Implement [`Widget`] for [`Option<Span>`] to simplify the common case of having an optional
/// [`Span`] field in a widget.
impl Widget for &Option<Span<'_>> {
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) { fn render(self, area: Rect, buf: &mut Buffer) {
let Rect { let Rect {
x: mut current_x, x: mut current_x,

View file

@ -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<Text>`] to simplify the common case of having an optional
/// [`Text`] field in a widget.
impl Widget for &Option<Text<'_>> {
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) { fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); 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 line_width = line.width() as u16;
let x_offset = match (self.alignment, line.alignment) { let x_offset = match (self.alignment, line.alignment) {

View file

@ -53,7 +53,59 @@ pub use self::{
}; };
use crate::{buffer::Buffer, layout::Rect}; 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(&paragraph, 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 { pub trait Widget {
/// Draws the current state of the widget in the given buffer. That is the only method required /// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom widget. /// to implement a custom widget.

View file

@ -1,5 +1,5 @@
#![warn(missing_docs)] #![warn(missing_docs)]
use crate::prelude::*; use crate::{prelude::*, widgets::Block};
mod bar; mod bar;
mod bar_group; mod bar_group;
@ -7,8 +7,6 @@ mod bar_group;
pub use bar::Bar; pub use bar::Bar;
pub use bar_group::BarGroup; pub use bar_group::BarGroup;
use super::{Block, Widget};
/// A chart showing values as [bars](Bar). /// A chart showing values as [bars](Bar).
/// ///
/// Here is a possible `BarChart` output. /// 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)). /// The chart can have a [`Direction`] (by default the bars are [`Vertical`](Direction::Vertical)).
/// This is set using [`BarChart::direction`]. /// 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 /// # Examples
/// ///
/// The following example creates a `BarChart` with two groups of bars. /// 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) { fn render_horizontal(self, buf: &mut Buffer, area: Rect) {
// get the longest label // get the longest label
let label_size = self let label_size = self
@ -586,19 +578,20 @@ impl<'a> BarChart<'a> {
} }
} }
impl<'a> Widget for BarChart<'a> { impl Widget for BarChart<'_> {
fn render(mut self, mut area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); 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; return;
} }
match self.direction { match self.direction {
Direction::Horizontal => self.render_horizontal(buf, area), Direction::Horizontal => self.render_horizontal(buf, inner),
Direction::Vertical => self.render_vertical(buf, area), Direction::Vertical => self.render_vertical(buf, inner),
} }
} }
} }

View file

@ -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 /// 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 /// using value_style. The second part is rendered outside the bar using bar_style
pub(super) fn render_value_with_different_styles( pub(super) fn render_value_with_different_styles(
self, &self,
buf: &mut Buffer, buf: &mut Buffer,
area: Rect, area: Rect,
bar_length: usize, bar_length: usize,
default_value_style: Style, default_value_style: Style,
bar_style: Style, bar_style: Style,
) { ) {
let text = if let Some(text) = self.text_value { let value = self.value.to_string();
text let text = self.text_value.as_ref().unwrap_or(&value);
} else {
self.value.to_string()
};
if !text.is_empty() { if !text.is_empty() {
let style = default_value_style.patch(self.value_style); 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 // 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 // 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 // render the second part with the bar_style
if text.len() > bar_length { if text.len() > bar_length {
let (first, second) = text.split_at(bar_length); let (first, second) = text.split_at(bar_length);

View file

@ -1,9 +1,5 @@
use super::Bar; use super::Bar;
use crate::{ use crate::prelude::*;
prelude::{Alignment, Buffer, Rect},
style::Style,
text::Line,
};
/// A group of bars to be shown by the Barchart. /// A group of bars to be shown by the Barchart.
/// ///

View file

@ -8,11 +8,7 @@
use strum::{Display, EnumString}; use strum::{Display, EnumString};
use crate::{ use crate::{prelude::*, symbols::border, widgets::Borders};
prelude::*,
symbols::border,
widgets::{Borders, Widget},
};
mod padding; mod padding;
pub mod title; pub mod title;
@ -520,7 +516,35 @@ impl<'a> Block<'a> {
self.padding = padding; self.padding = padding;
self self
} }
}
impl Widget for Block<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Widget::render(&self, area, buf);
}
}
/// Implement [`Widget`] for [`Option<Block>`] to simplify the common case of having an optional
/// [`Block`] field in a widget.
impl Widget for &Option<Block<'_>> {
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) { fn render_borders(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); buf.set_style(area, self.style);
let symbols = self.border_set; let symbols = self.border_set;
@ -703,13 +727,20 @@ impl<'a> Block<'a> {
} }
} }
impl<'a> Widget for Block<'a> { /// An extension trait for [`Block`] that provides some convenience methods.
fn render(self, area: Rect, buf: &mut Buffer) { ///
if area.area() == 0 { /// This is implemented for [`Option<Block>`](Option) to simplify the common case of having a
return; /// widget with an optional block.
} pub trait BlockExt {
self.render_borders(area, buf); /// Return the inner area of the block if it is `Some`. Otherwise, returns `area`.
self.render_titles(area, buf); ///
/// This is a useful convenience method for widgets that have an `Option<Block>` field
fn inner_if_some(&self, area: Rect) -> Rect;
}
impl BlockExt for Option<Block<'_>> {
fn inner_if_some(&self, area: Rect) -> Rect {
self.as_ref().map_or(area, |block| block.inner(area))
} }
} }

View file

@ -12,10 +12,7 @@ use std::collections::HashMap;
use time::{Date, Duration, OffsetDateTime}; use time::{Date, Duration, OffsetDateTime};
use crate::{ use crate::{prelude::*, widgets::Block};
prelude::*,
widgets::{Block, Widget},
};
/// Display a month calendar for the month containing `display_date` /// Display a month calendar for the month containing `display_date`
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[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> { impl<DS: DateStyler> Widget for Monthly<'_, DS> {
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
// Block is used for borders and such Widget::render(&self, area, buf);
// 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) => { impl<DS: DateStyler> Widget for &Monthly<'_, DS> {
let inner = b.inner(area); fn render(self, area: Rect, buf: &mut Buffer) {
b.render(area, buf); self.block.render(area, buf);
inner let inner = self.block.inner_if_some(area);
} self.render_monthly(inner, buf);
}; }
}
impl<DS: DateStyler> 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 // Draw the month name and year
if let Some(style) = self.show_month { if let Some(style) = self.show_month {
let line = Span::styled( Line::styled(
format!("{} {}", self.display_date.month(), self.display_date.year()), format!("{} {}", self.display_date.month(), self.display_date.year()),
style, style,
); )
// cal is 21 cells wide, so hard code the 11 .alignment(Alignment::Center)
let x_off = 11_u16.saturating_sub(line.width() as u16 / 2); .render(month_header, buf);
buf.set_line(area.x + x_off, area.y, &line.into(), area.width);
area.y += 1
} }
// Draw days of week // Draw days of week
if let Some(style) = self.show_weekday { if let Some(style) = self.show_weekday {
let days = String::from(" Su Mo Tu We Th Fr Sa"); Span::styled(" Su Mo Tu We Th Fr Sa", style).render(days_header, buf);
buf.set_string(area.x, area.y, days, style);
area.y += 1;
} }
// Set the start of the calendar to the Sunday before the 1st (or the sunday of the first) // 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 offset = Duration::days(first_of_month.weekday().number_days_from_sunday().into());
let mut curr_day = first_of_month - offset; 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. // 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 { while curr_day.month() as u8 != self.display_date.month().next() as u8 {
let mut spans = Vec::with_capacity(14); 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)); spans.push(self.format_date(curr_day));
curr_day += Duration::DAY; curr_day += Duration::DAY;
} }
buf.set_line(area.x, area.y, &spans.into(), area.width); buf.set_line(days_area.x, y, &spans.into(), area.width);
area.y += 1; y += 1;
} }
} }
} }

View file

@ -16,14 +16,7 @@ pub use self::{
points::Points, points::Points,
rectangle::Rectangle, rectangle::Rectangle,
}; };
use crate::{ use crate::{prelude::*, symbols, text::Line as TextLine, widgets::Block};
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols,
text::Line as TextLine,
widgets::{Block, Widget},
};
/// Interface for all shapes that may be drawn on a Canvas widget. /// Interface for all shapes that may be drawn on a Canvas widget.
pub trait Shape { pub trait Shape {
@ -694,19 +687,25 @@ where
} }
} }
impl<'a, F> Widget for Canvas<'a, F> impl<F> Widget for Canvas<'_, F>
where where
F: Fn(&mut Context), F: Fn(&mut Context),
{ {
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let canvas_area = match self.block.take() { Widget::render(&self, area, buf);
Some(b) => { }
let inner_area = b.inner(area); }
b.render(area, buf);
inner_area impl<F> Widget for &Canvas<'_, F>
} where
None => area, 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)); buf.set_style(canvas_area, Style::default().bg(self.background_color));

View file

@ -108,11 +108,7 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::Line; use super::Line;
use crate::{ use crate::{assert_buffer_eq, prelude::*, widgets::canvas::Canvas};
assert_buffer_eq,
prelude::*,
widgets::{canvas::Canvas, Widget},
};
#[track_caller] #[track_caller]
fn test(line: Line, expected_lines: Vec<&str>) { fn test(line: Line, expected_lines: Vec<&str>) {

View file

@ -46,11 +46,7 @@ mod tests {
use strum::ParseError; use strum::ParseError;
use super::*; use super::*;
use crate::{ use crate::{assert_buffer_eq, prelude::*, widgets::canvas::Canvas};
assert_buffer_eq,
prelude::*,
widgets::{canvas::Canvas, Widget},
};
#[test] #[test]
fn map_resolution_to_string() { fn map_resolution_to_string() {

View file

@ -54,11 +54,7 @@ impl Shape for Rectangle {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{ use crate::{assert_buffer_eq, prelude::*, widgets::canvas::Canvas};
assert_buffer_eq,
prelude::*,
widgets::{canvas::Canvas, Widget},
};
#[test] #[test]
fn draw_block_lines() { fn draw_block_lines() {

View file

@ -4,14 +4,14 @@ use std::cmp::max;
use strum::{Display, EnumString}; use strum::{Display, EnumString};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use super::block::BlockExt;
use crate::{ use crate::{
buffer::Buffer,
layout::Flex, layout::Flex,
prelude::*, prelude::*,
symbols, symbols,
widgets::{ widgets::{
canvas::{Canvas, Line as CanvasLine, Points}, canvas::{Canvas, Line as CanvasLine, Points},
Block, Borders, Widget, Block, Borders,
}, },
}; };
@ -809,7 +809,7 @@ impl<'a> Chart<'a> {
} }
fn render_x_labels( fn render_x_labels(
&mut self, &self,
buf: &mut Buffer, buf: &mut Buffer,
layout: &ChartLayout, layout: &ChartLayout,
chart_area: Rect, chart_area: Rect,
@ -892,7 +892,7 @@ impl<'a> Chart<'a> {
} }
fn render_y_labels( fn render_y_labels(
&mut self, &self,
buf: &mut Buffer, buf: &mut Buffer,
layout: &ChartLayout, layout: &ChartLayout,
chart_area: Rect, chart_area: Rect,
@ -916,26 +916,27 @@ impl<'a> Chart<'a> {
} }
} }
impl<'a> Widget for Chart<'a> { impl Widget for Chart<'_> {
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
if area.area() == 0 { 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; return;
} }
buf.set_style(area, self.style);
// Sample the style of the entire widget. This sample will be used to reset the style of // 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 // the cells that are part of the components put on top of the grah area (i.e legend and
// axis names). // axis names).
let original_style = buf.get(area.left(), area.top()).style(); 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 layout = self.layout(chart_area);
let graph_area = layout.graph_area; let graph_area = layout.graph_area;
if graph_area.width < 1 || graph_area.height < 1 { 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 { 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 let width = graph_area
.right() .right()
.saturating_sub(x) .saturating_sub(x)
@ -1010,11 +1011,11 @@ impl<'a> Widget for Chart<'a> {
}, },
original_style, original_style,
); );
buf.set_line(x, y, &title, width); buf.set_line(x, y, title, width);
} }
if let Some((x, y)) = layout.title_y { 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 let width = graph_area
.right() .right()
.saturating_sub(x) .saturating_sub(x)
@ -1028,7 +1029,7 @@ impl<'a> Widget for Chart<'a> {
}, },
original_style, original_style,
); );
buf.set_line(x, y, &title, width); buf.set_line(x, y, title, width);
} }
if let Some(legend_area) = layout.legend_area { if let Some(legend_area) = layout.legend_area {

View file

@ -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). /// 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; pub struct Clear;
impl Widget for 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) { fn render(self, area: Rect, buf: &mut Buffer) {
for x in area.left()..area.right() { for x in area.left()..area.right() {
for y in area.top()..area.bottom() { for y in area.top()..area.bottom() {

View file

@ -1,12 +1,6 @@
#![deny(missing_docs)] #![deny(missing_docs)]
use crate::{
buffer::Buffer, use crate::{prelude::*, widgets::Block};
layout::Rect,
style::{Color, Style, Styled},
symbols,
text::{Line, Span},
widgets::{Block, Widget},
};
/// A widget to display a progress bar. /// A widget to display a progress bar.
/// ///
@ -161,28 +155,33 @@ impl<'a> Gauge<'a> {
} }
} }
impl<'a> Widget for Gauge<'a> { impl Widget for Gauge<'_> {
fn render(mut self, area: Rect, buf: &mut Buffer) { 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); buf.set_style(area, self.style);
let gauge_area = match self.block.take() { self.block.render(area, buf);
Some(b) => { let inner = self.block.inner_if_some(area);
let inner_area = b.inner(area); self.render_gague(inner, buf);
b.render(area, buf); }
inner_area }
}
None => area, impl Gauge<'_> {
}; fn render_gague(&self, gauge_area: Rect, buf: &mut Buffer) {
buf.set_style(gauge_area, self.gauge_style); if gauge_area.is_empty() {
if gauge_area.height < 1 {
return; return;
} }
buf.set_style(gauge_area, self.gauge_style);
// compute label value and its position // compute label value and its position
// label is put at the center of the gauge_area // label is put at the center of the gauge_area
let label = { let default_label = Span::raw(format!("{}%", f64::round(self.ratio * 100.0)));
let pct = f64::round(self.ratio * 100.0); let label = self.label.as_ref().unwrap_or(&default_label);
self.label.unwrap_or_else(|| Span::from(format!("{pct}%")))
};
let clamped_label_width = gauge_area.width.min(label.width() as u16); 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_col = gauge_area.left() + (gauge_area.width - clamped_label_width) / 2;
let label_row = gauge_area.top() + gauge_area.height / 2; let label_row = gauge_area.top() + gauge_area.height / 2;
@ -217,7 +216,7 @@ impl<'a> Widget for Gauge<'a> {
} }
} }
// render the label // 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> { impl Widget for LineGauge<'_> {
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); Widget::render(&self, area, buf);
let gauge_area = match self.block.take() { }
Some(b) => { }
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
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; return;
} }
let ratio = self.ratio; let ratio = self.ratio;
let label = self let default_label = Line::from(format!("{:.0}%", ratio * 100.0));
.label let label = self.label.as_ref().unwrap_or(&default_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 (col, row) = buf.set_line(
gauge_area.left(),
gauge_area.top(),
&label,
gauge_area.width,
);
let start = col + 1; let start = col + 1;
if start >= gauge_area.right() { if start >= gauge_area.right() {
return; return;

View file

@ -4,7 +4,7 @@ use unicode_width::UnicodeWidthStr;
use crate::{ use crate::{
prelude::*, prelude::*,
widgets::{Block, HighlightSpacing, StatefulWidget, Widget}, widgets::{Block, HighlightSpacing},
}; };
/// State of the [`List`] widget /// 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; 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) {
buf.set_style(area, self.style); StatefulWidget::render(&self, area, buf, state);
let list_area = match self.block.take() { }
Some(b) => { }
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
if self.items.is_empty() || list_area.is_empty() { 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);
self.block.render(area, buf);
let list_area = self.block.inner_if_some(area);
if list_area.is_empty() || self.items.is_empty() {
return; return;
} }
@ -846,7 +862,7 @@ impl<'a> StatefulWidget for List<'a> {
let selection_spacing = self.highlight_spacing.should_add(state.selected.is_some()); let selection_spacing = self.highlight_spacing.should_add(state.selected.is_some());
for (i, item) in self for (i, item) in self
.items .items
.iter_mut() .iter()
.enumerate() .enumerate()
.skip(state.offset) .skip(state.offset)
.take(last_visible_index - first_visible_index) .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> { impl<'a> Styled for List<'a> {
type Item = List<'a>; type Item = List<'a>;
@ -961,7 +970,7 @@ mod tests {
prelude::Alignment, prelude::Alignment,
style::{Color, Modifier, Stylize}, style::{Color, Modifier, Stylize},
text::{Line, Span}, text::{Line, Span},
widgets::{Borders, StatefulWidget, Widget}, widgets::Borders,
}; };
#[test] #[test]

View file

@ -1,12 +1,10 @@
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use super::block::BlockExt;
use crate::{ use crate::{
prelude::*, prelude::*,
text::StyledGrapheme, text::StyledGrapheme,
widgets::{ widgets::{reflow::*, Block},
reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine},
Block, Widget,
},
}; };
fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 { 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> { impl Widget for Paragraph<'_> {
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); Widget::render(&self, area, buf);
let text_area = match self.block.take() { }
Some(b) => { }
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
if text_area.height < 1 { impl Widget for &Paragraph<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
self.block.render(area, buf);
let inner = self.block.inner_if_some(area);
self.render_paragraph(inner, buf);
}
}
impl Paragraph<'_> {
fn render_paragraph(&self, text_area: Rect, buf: &mut Buffer) {
if text_area.is_empty() {
return; return;
} }

View file

@ -3,10 +3,7 @@ use std::cmp::min;
use strum::{Display, EnumString}; use strum::{Display, EnumString};
use crate::{ use crate::{prelude::*, widgets::Block};
prelude::*,
widgets::{Block, Widget},
};
/// Widget to render a sparkline over one or more lines. /// 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> { impl Widget for Sparkline<'_> {
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let spark_area = match self.block.take() { Widget::render(&self, area, buf);
Some(b) => { }
let inner_area = b.inner(area); }
b.render(area, buf);
inner_area
}
None => area,
};
if spark_area.height < 1 { 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);
}
}
impl Sparkline<'_> {
fn render_sparkline(&self, spark_area: Rect, buf: &mut Buffer) {
if spark_area.is_empty() {
return; return;
} }

View file

@ -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`]. /// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
/// ///

View file

@ -4,7 +4,7 @@ use super::*;
use crate::{ use crate::{
layout::{Flex, SegmentSize}, layout::{Flex, SegmentSize},
prelude::*, prelude::*,
widgets::{Block, StatefulWidget, Widget}, widgets::Block,
}; };
/// A widget to display data in formatted columns. /// 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<'_> { impl StatefulWidget for Table<'_> {
type State = TableState; type State = TableState;
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) {
buf.set_style(area, self.style); 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() { if table_area.is_empty() {
return; return;
} }
let selection_width = self.selection_width(state); let selection_width = self.selection_width(state);
let columns_widths = self.get_columns_widths(table_area.width, selection_width); let columns_widths = self.get_columns_widths(table_area.width, selection_width);
let (header_area, rows_area, footer_area) = self.layout(table_area); let (header_area, rows_area, footer_area) = self.layout(table_area);
@ -663,16 +679,6 @@ impl Table<'_> {
(header_area, rows_area, footer_area) (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)]) { fn render_header(&self, area: Rect, buf: &mut Buffer, column_widths: &[(u16, u16)]) {
if let Some(ref header) = self.header { if let Some(ref header) = self.header {
buf.set_style(area, header.style); buf.set_style(area, header.style);

View file

@ -1,8 +1,5 @@
#![deny(missing_docs)] #![deny(missing_docs)]
use crate::{ use crate::{prelude::*, widgets::Block};
prelude::*,
widgets::{Block, Widget},
};
const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED); 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> { impl Widget for Tabs<'_> {
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); Widget::render(&self, area, buf);
let tabs_area = match self.block.take() { }
Some(b) => { }
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
if tabs_area.height < 1 { impl Widget for &Tabs<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
self.block.render(area, buf);
let inner = self.block.inner_if_some(area);
self.render_tabs(inner, buf);
}
}
impl Tabs<'_> {
fn render_tabs(&self, tabs_area: Rect, buf: &mut Buffer) {
if tabs_area.is_empty() {
return; return;
} }
let mut x = tabs_area.left(); let mut x = tabs_area.left();
let titles_length = self.titles.len(); 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 last_title = titles_length - 1 == i;
let remaining_width = tabs_area.right().saturating_sub(x); let remaining_width = tabs_area.right().saturating_sub(x);
@ -284,7 +286,7 @@ impl<'a> Widget for Tabs<'a> {
} }
// Title // 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 { if i == self.selected {
buf.set_style( buf.set_style(
Rect { Rect {

View file

@ -11,6 +11,7 @@ use ratatui::{
}; };
use time::{Date, Month}; use time::{Date, Month};
#[track_caller]
fn test_render<W: Widget>(widget: W, expected: Buffer, size: (u16, u16)) { fn test_render<W: Widget>(widget: W, expected: Buffer, size: (u16, u16)) {
let backend = TestBackend::new(size.0, size.1); let backend = TestBackend::new(size.0, size.1);
let mut terminal = Terminal::new(backend).unwrap(); let mut terminal = Terminal::new(backend).unwrap();
@ -63,7 +64,7 @@ fn show_month_header() {
) )
.show_month_header(Style::default()); .show_month_header(Style::default());
let expected = Buffer::with_lines(vec![ let expected = Buffer::with_lines(vec![
" January 2023 ", " January 2023 ",
" 1 2 3 4 5 6 7", " 1 2 3 4 5 6 7",
" 8 9 10 11 12 13 14", " 8 9 10 11 12 13 14",
" 15 16 17 18 19 20 21", " 15 16 17 18 19 20 21",
@ -101,7 +102,7 @@ fn show_combo() {
.show_month_header(Style::default()) .show_month_header(Style::default())
.show_surrounding(Style::default()); .show_surrounding(Style::default());
let expected = Buffer::with_lines(vec![ let expected = Buffer::with_lines(vec![
" January 2023 ", " January 2023 ",
" Su Mo Tu We Th Fr Sa", " Su Mo Tu We Th Fr Sa",
" 1 2 3 4 5 6 7", " 1 2 3 4 5 6 7",
" 8 9 10 11 12 13 14", " 8 9 10 11 12 13 14",

View file

@ -8,6 +8,7 @@ use ratatui::{
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType::Line}, widgets::{Axis, Block, Borders, Chart, Dataset, GraphType::Line},
Terminal, Terminal,
}; };
use rstest::rstest;
fn create_labels<'a>(labels: &'a [&'a str]) -> Vec<Span<'a>> { fn create_labels<'a>(labels: &'a [&'a str]) -> Vec<Span<'a>> {
labels.iter().map(|l| Span::from(*l)).collect() labels.iter().map(|l| Span::from(*l)).collect()
@ -30,38 +31,36 @@ where
terminal.backend().assert_buffer(&expected); terminal.backend().assert_buffer(&expected);
} }
#[test] #[rstest]
fn widgets_chart_can_render_on_small_areas() { #[case(0, 0)]
let test_case = |width, height| { #[case(0, 1)]
let backend = TestBackend::new(width, height); #[case(1, 0)]
let mut terminal = Terminal::new(backend).unwrap(); #[case(1, 1)]
terminal #[case(2, 2)]
.draw(|f| { fn widgets_chart_can_render_on_small_areas(#[case] width: u16, #[case] height: u16) {
let datasets = vec![Dataset::default() let backend = TestBackend::new(width, height);
.marker(symbols::Marker::Braille) let mut terminal = Terminal::new(backend).unwrap();
.style(Style::default().fg(Color::Magenta)) terminal
.data(&[(0.0, 0.0)])]; .draw(|f| {
let chart = Chart::new(datasets) let datasets = vec![Dataset::default()
.block(Block::default().title("Plot").borders(Borders::ALL)) .marker(symbols::Marker::Braille)
.x_axis( .style(Style::default().fg(Color::Magenta))
Axis::default() .data(&[(0.0, 0.0)])];
.bounds([0.0, 0.0]) let chart = Chart::new(datasets)
.labels(create_labels(&["0.0", "1.0"])), .block(Block::default().title("Plot").borders(Borders::ALL))
) .x_axis(
.y_axis( Axis::default()
Axis::default() .bounds([0.0, 0.0])
.bounds([0.0, 0.0]) .labels(create_labels(&["0.0", "1.0"])),
.labels(create_labels(&["0.0", "1.0"])), )
); .y_axis(
f.render_widget(chart, f.size()); Axis::default()
}) .bounds([0.0, 0.0])
.unwrap(); .labels(create_labels(&["0.0", "1.0"])),
}; );
test_case(0, 0); f.render_widget(chart, f.size());
test_case(0, 1); })
test_case(1, 0); .unwrap();
test_case(1, 1);
test_case(2, 2);
} }
#[test] #[test]