1use crate::common::PageSize;
4use std::borrow::Cow;
5
6pub(crate) mod bindings;
7mod panels;
8
9pub const PAGE_SIZE_OPTIONS: &[(PageSize, &str)] = &[
10 (PageSize::Ten, "10"),
11 (PageSize::TwentyFive, "25"),
12 (PageSize::Fifty, "50"),
13 (PageSize::OneHundred, "100"),
14];
15
16#[derive(Debug, Clone, PartialEq)]
17pub struct PreferenceColumn {
18 pub id: Cow<'static, str>,
19 pub label: String,
20 pub visible: bool,
21 pub locked: bool,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub enum PreferenceRow {
26 Section(&'static str),
27 Column(PreferenceColumn),
28 Separator,
29 Radio {
30 group: &'static str,
31 value: &'static str,
32 label: &'static str,
33 selected: bool,
34 },
35 Toggle {
36 id: &'static str,
37 label: &'static str,
38 enabled: bool,
39 },
40 PageSize {
41 value: PageSize,
42 label: &'static str,
43 selected: bool,
44 },
45}
46
47#[derive(Debug, Clone, PartialEq)]
48pub enum PreferenceAction {
49 SetColumnVisibility {
50 id: Cow<'static, str>,
51 visible: bool,
52 },
53 SetPageSize(PageSize),
54 SetRadio {
55 group: &'static str,
56 value: &'static str,
57 },
58 SetToggle {
59 id: &'static str,
60 enabled: bool,
61 },
62}
63
64#[derive(Debug)]
65pub struct Preferences {
66 rows: Vec<PreferenceRow>,
67 enabled: bool,
68}
69
70impl Preferences {
71 pub fn table(columns: Vec<PreferenceColumn>, page_size: PageSize) -> Self {
72 Self::table_with_page_sizes(columns, page_size, PAGE_SIZE_OPTIONS)
73 }
74
75 pub fn table_with_page_sizes(
76 columns: Vec<PreferenceColumn>,
77 page_size: PageSize,
78 options: &[(PageSize, &'static str)],
79 ) -> Self {
80 Self::columns_only(columns, true).with_page_sizes(Some(page_size), options)
81 }
82
83 pub fn columns_only(columns: Vec<PreferenceColumn>, header: bool) -> Self {
84 let mut rows = Vec::new();
85 if header {
86 rows.push(PreferenceRow::Section("Columns"));
87 }
88 rows.extend(columns.into_iter().map(PreferenceRow::Column));
89 Self {
90 rows,
91 enabled: true,
92 }
93 }
94
95 fn section(&mut self, title: &'static str) {
96 if !self.rows.is_empty() {
97 self.rows.push(PreferenceRow::Separator);
98 }
99 self.rows.push(PreferenceRow::Section(title));
100 }
101
102 pub fn with_page_sizes(
103 mut self,
104 current: Option<PageSize>,
105 options: &[(PageSize, &'static str)],
106 ) -> Self {
107 self.section("Page size");
108 self.rows.extend(
109 options
110 .iter()
111 .map(|&(value, label)| PreferenceRow::PageSize {
112 value,
113 label,
114 selected: current == Some(value),
115 }),
116 );
117 self
118 }
119
120 pub fn radio_group(
121 mut self,
122 group: &'static str,
123 title: &'static str,
124 choices: &[(&'static str, &'static str)],
125 selected: &str,
126 ) -> Self {
127 self.section(title);
128 self.rows
129 .extend(choices.iter().map(|&(value, label)| PreferenceRow::Radio {
130 group,
131 value,
132 label,
133 selected: selected == value,
134 }));
135 self
136 }
137
138 pub fn toggle(
139 mut self,
140 id: &'static str,
141 title: &'static str,
142 label: &'static str,
143 enabled: bool,
144 ) -> Self {
145 self.section(title);
146 self.rows.push(PreferenceRow::Toggle { id, label, enabled });
147 self
148 }
149
150 pub fn read_only(mut self) -> Self {
152 self.enabled = false;
153 self
154 }
155
156 pub fn is_enabled(&self) -> bool {
157 self.enabled
158 }
159 pub fn rows(&self) -> &[PreferenceRow] {
160 &self.rows
161 }
162 pub fn max_index(&self) -> usize {
163 self.rows.len().saturating_sub(1)
164 }
165 pub fn columns(&self) -> impl Iterator<Item = &PreferenceColumn> {
166 self.rows.iter().filter_map(|row| match row {
167 PreferenceRow::Column(column) => Some(column),
168 _ => None,
169 })
170 }
171 pub fn column_count(&self) -> usize {
172 self.columns().count()
173 }
174 pub fn is_separator(&self, index: usize) -> bool {
175 matches!(self.rows.get(index), Some(PreferenceRow::Separator))
176 }
177
178 pub fn action(&self, index: usize) -> Option<PreferenceAction> {
181 if !self.enabled {
182 return None;
183 }
184 match self.rows.get(index)? {
185 PreferenceRow::Column(column) if !column.locked => {
186 if column.visible && self.columns().filter(|c| c.visible).count() <= 1 {
187 return None;
188 }
189 Some(PreferenceAction::SetColumnVisibility {
190 id: column.id.clone(),
191 visible: !column.visible,
192 })
193 }
194 PreferenceRow::PageSize { value, .. } => Some(PreferenceAction::SetPageSize(*value)),
195 PreferenceRow::Radio { group, value, .. } => {
196 Some(PreferenceAction::SetRadio { group, value })
197 }
198 PreferenceRow::Toggle { id, enabled, .. } => Some(PreferenceAction::SetToggle {
199 id,
200 enabled: !enabled,
201 }),
202 _ => None,
203 }
204 }
205
206 pub fn move_row(&self, index: usize, forward: bool) -> usize {
208 let index = index.min(self.max_index());
209 if forward {
210 (index.saturating_add(1)..self.rows.len())
211 .find(|&i| !self.is_separator(i))
212 .unwrap_or(index)
213 } else {
214 (0..index)
215 .rev()
216 .find(|&i| !self.is_separator(i))
217 .unwrap_or(index)
218 }
219 }
220
221 pub fn move_section(&self, index: usize, forward: bool) -> usize {
223 let sections: Vec<_> = self
224 .rows
225 .iter()
226 .enumerate()
227 .filter_map(|(i, row)| matches!(row, PreferenceRow::Section(_)).then_some(i))
228 .collect();
229 if sections.is_empty() {
230 return 0;
231 }
232 let current = sections.iter().rposition(|&i| i <= index).unwrap_or(0);
233 let next = if forward {
234 (current + 1) % sections.len()
235 } else {
236 (current + sections.len() - 1) % sections.len()
237 };
238 sections[next]
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 fn model(n: usize) -> Preferences {
247 Preferences::table(
248 (0..n)
249 .map(|i| PreferenceColumn {
250 id: format!("attribute-{i}").into(),
251 label: format!("Attribute {i}"),
252 visible: true,
253 locked: false,
254 })
255 .collect(),
256 PageSize::Fifty,
257 )
258 }
259
260 #[test]
261 fn every_rendered_row_has_matching_bounds_and_action() {
262 for n in [0, 1, 2, 18, 100] {
263 let prefs = model(n);
264 assert_eq!(prefs.column_count(), n);
265 assert_eq!(prefs.max_index(), prefs.rows().len() - 1);
266 assert_eq!(prefs.action(0), None);
267 assert!(prefs.is_separator(n + 1));
268 assert_eq!(prefs.action(n + 1), None);
269 assert_eq!(prefs.action(n + 2), None);
270 for (offset, &(size, _)) in PAGE_SIZE_OPTIONS.iter().enumerate() {
271 assert_eq!(
272 prefs.action(n + 3 + offset),
273 Some(PreferenceAction::SetPageSize(size))
274 );
275 }
276 assert_eq!(prefs.action(prefs.rows().len()), None);
277 assert_eq!(prefs.action(usize::MAX), None);
278 }
279 }
280
281 #[test]
282 fn navigation_uses_rows_and_sections() {
283 let prefs = model(2);
284 assert_eq!(prefs.move_row(2, true), 4);
285 assert_eq!(prefs.move_row(4, false), 2);
286 assert_eq!(prefs.move_row(0, false), 0);
287 assert_eq!(prefs.move_row(usize::MAX, true), prefs.max_index());
288 for index in 0..=prefs.max_index() {
289 let expected = if index < 4 { 4 } else { 0 };
290 assert_eq!(prefs.move_section(index, true), expected);
291 assert_eq!(prefs.move_section(index, false), expected);
292 }
293 }
294
295 #[test]
296 fn locked_and_last_visible_columns_cannot_be_hidden() {
297 let mut prefs = model(2);
298 if let PreferenceRow::Column(column) = &mut prefs.rows[1] {
299 column.locked = true;
300 }
301 assert_eq!(prefs.action(1), None);
302 assert!(matches!(
303 prefs.action(2),
304 Some(PreferenceAction::SetColumnVisibility { visible: false, .. })
305 ));
306 assert_eq!(model(1).action(1), None);
307 if let PreferenceRow::Column(column) = &mut prefs.rows[2] {
308 column.visible = false;
309 }
310 assert!(matches!(
311 prefs.action(2),
312 Some(PreferenceAction::SetColumnVisibility { visible: true, .. })
313 ));
314 let prefs = prefs.read_only();
315 assert!((0..prefs.rows.len()).all(|i| prefs.action(i).is_none()));
316 }
317}