Skip to content

Commit

Permalink
feat(widget)!: render functions now accept references
Browse files Browse the repository at this point in the history
In some situations, ratatui's render functions can make it difficult to
implement specific behavior or can make it difficult and inefficient to
reuse widgets. E.g. for a list, you have to clone either the vec or it's
items again each frame to make the new list object. Another example is
rendering a widget in a loop, where you have to clone the widget each
time.

This commit makes it so the `Widget`/`StatefulWidget``::render`
functions only take `self` by immutable reference, meaning they're not
consumed and can be reused.

The `Frame::render_{,stateful_}widget` functions take `widget` by value
though. To allow these functions to be called with references or values,
we added a new trait `AsWidgetRef` which is implemented for `&W` and `W`
where `W: Widget`/`StatefulWidget. This trait is then used to call the
`render` functions. Thus backwards compatibility is maintained for the
`Frame` API.

This also removes any calls to `take()` in the `render` functions since
this cannot happen when the widget is passed by reference.

BREAKING CHANGE: `Widget`/`StatefulWidget`'s `render` functions now take
`self` by immutable reference instead of by value. To update your custom
widgets simply add `&` before the `self` parameter in the `render`
function.

src/widgets/block.rs #	modified:   src/widgets/canvas/mod.rs #
modified:   src/widgets/chart.rs #	modified:   src/widgets/clear.rs #
modified:   src/widgets/gauge.rs #	modified:   src/widgets/list.rs #
modified:   src/widgets/mod.rs #	modified:   src/widgets/paragraph.rs
src/widgets/table.rs #	modified:   src/widgets/tabs.rs #
  • Loading branch information
bolshoytoster authored and joshka committed Jun 2, 2023
1 parent 358b50b commit 907bf3e
Show file tree
Hide file tree
Showing 15 changed files with 267 additions and 57 deletions.
2 changes: 1 addition & 1 deletion examples/custom_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct Label<'a> {
}

impl<'a> Widget for Label<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
fn render(&self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.left(), area.top(), self.text, Style::default());
}
}
Expand Down
78 changes: 73 additions & 5 deletions src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::{
backend::{Backend, ClearType},
buffer::Buffer,
layout::Rect,
widgets::{StatefulWidget, Widget},
widgets::{AsStatefulWidgetRef, AsWidgetRef, StatefulWidget, Widget},
};
use std::io;

Expand Down Expand Up @@ -83,11 +83,14 @@ where
/// let mut frame = terminal.get_frame();
/// frame.render_widget(block, area);
/// ```
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
pub fn render_widget<W, R>(&mut self, widget: R, area: Rect)
where
W: Widget,
R: AsWidgetRef<W>,
{
widget.render(area, self.terminal.current_buffer_mut());
widget
.as_widget_ref()
.render(area, self.terminal.current_buffer_mut());
}

/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
Expand Down Expand Up @@ -115,11 +118,14 @@ where
/// let mut frame = terminal.get_frame();
/// frame.render_stateful_widget(list, area, &mut state);
/// ```
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
pub fn render_stateful_widget<W, R>(&mut self, widget: R, area: Rect, state: &mut W::State)
where
W: StatefulWidget,
R: AsStatefulWidgetRef<W>,
{
widget.render(area, self.terminal.current_buffer_mut(), state);
widget
.as_widget_ref()
.render(area, self.terminal.current_buffer_mut(), state);
}

/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
Expand Down Expand Up @@ -479,3 +485,65 @@ fn compute_inline_size<B: Backend>(
pos,
))
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{backend::TestBackend, style::Style};

struct TestWidget(&'static str);

impl Widget for TestWidget {
fn render(&self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.left(), area.top(), self.0, Style::default());
}
}

struct TestStatefulWidget(&'static str);

impl StatefulWidget for TestStatefulWidget {
type State = u8;
fn render(&self, area: Rect, buf: &mut Buffer, state: &mut u8) {
let message = format!("{}: {state}", self.0);
*state += 1;
buf.set_string(area.left(), area.top(), message, Style::default());
}
}

#[test]
fn test_render_widget() -> io::Result<()> {
let backend = TestBackend::new(10, 6);
let mut terminal = Terminal::new(backend)?;
let mut state = 0;
terminal.draw(|frame| {
frame.render_widget(&TestWidget("borrow"), Rect::new(0, 0, 10, 1));
frame.render_widget(&TestWidget("borrow"), Rect::new(0, 1, 10, 1));
frame.render_widget(TestWidget("own"), Rect::new(0, 2, 10, 1));
frame.render_stateful_widget(
TestStatefulWidget("borrow"),
Rect::new(0, 3, 10, 1),
&mut state,
);
frame.render_stateful_widget(
TestStatefulWidget("borrow"),
Rect::new(0, 4, 10, 1),
&mut state,
);
frame.render_stateful_widget(
TestStatefulWidget("own"),
Rect::new(0, 5, 10, 1),
&mut state,
);
})?;
terminal.backend.assert_buffer(&Buffer::with_lines(vec![
"borrow ",
"borrow ",
"own ",
"borrow: 0 ",
"borrow: 1 ",
"own: 2 ",
]));
assert_eq!(state, 3);
Ok(())
}
}
4 changes: 2 additions & 2 deletions src/widgets/barchart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@ impl<'a> BarChart<'a> {
}

impl<'a> Widget for BarChart<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
fn render(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);

let chart_area = match self.block.take() {
let chart_area = match &self.block {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
Expand Down
6 changes: 3 additions & 3 deletions src/widgets/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ impl<'a> Block<'a> {
}

impl<'a> Widget for Block<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
fn render(&self, area: Rect, buf: &mut Buffer) {
if area.area() == 0 {
return;
}
Expand Down Expand Up @@ -312,7 +312,7 @@ impl<'a> Widget for Block<'a> {
}

// Title
if let Some(title) = self.title {
if let Some(title) = &self.title {
let left_border_dx = u16::from(self.borders.intersects(Borders::LEFT));
let right_border_dx = u16::from(self.borders.intersects(Borders::RIGHT));

Expand All @@ -337,7 +337,7 @@ impl<'a> Widget for Block<'a> {
area.top()
};

buf.set_line(title_x, title_y, &title, title_area_width);
buf.set_line(title_x, title_y, title, title_area_width);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/widgets/calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ impl<'a, S: DateStyler> Monthly<'a, S> {
}

impl<'a, S: DateStyler> Widget for Monthly<'a, S> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
fn render(&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() {
let mut area = match &self.block {
None => area,
Some(b) => {
let inner = b.inner(area);
Expand Down
4 changes: 2 additions & 2 deletions src/widgets/canvas/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,8 @@ impl<'a, F> Widget for Canvas<'a, F>
where
F: Fn(&mut Context),
{
fn render(mut self, area: Rect, buf: &mut Buffer) {
let canvas_area = match self.block.take() {
fn render(&self, area: Rect, buf: &mut Buffer) {
let canvas_area = match &self.block {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
Expand Down
16 changes: 8 additions & 8 deletions src/widgets/chart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ impl<'a> Chart<'a> {
}

fn render_x_labels(
&mut self,
&self,
buf: &mut Buffer,
layout: &ChartLayout,
chart_area: Rect,
Expand Down Expand Up @@ -461,7 +461,7 @@ impl<'a> Chart<'a> {
}

fn render_y_labels(
&mut self,
&self,
buf: &mut Buffer,
layout: &ChartLayout,
chart_area: Rect,
Expand All @@ -486,7 +486,7 @@ impl<'a> Chart<'a> {
}

impl<'a> Widget for Chart<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
fn render(&self, area: Rect, buf: &mut Buffer) {
if area.area() == 0 {
return;
}
Expand All @@ -496,7 +496,7 @@ impl<'a> Widget for Chart<'a> {
// axis names).
let original_style = buf.get(area.left(), area.top()).style();

let chart_area = match self.block.take() {
let chart_area = match &self.block {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
Expand Down Expand Up @@ -580,7 +580,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);
buf.set_style(
Rect {
Expand All @@ -591,11 +591,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);
buf.set_style(
Rect {
Expand All @@ -606,7 +606,7 @@ impl<'a> Widget for Chart<'a> {
},
original_style,
);
buf.set_line(x, y, &title, width);
buf.set_line(x, y, title, width);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/widgets/clear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
pub struct Clear;

impl Widget for Clear {
fn render(self, area: Rect, buf: &mut Buffer) {
fn render(&self, area: Rect, buf: &mut Buffer) {

Check warning on line 30 in src/widgets/clear.rs

View check run for this annotation

Codecov / codecov/patch

src/widgets/clear.rs#L30

Added line #L30 was not covered by tests
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
buf.get_mut(x, y).reset();
Expand Down
13 changes: 8 additions & 5 deletions src/widgets/gauge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ impl<'a> Gauge<'a> {
}

impl<'a> Widget for Gauge<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
fn render(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let gauge_area = match self.block.take() {
let gauge_area = match &self.block {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
Expand All @@ -111,7 +111,9 @@ impl<'a> Widget for Gauge<'a> {
// 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}%")))
self.label
.clone()
.unwrap_or_else(|| Span::from(format!("{pct}%")))
};
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;
Expand Down Expand Up @@ -233,9 +235,9 @@ impl<'a> LineGauge<'a> {
}

impl<'a> Widget for LineGauge<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
fn render(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let gauge_area = match self.block.take() {
let gauge_area = match &self.block {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
Expand All @@ -251,6 +253,7 @@ impl<'a> Widget for LineGauge<'a> {
let ratio = self.ratio;
let label = self
.label
.clone()
.unwrap_or_else(move || Line::from(format!("{:.0}%", ratio * 100.0)));
let (col, row) = buf.set_line(
gauge_area.left(),
Expand Down
22 changes: 11 additions & 11 deletions src/widgets/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,9 @@ impl<'a> List<'a> {
impl<'a> StatefulWidget for List<'a> {
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);
let list_area = match self.block.take() {
let list_area = match &self.block {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
Expand Down Expand Up @@ -230,7 +230,7 @@ impl<'a> StatefulWidget for List<'a> {
let has_selection = state.selected.is_some();
for (i, item) in self
.items
.iter_mut()
.iter()
.enumerate()
.skip(state.offset)
.take(end - start)
Expand Down Expand Up @@ -284,7 +284,7 @@ impl<'a> StatefulWidget for List<'a> {
}

impl<'a> Widget for List<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
fn render(&self, area: Rect, buf: &mut Buffer) {
let mut state = ListState::default();
StatefulWidget::render(self, area, buf, &mut state);
}
Expand Down Expand Up @@ -417,7 +417,7 @@ mod tests {
/// helper method to render a widget to an empty buffer with the default state
fn render_widget(widget: List<'_>, width: u16, height: u16) -> Buffer {
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
Widget::render(widget, buffer.area, &mut buffer);
Widget::render(&widget, buffer.area, &mut buffer);
buffer
}

Expand All @@ -429,7 +429,7 @@ mod tests {
height: u16,
) -> Buffer {
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
StatefulWidget::render(widget, buffer.area, &mut buffer, state);
StatefulWidget::render(&widget, buffer.area, &mut buffer, state);
buffer
}

Expand All @@ -440,19 +440,19 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));

// attempt to render into an area of the buffer with 0 width
Widget::render(list.clone(), Rect::new(0, 0, 0, 3), &mut buffer);
Widget::render(&list.clone(), Rect::new(0, 0, 0, 3), &mut buffer);
assert_buffer_eq!(buffer, Buffer::empty(buffer.area));

// attempt to render into an area of the buffer with 0 height
Widget::render(list.clone(), Rect::new(0, 0, 15, 0), &mut buffer);
Widget::render(&list.clone(), Rect::new(0, 0, 15, 0), &mut buffer);
assert_buffer_eq!(buffer, Buffer::empty(buffer.area));

let list = List::new(items)
.highlight_symbol(">>")
.block(Block::default().borders(Borders::all()));
// attempt to render into an area of the buffer with zero height after
// setting the block borders
Widget::render(list, Rect::new(0, 0, 15, 2), &mut buffer);
Widget::render(&list, Rect::new(0, 0, 15, 2), &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
Expand All @@ -469,7 +469,7 @@ mod tests {
let list = List::new(items.to_owned()).highlight_symbol(">>");
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));

Widget::render(list, buffer.area, &mut buffer);
Widget::render(&list, buffer.area, &mut buffer);

let expected = Buffer::with_lines(expected_lines);
assert_buffer_eq!(buffer, expected);
Expand All @@ -483,7 +483,7 @@ mod tests {
let mut state = ListState::default().with_selected(selected);
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));

StatefulWidget::render(list, buffer.area, &mut buffer, &mut state);
StatefulWidget::render(&list, buffer.area, &mut buffer, &mut state);

let expected = Buffer::with_lines(expected_lines);
assert_buffer_eq!(buffer, expected);
Expand Down
Loading

0 comments on commit 907bf3e

Please sign in to comment.