ratatui_widgets/list/item.rs
1use ratatui_core::style::Style;
2use ratatui_core::text::Text;
3
4/// A single item in a [`List`]
5///
6/// The item's height is defined by the number of lines it contains. This can be queried using
7/// [`ListItem::height`]. Similarly, [`ListItem::width`] will return the maximum width of all
8/// lines.
9///
10/// You can set the style of an item with [`ListItem::style`] or using the [`Stylize`] trait.
11/// This [`Style`] will be combined with the [`Style`] of the inner [`Text`]. The [`Style`]
12/// of the [`Text`] will be added to the [`Style`] of the [`ListItem`].
13///
14/// You can also align a `ListItem` by aligning its underlying [`Text`] and [`Line`]s. For that,
15/// see [`Text::alignment`] and [`Line::alignment`]. On a multiline `Text`, one `Line` can override
16/// the alignment by setting it explicitly.
17///
18/// # Examples
19///
20/// You can create [`ListItem`]s from simple `&str`
21///
22/// ```rust
23/// use ratatui::widgets::ListItem;
24/// let item = ListItem::new("Item 1");
25/// ```
26///
27/// Anything that can be converted to [`Text`] can be a [`ListItem`].
28///
29/// ```rust
30/// use ratatui::text::Line;
31/// use ratatui::widgets::ListItem;
32///
33/// let item1: ListItem = "Item 1".into();
34/// let item2: ListItem = Line::raw("Item 2").into();
35/// ```
36///
37/// A [`ListItem`] styled with [`Stylize`]
38///
39/// ```rust
40/// use ratatui::style::Stylize;
41/// use ratatui::widgets::ListItem;
42///
43/// let item = ListItem::new("Item 1").red().on_white();
44/// ```
45///
46/// If you need more control over the item's style, you can explicitly style the underlying
47/// [`Text`]
48///
49/// ```rust
50/// use ratatui::style::Stylize;
51/// use ratatui::text::{Span, Text};
52/// use ratatui::widgets::ListItem;
53///
54/// let mut text = Text::default();
55/// text.extend(["Item".blue(), Span::raw(" "), "1".bold().red()]);
56/// let item = ListItem::new(text);
57/// ```
58///
59/// A right-aligned `ListItem`
60///
61/// ```rust
62/// use ratatui::text::Text;
63/// use ratatui::widgets::ListItem;
64///
65/// ListItem::new(Text::from("foo").right_aligned());
66/// ```
67///
68/// [`List`]: crate::list::List
69/// [`Stylize`]: ratatui_core::style::Stylize
70/// [`Line`]: ratatui_core::text::Line
71/// [`Line::alignment`]: ratatui_core::text::Line::alignment
72#[derive(Debug, Clone, Eq, PartialEq, Hash)]
73pub struct ListItem<'a> {
74 pub(crate) content: Text<'a>,
75 pub(crate) style: Style,
76}
77
78impl<'a> ListItem<'a> {
79 /// Creates a new [`ListItem`]
80 ///
81 /// The `content` parameter accepts any value that can be converted into [`Text`].
82 ///
83 /// # Examples
84 ///
85 /// You can create [`ListItem`]s from simple `&str`
86 ///
87 /// ```rust
88 /// use ratatui::widgets::ListItem;
89 ///
90 /// let item = ListItem::new("Item 1");
91 /// ```
92 ///
93 /// Anything that can be converted to [`Text`] can be a [`ListItem`].
94 ///
95 /// ```rust
96 /// use ratatui::text::Line;
97 /// use ratatui::widgets::ListItem;
98 ///
99 /// let item1: ListItem = "Item 1".into();
100 /// let item2: ListItem = Line::raw("Item 2").into();
101 /// ```
102 ///
103 /// You can also create multiline items
104 ///
105 /// ```rust
106 /// use ratatui::widgets::ListItem;
107 ///
108 /// let item = ListItem::new("Multi-line\nitem");
109 /// ```
110 ///
111 /// # See also
112 ///
113 /// - [`List::new`](super::List::new) to create a list of items that can be converted to
114 /// [`ListItem`]
115 pub fn new<T>(content: T) -> Self
116 where
117 T: Into<Text<'a>>,
118 {
119 Self {
120 content: content.into(),
121 style: Style::default(),
122 }
123 }
124
125 /// Sets the item style
126 ///
127 /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
128 /// your own type that implements [`Into<Style>`]).
129 ///
130 /// This [`Style`] can be overridden by the [`Style`] of the [`Text`] content.
131 ///
132 /// This is a fluent setter method which must be chained or used as it consumes self
133 ///
134 /// # Example
135 ///
136 /// ```rust
137 /// use ratatui::style::{Style, Stylize};
138 /// use ratatui::widgets::ListItem;
139 ///
140 /// let item = ListItem::new("Item 1").style(Style::new().red().italic());
141 /// ```
142 ///
143 /// `ListItem` also implements the [`Styled`] trait, which means you can use style shorthands
144 /// from the [`Stylize`](ratatui_core::style::Stylize) trait to set the style of the widget more
145 /// concisely.
146 ///
147 /// ```rust
148 /// use ratatui::style::Stylize;
149 /// use ratatui::widgets::ListItem;
150 ///
151 /// let item = ListItem::new("Item 1").red().italic();
152 /// ```
153 ///
154 /// [`Styled`]: ratatui_core::style::Styled
155 /// [`ListState`]: crate::list::ListState
156 /// [`Color`]: ratatui_core::style::Color
157 #[must_use = "method moves the value of self and returns the modified value"]
158 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
159 self.style = style.into();
160 self
161 }
162
163 /// Returns the item height
164 ///
165 /// # Examples
166 ///
167 /// One line item
168 ///
169 /// ```rust
170 /// use ratatui::widgets::ListItem;
171 ///
172 /// let item = ListItem::new("Item 1");
173 /// assert_eq!(item.height(), 1);
174 /// ```
175 ///
176 /// Two lines item (note the `\n`)
177 ///
178 /// ```rust
179 /// use ratatui::widgets::ListItem;
180 ///
181 /// let item = ListItem::new("Multi-line\nitem");
182 /// assert_eq!(item.height(), 2);
183 /// ```
184 pub fn height(&self) -> usize {
185 self.content.height()
186 }
187
188 /// Returns the max width of all the lines
189 ///
190 /// # Examples
191 ///
192 /// ```rust
193 /// use ratatui::widgets::ListItem;
194 ///
195 /// let item = ListItem::new("12345");
196 /// assert_eq!(item.width(), 5);
197 /// ```
198 ///
199 /// ```rust
200 /// use ratatui::widgets::ListItem;
201 ///
202 /// let item = ListItem::new("12345\n1234567");
203 /// assert_eq!(item.width(), 7);
204 /// ```
205 pub fn width(&self) -> usize {
206 self.content.width()
207 }
208}
209
210impl<'a, T> From<T> for ListItem<'a>
211where
212 T: Into<Text<'a>>,
213{
214 fn from(value: T) -> Self {
215 Self::new(value)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use alloc::borrow::Cow;
222 use alloc::string::{String, ToString};
223 use alloc::vec;
224
225 use pretty_assertions::assert_eq;
226 use ratatui_core::style::{Color, Modifier, Stylize};
227 use ratatui_core::text::{Line, Span};
228
229 use super::*;
230
231 #[test]
232 fn new_from_str() {
233 let item = ListItem::new("Test item");
234 assert_eq!(item.content, Text::from("Test item"));
235 assert_eq!(item.style, Style::default());
236 }
237
238 #[test]
239 fn new_from_string() {
240 let item = ListItem::new("Test item".to_string());
241 assert_eq!(item.content, Text::from("Test item"));
242 assert_eq!(item.style, Style::default());
243 }
244
245 #[test]
246 fn new_from_cow_str() {
247 let item = ListItem::new(Cow::Borrowed("Test item"));
248 assert_eq!(item.content, Text::from("Test item"));
249 assert_eq!(item.style, Style::default());
250 }
251
252 #[test]
253 fn new_from_span() {
254 let span = Span::styled("Test item", Style::default().fg(Color::Blue));
255 let item = ListItem::new(span.clone());
256 assert_eq!(item.content, Text::from(span));
257 assert_eq!(item.style, Style::default());
258 }
259
260 #[test]
261 fn new_from_spans() {
262 let spans = Line::from(vec![
263 Span::styled("Test ", Style::default().fg(Color::Blue)),
264 Span::styled("item", Style::default().fg(Color::Red)),
265 ]);
266 let item = ListItem::new(spans.clone());
267 assert_eq!(item.content, Text::from(spans));
268 assert_eq!(item.style, Style::default());
269 }
270
271 #[test]
272 fn new_from_vec_spans() {
273 let lines = vec![
274 Line::from(vec![
275 Span::styled("Test ", Style::default().fg(Color::Blue)),
276 Span::styled("item", Style::default().fg(Color::Red)),
277 ]),
278 Line::from(vec![
279 Span::styled("Second ", Style::default().fg(Color::Green)),
280 Span::styled("line", Style::default().fg(Color::Yellow)),
281 ]),
282 ];
283 let item = ListItem::new(lines.clone());
284 assert_eq!(item.content, Text::from(lines));
285 assert_eq!(item.style, Style::default());
286 }
287
288 #[test]
289 fn str_into_list_item() {
290 let s = "Test item";
291 let item: ListItem = s.into();
292 assert_eq!(item.content, Text::from(s));
293 assert_eq!(item.style, Style::default());
294 }
295
296 #[test]
297 fn string_into_list_item() {
298 let s = String::from("Test item");
299 let item: ListItem = s.clone().into();
300 assert_eq!(item.content, Text::from(s));
301 assert_eq!(item.style, Style::default());
302 }
303
304 #[test]
305 fn span_into_list_item() {
306 let s = Span::from("Test item");
307 let item: ListItem = s.clone().into();
308 assert_eq!(item.content, Text::from(s));
309 assert_eq!(item.style, Style::default());
310 }
311
312 #[test]
313 fn vec_lines_into_list_item() {
314 let lines = vec![Line::raw("l1"), Line::raw("l2")];
315 let item: ListItem = lines.clone().into();
316 assert_eq!(item.content, Text::from(lines));
317 assert_eq!(item.style, Style::default());
318 }
319
320 #[test]
321 fn style() {
322 let item = ListItem::new("Test item").style(Style::default().bg(Color::Red));
323 assert_eq!(item.content, Text::from("Test item"));
324 assert_eq!(item.style, Style::default().bg(Color::Red));
325 }
326
327 #[test]
328 fn height() {
329 let item = ListItem::new("Test item");
330 assert_eq!(item.height(), 1);
331
332 let item = ListItem::new("Test item\nSecond line");
333 assert_eq!(item.height(), 2);
334 }
335
336 #[test]
337 fn width() {
338 let item = ListItem::new("Test item");
339 assert_eq!(item.width(), 9);
340 }
341
342 #[test]
343 fn can_be_stylized() {
344 assert_eq!(
345 ListItem::new("").black().on_white().bold().not_dim().style,
346 Style::default()
347 .fg(Color::Black)
348 .bg(Color::White)
349 .add_modifier(Modifier::BOLD)
350 .remove_modifier(Modifier::DIM)
351 );
352 }
353}