ratatui_core/widgets/
stateful_widget.rs

1use crate::buffer::Buffer;
2use crate::layout::Rect;
3
4/// A `StatefulWidget` is a widget that can take advantage of some local state to remember things
5/// between two draw calls.
6///
7/// For a comprehensive guide to widgets, including trait explanations, implementation patterns,
8/// and available widgets, see the [`widgets`] module documentation.
9///
10/// [`widgets`]: ../../ratatui/widgets/index.html
11///
12/// Most widgets can be drawn directly based on the input parameters. However, some features may
13/// require some kind of associated state to be implemented.
14///
15/// For example, the `List` widget can highlight the item currently selected. This can be translated
16/// in an offset, which is the number of elements to skip in order to have the selected item within
17/// the viewport currently allocated to this widget. The widget can therefore only provide the
18/// following behavior: whenever the selected item is out of the viewport scroll to a predefined
19/// position (making the selected item the last viewable item or the one in the middle for example).
20/// Nonetheless, if the widget has access to the last computed offset then it can implement a
21/// natural scrolling experience where the last offset is reused until the selected item is out of
22/// the viewport.
23///
24/// ## Examples
25///
26/// ```rust,ignore
27/// use std::io;
28///
29/// use ratatui::{
30///     backend::TestBackend,
31///     widgets::{List, ListItem, ListState, StatefulWidget, Widget},
32///     Terminal,
33/// };
34///
35/// // Let's say we have some events to display.
36/// struct Events {
37///     // `items` is the state managed by your application.
38///     items: Vec<String>,
39///     // `state` is the state that can be modified by the UI. It stores the index of the selected
40///     // item as well as the offset computed during the previous draw call (used to implement
41///     // natural scrolling).
42///     state: ListState,
43/// }
44///
45/// impl Events {
46///     fn new(items: Vec<String>) -> Events {
47///         Events {
48///             items,
49///             state: ListState::default(),
50///         }
51///     }
52///
53///     pub fn set_items(&mut self, items: Vec<String>) {
54///         self.items = items;
55///         // We reset the state as the associated items have changed. This effectively reset
56///         // the selection as well as the stored offset.
57///         self.state = ListState::default();
58///     }
59///
60///     // Select the next item. This will not be reflected until the widget is drawn in the
61///     // `Terminal::draw` callback using `Frame::render_stateful_widget`.
62///     pub fn next(&mut self) {
63///         let i = match self.state.selected() {
64///             Some(i) => {
65///                 if i >= self.items.len() - 1 {
66///                     0
67///                 } else {
68///                     i + 1
69///                 }
70///             }
71///             None => 0,
72///         };
73///         self.state.select(Some(i));
74///     }
75///
76///     // Select the previous item. This will not be reflected until the widget is drawn in the
77///     // `Terminal::draw` callback using `Frame::render_stateful_widget`.
78///     pub fn previous(&mut self) {
79///         let i = match self.state.selected() {
80///             Some(i) => {
81///                 if i == 0 {
82///                     self.items.len() - 1
83///                 } else {
84///                     i - 1
85///                 }
86///             }
87///             None => 0,
88///         };
89///         self.state.select(Some(i));
90///     }
91///
92///     // Unselect the currently selected item if any. The implementation of `ListState` makes
93///     // sure that the stored offset is also reset.
94///     pub fn unselect(&mut self) {
95///         self.state.select(None);
96///     }
97/// }
98///
99/// # let backend = TestBackend::new(5, 5);
100/// # let mut terminal = Terminal::new(backend).unwrap();
101///
102/// let mut events = Events::new(vec![String::from("Item 1"), String::from("Item 2")]);
103///
104/// loop {
105///     terminal.draw(|f| {
106///         // The items managed by the application are transformed to something
107///         // that is understood by ratatui.
108///         let items: Vec<ListItem> = events
109///             .items
110///             .iter()
111///             .map(|i| ListItem::new(i.as_str()))
112///             .collect();
113///         // The `List` widget is then built with those items.
114///         let list = List::new(items);
115///         // Finally the widget is rendered using the associated state. `events.state` is
116///         // effectively the only thing that we will "remember" from this draw call.
117///         f.render_stateful_widget(list, f.size(), &mut events.state);
118///     });
119///
120///     // In response to some input events or an external http request or whatever:
121///     events.next();
122/// }
123/// ```
124pub trait StatefulWidget {
125    /// State associated with the stateful widget.
126    ///
127    /// If you don't need this then you probably want to implement [`Widget`] instead.
128    ///
129    /// [`Widget`]: super::Widget
130    type State: ?Sized;
131    /// Draws the current state of the widget in the given buffer. That is the only method required
132    /// to implement a custom stateful widget.
133    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
134}
135
136#[cfg(test)]
137mod tests {
138    use alloc::format;
139    use alloc::string::{String, ToString};
140
141    use rstest::{fixture, rstest};
142
143    use super::*;
144    use crate::buffer::Buffer;
145    use crate::layout::Rect;
146    use crate::text::Line;
147    use crate::widgets::Widget;
148
149    #[fixture]
150    fn buf() -> Buffer {
151        Buffer::empty(Rect::new(0, 0, 20, 1))
152    }
153
154    #[fixture]
155    fn state() -> String {
156        "world".to_string()
157    }
158
159    struct PersonalGreeting;
160
161    impl StatefulWidget for PersonalGreeting {
162        type State = String;
163        fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
164            Line::from(format!("Hello {state}")).render(area, buf);
165        }
166    }
167
168    #[rstest]
169    fn render(mut buf: Buffer, mut state: String) {
170        let widget = PersonalGreeting;
171        widget.render(buf.area, &mut buf, &mut state);
172        assert_eq!(buf, Buffer::with_lines(["Hello world         "]));
173    }
174
175    struct Bytes;
176
177    /// A widget with an unsized state type.
178    impl StatefulWidget for Bytes {
179        type State = [u8];
180        fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
181            let slice = core::str::from_utf8(state).unwrap();
182            Line::from(format!("Bytes: {slice}")).render(area, buf);
183        }
184    }
185
186    #[rstest]
187    fn render_unsized_state_type(mut buf: Buffer) {
188        let widget = Bytes;
189        let state = b"hello";
190        widget.render(buf.area, &mut buf, &mut state.clone());
191        assert_eq!(buf, Buffer::with_lines(["Bytes: hello        "]));
192    }
193}