Skip to content

Commit

Permalink
feat(widgets): implement Widget for Widget refs (#833)
Browse files Browse the repository at this point in the history
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: #164
Replaces PRs: #122 and
#16
Enables: #132
Validated as a viable working solution by:
#836
  • Loading branch information
joshka authored Jan 24, 2024
1 parent 736605e commit 815757f
Show file tree
Hide file tree
Showing 29 changed files with 426 additions and 302 deletions.
2 changes: 1 addition & 1 deletion examples/colors_rgb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion examples/demo2/colors.rs
Original file line number Diff line number Diff line change
@@ -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.
///
Expand Down
2 changes: 1 addition & 1 deletion src/layout/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
10 changes: 2 additions & 8 deletions src/terminal/frame.rs
Original file line number Diff line number Diff line change
@@ -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.
///
Expand Down Expand Up @@ -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);
}

Expand Down
18 changes: 17 additions & 1 deletion src/text/line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions src/text/span.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 18 additions & 2 deletions src/text/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
54 changes: 53 additions & 1 deletion src/widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
29 changes: 11 additions & 18 deletions src/widgets/barchart.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
#![warn(missing_docs)]
use crate::prelude::*;
use crate::{prelude::*, widgets::Block};

mod bar;
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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
}
}
}
Expand Down
11 changes: 4 additions & 7 deletions src/widgets/barchart/bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 1 addition & 5 deletions src/widgets/barchart/bar_group.rs
Original file line number Diff line number Diff line change
@@ -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.
///
Expand Down
55 changes: 43 additions & 12 deletions src/widgets/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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))
}
}

Expand Down
Loading

0 comments on commit 815757f

Please sign in to comment.