diff --git a/ratatui-core/src/widgets/stateful_widget.rs b/ratatui-core/src/widgets/stateful_widget.rs index b4b5df0c..aea2d6f2 100644 --- a/ratatui-core/src/widgets/stateful_widget.rs +++ b/ratatui-core/src/widgets/stateful_widget.rs @@ -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 "])); + } } diff --git a/ratatui/src/widgets/stateful_widget_ref.rs b/ratatui/src/widgets/stateful_widget_ref.rs index 35f4337a..2080179c 100644 --- a/ratatui/src/widgets/stateful_widget_ref.rs +++ b/ratatui/src/widgets/stateful_widget_ref.rs @@ -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 StatefulWidgetRef for &W +impl StatefulWidgetRef for &W where for<'a> &'a W: StatefulWidget, { @@ -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 "])); + } } diff --git a/ratatui/tests/stateful_widget_ref_dyn.rs b/ratatui/tests/stateful_widget_ref_dyn.rs new file mode 100644 index 00000000..8f307954 --- /dev/null +++ b/ratatui/tests/stateful_widget_ref_dyn.rs @@ -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 { + fn title(&self) -> &str { + type_name::() + } +} + +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::().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::().expect("window2 state"); + Line::from(format!("{}, String: {}", self.title(), state.value)).render(area, buf); + } +} + +type BoxedWindow = Box; +type BoxedState = Box>; + +#[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 ", + ]) + ); +}