fix: Make StatefulWidget and Ref work with unsized State (#1505)

StatefulWidget::State and StatefulWidgetRef::State are now ?Sized.

This allows implementations of the traits to use unsized types for the
State associated type. This is turn is useful when doing things like
boxing different stateful widget types with State which implements
`Any`, are slices or any other dynamically sized type.

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
This commit is contained in:
thscharler 2024-11-20 02:56:26 +01:00 committed by GitHub
parent e4e95bcecf
commit 4d7704fba5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 147 additions and 3 deletions

View file

@ -121,7 +121,7 @@ pub trait StatefulWidget {
/// If you don't need this then you probably want to implement [`Widget`] instead.
///
/// [`Widget`]: super::Widget
type State;
type State: ?Sized;
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom stateful widget.
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
@ -159,4 +159,23 @@ mod tests {
widget.render(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
}
struct Bytes;
/// A widget with an unsized state type.
impl StatefulWidget for Bytes {
type State = [u8];
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let slice = std::str::from_utf8(state).unwrap();
Line::from(format!("Bytes: {slice}")).render(area, buf);
}
}
#[rstest]
fn render_unsized_state_type(mut buf: Buffer) {
let widget = Bytes;
let state = b"hello";
widget.render(buf.area, &mut buf, &mut state.clone());
assert_eq!(buf, Buffer::with_lines(["Bytes: hello "]));
}
}

View file

@ -61,7 +61,7 @@ pub trait StatefulWidgetRef {
/// If you don't need this then you probably want to implement [`WidgetRef`] instead.
///
/// [`WidgetRef`]: super::WidgetRef
type State;
type State: ?Sized;
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom stateful widget.
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
@ -70,7 +70,7 @@ pub trait StatefulWidgetRef {
/// Blanket implementation of `StatefulWidgetRef` for `&W` where `W` implements `StatefulWidget`.
///
/// This allows you to render a stateful widget by reference.
impl<W, State> StatefulWidgetRef for &W
impl<W, State: ?Sized> StatefulWidgetRef for &W
where
for<'a> &'a W: StatefulWidget<State = State>,
{
@ -119,4 +119,38 @@ mod tests {
widget.render_ref(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
}
#[rstest]
fn render_stateful_widget_ref_with_unsized_state(mut buf: Buffer) {
struct Bytes;
impl StatefulWidgetRef for Bytes {
type State = [u8];
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let slice = std::str::from_utf8(state).unwrap();
Line::from(format!("Bytes: {slice}")).render(area, buf);
}
}
let widget = Bytes;
let state = b"hello";
widget.render_ref(buf.area, &mut buf, &mut state.clone());
assert_eq!(buf, Buffer::with_lines(["Bytes: hello "]));
}
#[rstest]
fn render_stateful_widget_with_unsized_state(mut buf: Buffer) {
struct Bytes;
impl StatefulWidget for &Bytes {
type State = [u8];
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let slice = std::str::from_utf8(state).unwrap();
Line::from(format!("Bytes: {slice}")).render(area, buf);
}
}
let widget = &Bytes;
let mut state = b"hello".to_owned();
let state = state.as_mut_slice();
widget.render_ref(buf.area, &mut buf, state);
assert_eq!(buf, Buffer::with_lines(["Bytes: hello "]));
}
}

View file

@ -0,0 +1,91 @@
#![cfg(feature = "unstable-widget-ref")]
use std::{
any::{type_name, Any},
cell::RefCell,
};
use pretty_assertions::assert_eq;
use ratatui::widgets::StatefulWidgetRef;
use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
trait AnyWindow: StatefulWidgetRef<State = dyn Any> {
fn title(&self) -> &str {
type_name::<Self>()
}
}
struct Window1;
struct Window1State {
pub value: u32,
}
impl AnyWindow for Window1 {}
impl StatefulWidgetRef for Window1 {
type State = dyn Any;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let state = state.downcast_mut::<Window1State>().expect("window1 state");
Line::from(format!("{}, u32: {}", self.title(), state.value)).render(area, buf);
}
}
struct Window2;
struct Window2State {
pub value: String,
}
impl AnyWindow for Window2 {}
impl StatefulWidgetRef for Window2 {
type State = dyn Any;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let state = state.downcast_mut::<Window2State>().expect("window2 state");
Line::from(format!("{}, String: {}", self.title(), state.value)).render(area, buf);
}
}
type BoxedWindow = Box<dyn AnyWindow>;
type BoxedState = Box<RefCell<dyn Any>>;
#[test]
fn render_dyn_widgets() {
let windows: Vec<(BoxedWindow, BoxedState)> = vec![
(
Box::new(Window1),
Box::new(RefCell::new(Window1State { value: 32 })),
),
(
Box::new(Window2),
Box::new(RefCell::new(Window2State {
value: "Some".to_string(),
})),
),
(
Box::new(Window1),
Box::new(RefCell::new(Window1State { value: 42 })),
),
];
let mut buf = Buffer::empty(Rect::new(0, 0, 50, 3));
let mut area = Rect::new(0, 0, 50, 1);
for (w, s) in &windows {
let mut s = s.borrow_mut();
w.render_ref(area, &mut buf, &mut *s);
area.y += 1;
}
assert_eq!(
buf,
Buffer::with_lines([
"stateful_widget_ref_dyn::Window1, u32: 32 ",
"stateful_widget_ref_dyn::Window2, String: Some ",
"stateful_widget_ref_dyn::Window1, u32: 42 ",
])
);
}