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}