feat(widgets): Rename StatefulWidget::render to render_stateful

This change renames the `StatefulWidget::render` method to
`render_stateful` to avoid conflicts with the `Widget::render` method.

Often both the `Widget` and `StatefulWidget` traits are in scope, and
when calling the `render` method on a `StatefulWidget` the compiler
cannot determine which trait to use. This change resolves that issue by
renaming the `StatefulWidget::render` method to `render_stateful`.

The `StatefulWidget::render` method is still available, but it is
deprecated. A default implementation of `render_stateful` is provided
that calls the deprecated `render` method.

Callers should update their code to use `render_stateful` instead of
`render`.
implementors of the `StatefulWidget` trait should update their
implementations to implement `render_stateful` instead of `render`, and
provide an implementation of `render` that calls `render_stateful`.

This change is non-breaking. The deprecated `render` method will be
removed in a future release of Ratatui (likely 0.29.0).

Addresses part of a problem raised in
<https://github.com/ratatui-org/ratatui/issues/996>
This commit is contained in:
Josh McKinney 2024-06-16 17:14:53 -07:00
parent 4bfdc15b80
commit fa8001f21d
No known key found for this signature in database
GPG key ID: 722287396A903BC5
12 changed files with 132 additions and 64 deletions

View file

@ -63,7 +63,7 @@ fn render_stateful(bencher: &mut Bencher, list: &List, mut state: ListState) {
bencher.iter_batched(
|| list.to_owned(),
|bench_list| {
StatefulWidget::render(bench_list, buffer.area, &mut buffer, &mut state);
bench_list.render_stateful(buffer.area, &mut buffer, &mut state);
},
BatchSize::LargeInput,
);

View file

@ -256,7 +256,8 @@ impl App {
if scrollbar_needed {
let mut state = ScrollbarState::new(self.max_scroll_offset as usize)
.position(self.scroll_offset as usize);
Scrollbar::new(ScrollbarOrientation::VerticalRight).render(area, buf, &mut state);
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.render_stateful(area, buf, &mut state);
}
}
}

View file

@ -104,15 +104,12 @@ fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
})
.collect_vec();
let mut state = ListState::default().with_selected(Some(selected_index));
StatefulWidget::render(
List::new(items)
.style(theme.inbox)
.highlight_style(theme.selected_item)
.highlight_symbol(highlight_symbol),
inbox,
buf,
&mut state,
);
List::new(items)
.style(theme.inbox)
.highlight_style(theme.selected_item)
.highlight_symbol(highlight_symbol)
.render_stateful(inbox, buf, &mut state);
let mut scrollbar_state = ScrollbarState::default()
.content_length(EMAILS.len())
.position(selected_index);
@ -121,7 +118,7 @@ fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
.end_symbol(None)
.track_symbol(None)
.thumb_symbol("")
.render(inbox, buf, &mut scrollbar_state);
.render_stateful(inbox, buf, &mut scrollbar_state);
}
fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {

View file

@ -160,15 +160,12 @@ fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default().with_selected(Some(selected_row));
let rows = INGREDIENTS.iter().copied();
let theme = THEME.recipe;
StatefulWidget::render(
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
.block(Block::new().style(theme.ingredients))
.header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
.highlight_style(Style::new().light_yellow()),
area,
buf,
&mut state,
);
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
.block(Block::new().style(theme.ingredients))
.header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
.highlight_style(Style::new().light_yellow())
.render_stateful(area, buf, &mut state);
}
fn render_scrollbar(position: usize, area: Rect, buf: &mut Buffer) {
@ -181,5 +178,5 @@ fn render_scrollbar(position: usize, area: Rect, buf: &mut Buffer) {
.end_symbol(None)
.track_symbol(None)
.thumb_symbol("")
.render(area, buf, &mut state);
.render_stateful(area, buf, &mut state);
}

View file

@ -60,15 +60,12 @@ fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
.padding(Padding::new(1, 1, 1, 1))
.title_alignment(Alignment::Center)
.title("Traceroute bad.horse".bold().white());
StatefulWidget::render(
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
.highlight_style(THEME.traceroute.selected)
.block(block),
area,
buf,
&mut state,
);
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
.highlight_style(THEME.traceroute.selected)
.block(block)
.render_stateful(area, buf, &mut state);
let mut scrollbar_state = ScrollbarState::default()
.content_length(HOPS.len())
.position(selected_row);
@ -84,7 +81,7 @@ fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
.end_symbol(None)
.track_symbol(None)
.thumb_symbol("")
.render(area, buf, &mut scrollbar_state);
.render_stateful(area, buf, &mut scrollbar_state);
}
pub fn render_ping(progress: usize, area: Rect, buf: &mut Buffer) {

View file

@ -331,7 +331,7 @@ impl App {
let mut spacing = self.spacing;
self.selected_tab
.render(content_area, &mut demo_buf, &mut spacing);
.render_stateful(content_area, &mut demo_buf, &mut spacing);
let visible_content = demo_buf
.content
@ -348,7 +348,8 @@ impl App {
let area = area.intersection(buf.area);
let mut state = ScrollbarState::new(max_scroll_offset() as usize)
.position(self.scroll_offset as usize);
Scrollbar::new(ScrollbarOrientation::VerticalRight).render(area, buf, &mut state);
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.render_stateful(area, buf, &mut state);
}
scrollbar_needed
}
@ -387,8 +388,15 @@ impl SelectedTab {
impl StatefulWidget for SelectedTab {
type State = u16;
fn render(self, area: Rect, buf: &mut Buffer, spacing: &mut Self::State) {
let spacing = *spacing;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
self.render_stateful(area, buf, state);
}
fn render_stateful(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where
Self: Sized,
{
let spacing = *state;
match self {
Self::Legacy => Self::render_examples(area, buf, Flex::Legacy, spacing),
Self::Start => Self::render_examples(area, buf, Flex::Start, spacing),

View file

@ -249,9 +249,7 @@ impl App {
.highlight_spacing(HighlightSpacing::Always);
// We can now render the item list
// (look careful we are using StatefulWidget's render.)
// ratatui::widgets::StatefulWidget::render as stateful_render
StatefulWidget::render(items, inner_area, buf, &mut self.items.state);
items.render_stateful(inner_area, buf, &mut self.items.state);
}
fn render_info(&self, area: Rect, buf: &mut Buffer) {

View file

@ -99,7 +99,7 @@ impl Frame<'_> {
widget.render_ref(area, self.buffer);
}
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render_stateful`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
@ -125,7 +125,7 @@ impl Frame<'_> {
where
W: StatefulWidget,
{
widget.render(area, self.buffer, state);
widget.render_stateful(area, self.buffer, state);
}
/// Render a [`StatefulWidgetRef`] to the current buffer using

View file

@ -221,9 +221,43 @@ pub trait StatefulWidget {
///
/// If you don't need this then you probably want to implement [`Widget`] instead.
type State;
/// Renders the the widget into the buffer using the provided state.
///
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom stateful widget.
///
/// When both `Widget` and `StatefulWidget` are in scope, this method conflicts with the
/// `render` method from the `Widget` trait. Prior to Ratatui 0.27.0, this conflict caused
/// apps to have to qualify the method call when using the `StatefulWidget` trait. To avoid
/// this, the `render` method is deprecated and replaced with a new method called
/// `render_stateful`. This new method does not conflict with the `render` method from the
/// `Widget` trait.
///
/// This method will be removed in a future release (likely Ratatui 0.29.0). Callers should
/// update their code to use the `render_stateful` method instead. Widget implementors may
/// either:
/// - Implement the `render` method, and change the name of the method to `render_stateful` when
/// the `render` method is removed. A default implementation of `render_stateful` is provided
/// that calls `render`.
/// - Implement the `render_stateful` method directly and add a temporary implementation of
/// `render` that calls `render_stateful` until the `render` method is removed.
#[deprecated(note = "Use `render_stateful` instead")]
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
/// Renders the the widget into the buffer using the provided state.
///
/// This method replaces the `render` method with a name that does not conflict with the
/// `render` method from the `Widget` trait. (This conflict causes apps to have to specifially
/// disambiguate the method call when using the `StatefulWidget` trait in situations where both
/// traits are in scope.)
fn render_stateful(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where
Self: Sized,
{
#[allow(deprecated)]
self.render(area, buf, state);
}
}
/// A `WidgetRef` is a trait that allows rendering a widget by reference.
@ -387,6 +421,13 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
/// impl StatefulWidget for PersonalGreeting {
/// type State = String;
/// fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
/// self.render_stateful(area, buf, state);
/// }
///
/// fn render_stateful(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
/// where
/// Self: Sized,
/// {
/// (&self).render_ref(area, buf, state);
/// }
/// }
@ -571,7 +612,7 @@ mod tests {
#[rstest]
fn render(mut buf: Buffer, mut state: String) {
let widget = PersonalGreeting;
widget.render(buf.area, &mut buf, &mut state);
widget.render_stateful(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
}
}

View file

@ -862,6 +862,13 @@ impl StatefulWidget for List<'_> {
type State = ListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
self.render_stateful(area, buf, state);
}
fn render_stateful(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where
Self: Sized,
{
StatefulWidgetRef::render_ref(&self, area, buf, state);
}
}
@ -869,7 +876,15 @@ impl StatefulWidget for List<'_> {
// Note: remove this when StatefulWidgetRef is stabilized and replace with the blanket impl
impl StatefulWidget for &List<'_> {
type State = ListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
self.render_stateful(area, buf, state);
}
fn render_stateful(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where
Self: Sized,
{
StatefulWidgetRef::render_ref(self, area, buf, state);
}
}
@ -1162,7 +1177,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);
widget.render_stateful(buffer.area, &mut buffer, state);
buffer
}
@ -1222,7 +1237,7 @@ mod tests {
let list = List::new(items.to_owned()).highlight_symbol(">>");
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);
list.render_stateful(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(expected));
}
@ -2121,7 +2136,7 @@ mod tests {
let list = List::new([item]).highlight_symbol(highlight_symbol);
let mut state = ListState::default();
state.select(Some(0));
StatefulWidget::render(list, single_line_buf.area, &mut single_line_buf, &mut state);
list.render_stateful(single_line_buf.area, &mut single_line_buf, &mut state);
assert_eq!(single_line_buf, Buffer::with_lines([expected]));
}
}

View file

@ -680,7 +680,7 @@ mod tests {
) {
let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
scrollbar_no_arrows.render_stateful(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
@ -703,7 +703,7 @@ mod tests {
) {
let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
scrollbar_no_arrows.render_stateful(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
@ -718,7 +718,7 @@ mod tests {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
scrollbar_no_arrows.render_stateful(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
@ -735,7 +735,7 @@ mod tests {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
scrollbar_no_arrows.render_stateful(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
@ -751,7 +751,7 @@ mod tests {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
scrollbar_no_arrows.render_stateful(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
@ -778,7 +778,7 @@ mod tests {
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
.render_stateful(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
@ -806,7 +806,7 @@ mod tests {
.track_symbol(None)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
.render_stateful(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
@ -837,7 +837,7 @@ mod tests {
.track_symbol(None)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
.render_stateful(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
@ -868,7 +868,7 @@ mod tests {
.end_symbol(Some(">"))
.track_symbol(Some("-"))
.thumb_symbol("#")
.render(buffer.area, &mut buffer, &mut state);
.render_stateful(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
@ -895,7 +895,7 @@ mod tests {
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
.render_stateful(buffer.area, &mut buffer, &mut state);
let empty_string = " ".repeat(size as usize);
assert_eq!(buffer, Buffer::with_lines([&empty_string, expected]));
}
@ -923,7 +923,7 @@ mod tests {
Scrollbar::new(ScrollbarOrientation::HorizontalTop)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
.render_stateful(buffer.area, &mut buffer, &mut state);
let empty_string = " ".repeat(size as usize);
assert_eq!(buffer, Buffer::with_lines([expected, &empty_string]));
}
@ -953,7 +953,7 @@ mod tests {
.end_symbol(Some(">"))
.track_symbol(Some("-"))
.thumb_symbol("#")
.render(buffer.area, &mut buffer, &mut state);
.render_stateful(buffer.area, &mut buffer, &mut state);
let bar = expected.chars().map(|c| format!("{c} "));
assert_eq!(buffer, Buffer::with_lines(bar));
}
@ -983,7 +983,7 @@ mod tests {
.end_symbol(Some(">"))
.track_symbol(Some("-"))
.thumb_symbol("#")
.render(buffer.area, &mut buffer, &mut state);
.render_stateful(buffer.area, &mut buffer, &mut state);
let bar = expected.chars().map(|c| format!(" {c}"));
assert_eq!(buffer, Buffer::with_lines(bar));
}
@ -1011,7 +1011,7 @@ mod tests {
let mut state = ScrollbarState::new(content_length)
.position(position)
.viewport_content_length(2);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
scrollbar_no_arrows.render_stateful(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
@ -1040,7 +1040,7 @@ mod tests {
let mut state = ScrollbarState::new(content_length)
.position(position)
.viewport_content_length(2);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
scrollbar_no_arrows.render_stateful(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
}

View file

@ -580,7 +580,7 @@ impl Widget for Table<'_> {
impl WidgetRef for Table<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default();
StatefulWidget::render(self, area, buf, &mut state);
self.render_stateful(area, buf, &mut state);
}
}
@ -588,7 +588,14 @@ impl StatefulWidget for Table<'_> {
type State = TableState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
StatefulWidget::render(&self, area, buf, state);
self.render_stateful(area, buf, state);
}
fn render_stateful(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where
Self: Sized,
{
(&self).render_stateful(area, buf, state);
}
}
@ -596,6 +603,13 @@ impl StatefulWidget for Table<'_> {
impl StatefulWidget for &Table<'_> {
type State = TableState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
self.render_stateful(area, buf, state);
}
fn render_stateful(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where
Self: Sized,
{
StatefulWidgetRef::render_ref(self, area, buf, state);
}
}
@ -1189,7 +1203,7 @@ mod tests {
.highlight_style(Style::new().red())
.highlight_symbol(">>");
let mut state = TableState::new().with_selected(0);
StatefulWidget::render(table, Rect::new(0, 0, 15, 3), &mut buf, &mut state);
table.render_stateful(Rect::new(0, 0, 15, 3), &mut buf, &mut state);
let expected = Buffer::with_lines([
">>Cell1 Cell2 ".red(),
" Cell3 Cell4 ".into(),
@ -1414,7 +1428,7 @@ mod tests {
let area = Rect::new(0, 0, columns, 3);
let mut buf = Buffer::empty(area);
let mut state = TableState::default().with_selected(selection);
StatefulWidget::render(table, area, &mut buf, &mut state);
table.render_stateful(area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(expected));
}