Skip to main content

gpg_tui/widget/
table.rs

1use crate::widget::row::{ScrollAmount, ScrollDirection};
2use ratatui::widgets::TableState as TuiState;
3
4/// Table size mode.
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub enum TableSize {
7	/// Normal sized table.
8	Normal,
9	/// Compact table with some rows truncated.
10	Compact,
11	/// Minimized table with all rows truncated.
12	Minimized,
13}
14
15impl TableSize {
16	/// Sets the table size to minimized.
17	pub fn set_minimized(&mut self, minimized: bool) {
18		*self = if minimized {
19			Self::Minimized
20		} else {
21			Self::Normal
22		}
23	}
24
25	/// Returns the next mode.
26	pub fn next(&self) -> Self {
27		match self {
28			Self::Normal => Self::Compact,
29			Self::Compact => Self::Minimized,
30			_ => Self::Normal,
31		}
32	}
33}
34
35/// State of the table widget.
36#[derive(Clone, Debug)]
37pub struct TableState {
38	/// State that can be modified by TUI.
39	pub tui: TuiState,
40	/// Scroll amount of the table.
41	pub scroll: ScrollAmount,
42	/// Table size.
43	pub size: TableSize,
44	/// Threshold value (width) for minimizing.
45	pub minimize_threshold: u16,
46}
47
48impl Default for TableState {
49	fn default() -> Self {
50		Self {
51			tui: TuiState::default(),
52			scroll: ScrollAmount::default(),
53			size: TableSize::Normal,
54			minimize_threshold: 90,
55		}
56	}
57}
58
59/// Table widget with TUI controlled states.
60#[derive(Clone, Debug)]
61pub struct StatefulTable<T: Clone> {
62	/// Default table items (for search functionality).
63	pub default_items: Vec<T>,
64	/// Table items.
65	pub items: Vec<T>,
66	/// Table state.
67	pub state: TableState,
68}
69
70impl<T: Clone> StatefulTable<T> {
71	/// Constructs a new instance of `StatefulTable`.
72	pub fn new(items: Vec<T>, mut state: TableState) -> StatefulTable<T> {
73		state.tui.select(Some(0));
74		Self {
75			default_items: items.clone(),
76			items,
77			state,
78		}
79	}
80
81	/// Construct a new `StatefulTable` with given items.
82	pub fn with_items(items: Vec<T>) -> StatefulTable<T> {
83		Self::new(items, TableState::default())
84	}
85
86	/// Returns the selected item.
87	pub fn selected(&self) -> Option<&T> {
88		self.items.get(self.state.tui.selected()?)
89	}
90
91	/// Selects the next item.
92	pub fn next(&mut self) {
93		let i = match self.state.tui.selected() {
94			Some(i) => {
95				if i >= self.items.len().checked_sub(1).unwrap_or(i) {
96					0
97				} else {
98					i + 1
99				}
100			}
101			None => 0,
102		};
103		self.state.tui.select(Some(i));
104		self.reset_scroll();
105	}
106
107	/// Selects the previous item.
108	pub fn previous(&mut self) {
109		let i = match self.state.tui.selected() {
110			Some(i) => {
111				if i == 0 {
112					self.items.len().checked_sub(1).unwrap_or(i)
113				} else {
114					i - 1
115				}
116			}
117			None => 0,
118		};
119		self.state.tui.select(Some(i));
120		self.reset_scroll();
121	}
122
123	/// Sets the scrolling state of the table row
124	/// depending on the given direction and offset.
125	pub fn scroll_row(&mut self, direction: ScrollDirection) {
126		match direction {
127			ScrollDirection::Up(value) => {
128				self.state.scroll.vertical =
129					self.state.scroll.vertical.saturating_sub(value);
130			}
131			ScrollDirection::Right(value) => {
132				self.state.scroll.horizontal = self
133					.state
134					.scroll
135					.horizontal
136					.checked_add(value)
137					.unwrap_or(self.state.scroll.horizontal)
138			}
139			ScrollDirection::Down(value) => {
140				self.state.scroll.vertical = self
141					.state
142					.scroll
143					.vertical
144					.checked_add(value)
145					.unwrap_or(self.state.scroll.vertical)
146			}
147			ScrollDirection::Left(value) => {
148				self.state.scroll.horizontal =
149					self.state.scroll.horizontal.saturating_sub(value);
150			}
151			_ => {}
152		}
153	}
154
155	/// Resets the items state.
156	pub fn reset_state(&mut self) {
157		self.items.clone_from(&self.default_items);
158		self.state.tui.select(Some(0));
159	}
160
161	/// Resets the scroll state.
162	pub fn reset_scroll(&mut self) {
163		self.state.scroll = ScrollAmount::default();
164	}
165}
166
167#[cfg(test)]
168mod tests {
169	use super::*;
170	use pretty_assertions::assert_eq;
171	#[test]
172	fn test_widget_table() {
173		let mut table =
174			StatefulTable::with_items(vec!["data1", "data2", "data3"]);
175		table.state.tui.select(Some(1));
176		assert_eq!(Some(&"data2"), table.selected());
177		table.next();
178		assert_eq!(Some(2), table.state.tui.selected());
179		table.previous();
180		assert_eq!(Some(1), table.state.tui.selected());
181		table.reset_scroll();
182		assert_eq!(
183			"ScrollAmount { vertical: 0, horizontal: 0 }",
184			&format!("{:?}", table.state.scroll)
185		);
186		table.scroll_row(ScrollDirection::Down(3));
187		table.scroll_row(ScrollDirection::Right(2));
188		assert_eq!(
189			"ScrollAmount { vertical: 3, horizontal: 2 }",
190			&format!("{:?}", table.state.scroll)
191		);
192		table.scroll_row(ScrollDirection::Up(1));
193		table.scroll_row(ScrollDirection::Left(1));
194		assert_eq!(
195			"ScrollAmount { vertical: 2, horizontal: 1 }",
196			&format!("{:?}", table.state.scroll)
197		);
198		table.reset_state();
199		assert_eq!(Some(0), table.state.tui.selected());
200		assert_eq!(table.default_items, table.items);
201		assert_eq!(TableSize::Normal, table.state.size);
202		table.state.size = TableSize::Minimized;
203		table.state.size.set_minimized(false);
204		assert_eq!(TableSize::Compact, table.state.size.next());
205	}
206}