Skip to main content

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}