feather_tui/list.rs
1use crate::{
2 components::{Header, Text, TextFlags}, error::{FtuiError, FtuiResult},
3 util::id::IdGenerator
4};
5
6#[doc = "⚠️ **Experimental** ⚠️"]
7/// Another variant of a `Container` designed to display data in a vertical
8/// list format. A `List` is scrollable, allowing it to handle a dynamic number
9/// of elements. It can be created using the `ListBuilder`, and new elements can
10/// be added using the `add` method.
11///
12/// # Usage
13/// Use `List` to present information in a vertically ordered list.
14///
15/// `1. Item one`
16/// `2. Item two`
17/// `3. Item three`
18pub struct List {
19 header: Option<Header>,
20 elements: Vec<Text>,
21 offset: usize,
22 default_flags: Option<TextFlags>,
23 number: bool,
24 id_generator: IdGenerator<u16>,
25}
26
27impl List {
28 /// Constructs a new `List`.
29 ///
30 /// # Returns
31 /// `List`: A new instance of `List`.
32 ///
33 /// # Example
34 /// ```rust
35 /// let _ = List::new();
36 /// ```
37 pub(crate) fn new() -> Self {
38 List {
39 header: None,
40 elements: vec![],
41 offset: 0,
42 default_flags: None,
43 number: false,
44 id_generator: IdGenerator::new(),
45 }
46 }
47
48 /// Adds a new element to the `List`.
49 ///
50 /// # Parameters
51 /// - `label`: A `&str` representing the element label.
52 /// - `flags`: A set of `TextFlags` combined using the bitwise OR operator.
53 ///
54 /// # Notes
55 /// - The bitwise OR operator combines flags like this: `flag1 | flag2 | flag3`
56 /// - A `List` element is just a `Text` component.
57 ///
58 /// # Returns
59 /// - `Ok(u16)`: Return the ID of the added element.
60 /// - `Err(FtuiError)`: Returns an error.
61 ///
62 /// # Example
63 /// ```rust
64 /// // Create a new `List`.
65 /// let mut list = ListBuilder::new().build();
66 ///
67 /// // Add an element labeled "Element" with red text and bold styling.
68 /// list.add("Element", TextFlags::COLOR_RED | TextFlags::STYLE_BOLD)?;
69 /// ```
70 pub fn add(
71 &mut self, label: &str, flags: impl Into<Option<TextFlags>>
72 ) -> FtuiResult<u16> {
73 let flags: Option<TextFlags> = flags.into();
74
75 let id = self.id_generator.get_id();
76
77 match flags {
78 Some(flags) if flags.contains(TextFlags::ALIGN_BOTTOM) =>
79 return Err(FtuiError::TextFlagAlignBottomWithListElement),
80 Some(flags) => self.elements.push(Text::with_id(label, flags, id)?),
81 None => self.elements.push(Text::with_id(label, self.default_flags, id)?),
82 }
83
84 Ok(id)
85 }
86
87 /// Attempts to scroll the `List` up by one position.
88 ///
89 /// # Returns
90 /// - `true` if the list was successfully scrolled up.
91 /// - `false`: The `List` fail to scroll up (already at the top).
92 ///
93 /// # Example
94 /// ```rust
95 /// // Create a new `List`.
96 /// let mut list = ListBuilder::new().build();
97 ///
98 /// // Add two elements to the list.
99 /// list.add(...)?;
100 /// list.add(...)?;
101 ///
102 /// // Initially, the list is at the bottom after scrolling down.
103 /// list.scroll_down();
104 ///
105 /// // Now it can scroll back up.
106 /// assert_eq!(list.scroll_up(), true);
107 /// ```
108 pub fn scroll_up(&mut self) -> bool {
109 if self.offset != 0 {
110 self.offset -= 1;
111 true
112 } else {
113 false
114 }
115 }
116
117 /// Attempts to scroll the `List` down by one position.
118 ///
119 /// # Returns
120 /// - `true` if the list was successfully scrolled down.
121 /// - `false`: The `List` fail to scroll down (already at the bottom).
122 ///
123 /// # Example
124 /// ```rust
125 /// // Create a new `List`.
126 /// let mut list = ListBuilder::new().build();
127 ///
128 /// // Add two elements to the list.
129 /// list.add(...)?;
130 /// list.add(...)?;
131 ///
132 /// // The list can scroll down since it's not at the bottom yet.
133 /// assert_eq!(list.scroll_down(), true);
134 /// ```
135 pub fn scroll_down(&mut self) -> bool {
136 if self.offset < self.elements.len() - 1 {
137 self.offset += 1;
138 true
139 } else {
140 false
141 }
142 }
143
144 /// Finds the index of an element by its ID.
145 ///
146 /// # Parameters
147 /// - `id`: The ID of the `Text` component to search for.
148 ///
149 /// # Returns
150 /// - `Ok(usize)`: The index of the element with the specified ID.
151 /// - `Err(FtuiError)`: Return an error.
152 ///
153 /// # Example
154 /// ```rust
155 /// // Create a new `List`.
156 /// let mut list = ListBuilder::new().build();
157 ///
158 /// // Add an element to the list and retrieve its ID.
159 /// let id = list.add(...)?;
160 ///
161 /// // Find the index of the element by its ID.
162 /// let index = list.find(id)?;
163 /// ```
164 pub fn find(&self, id: u16) -> FtuiResult<usize> {
165 self.elements
166 .iter()
167 .position(|element| element.id() == id)
168 .ok_or(FtuiError::ListNoElementById)
169 }
170
171 /// Returns a reference to the element at the given index, if it exists.
172 ///
173 /// # Parameters
174 /// - `i`: The index of the element to retrieve.
175 ///
176 /// # Returns
177 /// - `Ok(&Text)`: A reference to the element at the specified index.
178 /// - `Err(FtuiError)`: Returns an error.
179 ///
180 /// # Example
181 /// ```rust
182 /// // Create a new `List`.
183 /// let mut list = ListBuilder::new().build();
184 ///
185 /// // Add elements to the list.
186 /// list.add(...)?;
187 /// list.add(...)?;
188 ///
189 /// // Access the first element.
190 /// list.at(0)?;
191 /// ```
192 pub fn at(&self, i: usize) -> FtuiResult<&Text> {
193 if i < self.elements.len() {
194 Ok(&self.elements[i])
195 } else {
196 Err(FtuiError::ListIndexOutOfBound)
197 }
198 }
199
200 /// Removes the element at the specified index, if it exists.
201 ///
202 /// # Parameters
203 /// - `i`: The index of the element to remove.
204 ///
205 /// # Returns
206 /// - `Ok(())`: If the element was successfully removed.
207 /// - `Err(FtuiError)`: If the index is out of bounds.
208 ///
209 /// # Example
210 /// ```rust
211 /// // Create a new `List`.
212 /// let mut list = ListBuilder::new().build();
213 ///
214 /// // Add elements to the list.
215 /// list.add(...)?;
216 /// list.add(...)?;
217 ///
218 /// // Remove the first element from the list.
219 /// list.remove(0)?;
220 /// ```
221 pub fn remove(&mut self, i: usize) -> FtuiResult<()> {
222 if i < self.elements.len() {
223 self.elements.remove(i);
224 Ok(())
225 } else {
226 Err(FtuiError::ListIndexOutOfBound)
227 }
228 }
229
230 pub(crate) fn header(&self) -> &Option<Header> {
231 &self.header
232 }
233
234 pub(crate) fn elements_mut(&mut self) -> &mut [Text] {
235 &mut self.elements
236 }
237
238 pub(crate) fn offset(&self) -> usize {
239 self.offset
240 }
241
242 pub(crate) fn len(&self) -> usize {
243 self.elements.len()
244 }
245
246 pub(crate) fn is_number(&self) -> bool {
247 self.number
248 }
249}
250
251/// `ListBuilder` is used to create `List` instances using the builder pattern.
252/// This allows for a flexible and readable way to construct `List` with different
253/// options by chaining method calls.
254///
255/// # Example
256/// ```rust
257/// ListBuilder::new()
258/// .header(...)?
259/// .default_flags(...)?
260/// .number()
261/// .build();
262/// ```
263pub struct ListBuilder {
264 list: List,
265}
266
267impl ListBuilder {
268 /// Constructs a new `ListBuilder`.
269 ///
270 /// # Return
271 /// `ListBuilder`: A new instance of `ListBuilder`.
272 ///
273 /// # Example
274 /// ```rust
275 /// let _ = ListBuilder::new();
276 /// ```
277 pub fn new() -> Self {
278 ListBuilder { list: List::new(), }
279 }
280
281 /// Explicitly sets a `Header` component for the `List`. Unlike the `header`
282 /// method, which takes a label and internally constructs a `Header`, this
283 /// method allows you to directly provide a preconstructed `Header` component.
284 ///
285 /// # Parameters
286 /// - `header`: A `Header` component.
287 ///
288 /// # Returns
289 /// - `ListBuilder`: Returns `self`.
290 ///
291 /// # Example
292 /// ```rust
293 /// // Create a `Header` component.
294 /// let header = Header::new(...)?;
295 ///
296 /// // Set a preconstructed `Header` component.
297 /// ListBuilder::new()
298 /// .header_expl(header);
299 /// ```
300 pub fn header_expl(mut self, header: Header) -> Self {
301 self.list.header = Some(header);
302 self
303 }
304
305 /// Sets a `Header` component for the `List`.
306 ///
307 /// # Parameters
308 /// - `label`: A `&str` representing the text to display in the header.
309 ///
310 /// # Returns
311 /// - `Ok(ListBuilder)`: Returns `self`.
312 /// - `Err(FtuiError)`: Returns an error.
313 ///
314 /// # Example
315 /// ```rust
316 /// // Sets a `Header` component with the label "Welcome".
317 /// List::new()
318 /// .header("Welcome")?;
319 /// ```
320 #[inline]
321 pub fn header(self, label: &str) -> FtuiResult<Self> {
322 Ok(self.header_expl(Header::new(label)?))
323 }
324
325 /// Sets the default `TextFlags` to be used when adding elements to the `List`.
326 ///
327 /// # Parameters
328 /// - `flags`: The `TextFlags` to apply to elements unless explicitly overridden.
329 ///
330 /// # Returns
331 /// - `Ok(ListBuilder)`: Returns `self`.
332 /// - `Err(FtuiError)`: Returns an error.
333 ///
334 /// # Example
335 /// ```rust
336 /// // Set a default red color for all elements added to the list, unless overridden.
337 /// ListBuilder::new()
338 /// .default_flags(tui::TextFlags::COLOR_RED)?;
339 /// ```
340 pub fn default_flags(mut self, flags: TextFlags) -> FtuiResult<Self> {
341 Text::ensure_compatible_flags(&flags)?;
342 self.list.default_flags = Some(flags);
343 Ok(self)
344 }
345
346 /// Enables numbering for the `List`, adding a number prefix to each element.
347 ///
348 /// # Returns
349 /// - `Self`: Returns `self`.
350 ///
351 /// # Example
352 /// ```rust
353 /// ListBuilder::new()
354 /// .number();
355 /// ```
356 pub fn number(mut self) -> Self {
357 self.list.number = true;
358 self
359 }
360
361 /// Finalizes the construction of a `List`. This method should be called
362 /// after all desired options have been set using the builder pattern.
363 /// It consumes `self` and returns the completed `List`.
364 ///
365 /// # Returns
366 /// - `List`: Returns the created `List`.
367 ///
368 /// # Example
369 /// ```rust
370 /// ListBuilder::new()
371 /// .header(...)?
372 /// .default_flags(...)?
373 /// .number()
374 /// .build(); // Finalize and retrieve the constructed list.
375 /// ```
376 pub fn build(self) -> List {
377 self.list
378 }
379}