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