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,
};
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
use ratatui::{prelude::*, widgets::*};
use ratatui::prelude::*;
#[derive(Debug, Default)]
struct App {

View file

@ -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.
///

View file

@ -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.

View file

@ -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},
};

View file

@ -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<W>(&mut self, widget: W, area: Rect)
where
W: Widget,
{
pub fn render_widget<W: Widget>(&mut self, widget: W, area: Rect) {
widget.render(area, self.buffer);
}

View file

@ -407,6 +407,22 @@ impl<'a> From<Line<'a>> for String {
}
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) {
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,

View file

@ -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<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) {
let Rect {
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) {
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) {

View file

@ -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(&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 {
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom widget.

View file

@ -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),
}
}
}

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
/// 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);

View file

@ -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.
///

View file

@ -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<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) {
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<Block>`](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<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 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<DS: DateStyler> Widget for Monthly<'_, DS> {
fn render(self, area: Rect, buf: &mut Buffer) {
Widget::render(&self, area, buf);
}
}
impl<DS: DateStyler> 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<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
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;
}
}
}

View file

@ -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<F> 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<F> 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));

View file

@ -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>) {

View file

@ -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() {

View file

@ -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() {

View file

@ -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 {

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).
///
@ -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() {

View file

@ -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;

View file

@ -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) {
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,
};
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
StatefulWidget::render(&self, area, buf, state);
}
}
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;
}
@ -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]

View file

@ -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) {
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,
};
impl Widget for Paragraph<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Widget::render(&self, area, buf);
}
}
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;
}

View file

@ -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);
}
}
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;
}

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`].
///

View file

@ -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);

View file

@ -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) {
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,
};
impl Widget for Tabs<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Widget::render(&self, area, buf);
}
}
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;
}
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 {

View file

@ -11,6 +11,7 @@ use ratatui::{
};
use time::{Date, Month};
#[track_caller]
fn test_render<W: Widget>(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",

View file

@ -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<Span<'a>> {
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]