tui_additions/framework/
state.rs

1use super::{FrameworkError, FrameworkItem};
2use ratatui::layout::{Constraint, Direction, Layout, Rect};
3
4/// Contains an item
5#[derive(Clone)]
6pub struct RowItem {
7    /// The actual item Boxed
8    pub item: Box<dyn FrameworkItem>,
9    /// Width of the item
10    pub width: Constraint,
11}
12
13/// Contains a row of objects
14#[derive(Clone)]
15pub struct Row {
16    /// All the items in the row
17    pub items: Vec<RowItem>,
18    /// If the row should be centered or not
19    pub centered: bool,
20    /// The height of the row
21    pub height: Constraint,
22}
23
24/// Contains the items and the layout of the TUI
25#[derive(Clone)]
26pub struct State(pub Vec<Row>);
27
28impl State {
29    /// Generate selectables which is a 2D vector of items that can be selected
30    ///
31    /// `(usize, usize)` maps to the `(x, y)` position to `State.0`, items that are not selectable
32    /// are excluded
33    pub fn selectables(&self) -> Vec<Vec<(usize, usize)>> {
34        let mut selectables = Vec::new();
35
36        self.0.iter().enumerate().for_each(|(y, row)| {
37            let mut row_selectables = Vec::new();
38            row.items.iter().enumerate().for_each(|(x, row_item)| {
39                if row_item.item.selectable() {
40                    row_selectables.push((x, y));
41                }
42            });
43            if !row_selectables.is_empty() {
44                selectables.push(row_selectables);
45            }
46        });
47
48        selectables
49    }
50
51    /// Return chunks as 2D array of rects
52    pub fn get_chunks(&self, area: Rect) -> Vec<Vec<Rect>> {
53        // chunks
54        let mut row_constraints = vec![Constraint::Length(0)];
55        row_constraints.extend(self.0.iter().map(|row| row.height));
56        row_constraints.push(Constraint::Length(0));
57
58        let row_constraints_length = row_constraints.len() - 2;
59
60        Layout::default()
61            .direction(Direction::Vertical)
62            .constraints(row_constraints)
63            .split(area)
64            .iter()
65            .skip(1)
66            .take(row_constraints_length)
67            .zip(self.0.iter().map(|row| {
68                let begin_length = if row.centered {
69                    Constraint::Length(
70                        (area.width
71                            - row
72                                .items
73                                .iter()
74                                .map(|item| item.width.apply(area.width))
75                                .sum::<u16>())
76                            / 2,
77                    )
78                } else {
79                    Constraint::Length(0)
80                };
81
82                let mut out = vec![begin_length];
83                out.extend(row.items.iter().map(|item| item.width));
84                out.push(Constraint::Length(0));
85                out
86            }))
87            .map(|(row_chunk, constraints)| {
88                let constraints_length = constraints.len() - 2;
89
90                Layout::default()
91                    .direction(Direction::Horizontal)
92                    .constraints(constraints)
93                    .split(*row_chunk)
94                    .iter()
95                    .skip(1)
96                    .take(constraints_length)
97                    .copied()
98                    .collect()
99            })
100            .collect::<Vec<_>>()
101    }
102
103    /// Get reference to item with x and y value
104    pub fn get(&self, x: usize, y: usize) -> &dyn FrameworkItem {
105        &*self.0[y].items[x].item
106    }
107
108    /// Get mutable reference to item with x and y value
109    pub fn get_mut(&mut self, x: usize, y: usize) -> &mut Box<dyn FrameworkItem> {
110        &mut self.0[y].items[x].item
111    }
112}
113
114/// State of cursor
115///
116/// The 2 numbers represent the x and y in `Framework.selectables` rather than `State.0`
117#[derive(Clone, Copy, PartialEq, Eq)]
118pub enum CursorState {
119    /// Nothing is selected
120    None,
121    /// Cursor is hovering on an item
122    Hover(usize, usize),
123    /// An item is selected
124    Selected(usize, usize),
125}
126
127impl Default for CursorState {
128    fn default() -> Self {
129        Self::None
130    }
131}
132
133impl CursorState {
134    pub fn is_selected(&self) -> bool {
135        matches!(self, Self::Selected(_, _))
136    }
137
138    pub fn is_hover(&self) -> bool {
139        matches!(self, Self::Hover(_, _))
140    }
141
142    pub fn is_none(&self) -> bool {
143        self == &Self::None
144    }
145}
146
147impl CursorState {
148    /// Try select an item, will not work if an item is already selected or the cursor is not
149    /// hovering on anything
150    pub fn select(&mut self) -> Result<(), FrameworkError> {
151        match self {
152            Self::Hover(x, y) => *self = Self::Selected(*x, *y),
153            _ => return Err(FrameworkError::CursorStateMismatch),
154        }
155
156        Ok(())
157    }
158
159    /// Try deselect an item, will not work if no items are selected
160    pub fn deselect(&mut self) -> Result<(), FrameworkError> {
161        match self {
162            Self::Selected(x, y) => *self = Self::Hover(*x, *y),
163            _ => return Err(FrameworkError::CursorStateMismatch),
164        }
165
166        Ok(())
167    }
168}
169
170impl CursorState {
171    pub fn to_hover(location: (usize, usize)) -> Self {
172        Self::Hover(location.0, location.1)
173    }
174
175    pub fn to_selected(location: (usize, usize)) -> Self {
176        Self::Selected(location.0, location.1)
177    }
178
179    pub fn hover(&self, selectables: &[Vec<(usize, usize)>]) -> Option<(usize, usize)> {
180        match self {
181            Self::Hover(x, y) if !selectables.is_empty() => {
182                Some(Self::selectables_to_coors(selectables, (*x, *y)))
183            }
184            _ => None,
185        }
186    }
187
188    pub fn selected(&self, selectables: &[Vec<(usize, usize)>]) -> Option<(usize, usize)> {
189        match self {
190            Self::Selected(x, y) if !selectables.is_empty() => {
191                Some(Self::selectables_to_coors(selectables, (*x, *y)))
192            }
193            _ => None,
194        }
195    }
196
197    fn selectables_to_coors(
198        selectables: &[Vec<(usize, usize)>],
199        location: (usize, usize),
200    ) -> (usize, usize) {
201        let (location_x, location_y) = location;
202
203        selectables[location_y][location_x]
204    }
205}
206
207impl CursorState {
208    /// Move in the corresponding direction
209    pub fn r#move(
210        &mut self,
211        direction: FrameworkDirection,
212        selectables: &[Vec<(usize, usize)>],
213    ) -> Result<(), FrameworkError> {
214        match direction {
215            FrameworkDirection::Up => self.up(),
216            FrameworkDirection::Down => self.down(),
217            FrameworkDirection::Left => self.left(),
218            FrameworkDirection::Right => self.right(),
219        }?;
220
221        self.move_check(selectables);
222
223        Ok(())
224    }
225
226    fn move_check(&mut self, selectables: &[Vec<(usize, usize)>]) {
227        if let Self::Hover(x, y) = self {
228            if selectables.is_empty() {
229                *x = 0;
230                *y = 0;
231                return;
232            }
233            let y_max = selectables.len() - 1;
234            if *y > y_max {
235                *y = y_max;
236            }
237
238            let x_max = selectables[*y].len() - 1;
239            if *x > x_max {
240                *x = x_max;
241            }
242        } else {
243            unreachable!("move_check is only called after a hovering cursor is moved, when cursor is at hover state")
244        }
245    }
246
247    fn left(&mut self) -> Result<(), FrameworkError> {
248        match self {
249            Self::Hover(x, _) => {
250                if *x != 0 {
251                    *x -= 1
252                }
253            }
254            Self::None => *self = Self::Hover(0, 0),
255            Self::Selected(_, _) => return Err(FrameworkError::MoveSelected),
256        }
257
258        Ok(())
259    }
260
261    fn right(&mut self) -> Result<(), FrameworkError> {
262        match self {
263            Self::Hover(x, _) => *x += 1,
264            Self::None => *self = Self::Hover(usize::MAX, 0),
265            Self::Selected(_, _) => return Err(FrameworkError::MoveSelected),
266        }
267
268        Ok(())
269    }
270
271    fn up(&mut self) -> Result<(), FrameworkError> {
272        match self {
273            Self::Hover(_, y) => {
274                if *y != 0 {
275                    *y -= 1
276                }
277            }
278            Self::None => *self = Self::Hover(0, 0),
279            Self::Selected(_, _) => return Err(FrameworkError::MoveSelected),
280        }
281
282        Ok(())
283    }
284
285    fn down(&mut self) -> Result<(), FrameworkError> {
286        match self {
287            Self::Hover(_, y) => *y += 1,
288            Self::None => *self = Self::Hover(0, usize::MAX),
289            Self::Selected(_, _) => return Err(FrameworkError::MoveSelected),
290        }
291
292        Ok(())
293    }
294}
295
296/// Used to represent direction in this crate
297#[derive(Clone, Copy)]
298pub enum FrameworkDirection {
299    Up,
300    Down,
301    Left,
302    Right,
303}
304
305/// Passed into the `FrameworkItem` trait functions for info of the item
306#[derive(Clone, Copy)]
307pub struct ItemInfo {
308    pub selected: bool,
309    pub hover: bool,
310    pub x: usize,
311    pub y: usize,
312}