Skip to main content

datui_lib/app/modals/
sort_modal.rs

1use crate::widgets::column_widths::WidthChoice;
2use crate::widgets::text_input::TextInput;
3use ratatui::widgets::TableState;
4
5#[derive(Debug, Clone)]
6pub struct SortColumn {
7    pub name: String,
8    pub sort_order: Option<usize>, // For sorting (which columns to sort by and in what order)
9    /// This column's own direction, meaningful while `sort_order` is set. Each column
10    /// of a multi-sort runs its own way.
11    pub sort_descending: bool,
12    pub display_order: usize,  // For column display order
13    pub is_locked: bool,       // Whether this column is locked (and all columns before it)
14    pub is_to_be_locked: bool, // Whether this column is to-be-locked (pending, shown as dim lock)
15    pub is_visible: bool,      // Whether this column is visible in the table
16    /// How the column's width is chosen, as staged.
17    pub width: WidthChoice,
18    /// The width the table last drew the column at, where narrower and wider start.
19    pub shown_width: Option<u16>,
20}
21
22pub struct SortModal {
23    pub filter_input: TextInput,
24    pub columns: Vec<SortColumn>,
25    pub table_state: TableState,
26    pub has_unapplied_changes: bool,
27    pub history_limit: usize,
28    /// Why the last key did nothing, for the sidebar's status line; the next key
29    /// clears it.
30    pub status: Option<String>,
31    /// The column order last applied, hidden columns included. The table keeps only
32    /// the visible order, so this is where a hidden column is listed, and returns
33    /// to, when the sidebar reopens.
34    pub applied_order: Vec<String>,
35    /// How many leading columns of `applied_order` were frozen, hidden ones included:
36    /// the table's count leaves out a hidden column that ended the span.
37    pub applied_locked: usize,
38    /// Rows the Columns list showed when last drawn: what PgUp and PgDn move.
39    pub page_rows: usize,
40    /// The Columns list as the find shows it, and a fingerprint of the find text and
41    /// the columns it was worked out for: see [`Self::filtered_columns`].
42    shown: std::sync::Mutex<Option<(u64, Vec<usize>)>>,
43    /// Times the list was worked out, for a test that a key reuses it.
44    #[cfg(test)]
45    shown_builds: std::sync::atomic::AtomicUsize,
46}
47
48impl Default for SortModal {
49    fn default() -> Self {
50        Self {
51            filter_input: TextInput::new(),
52            columns: Vec::new(),
53            table_state: TableState::default(),
54            has_unapplied_changes: false,
55            history_limit: 1000,
56            status: None,
57            applied_order: Vec::new(),
58            applied_locked: 0,
59            page_rows: 10,
60            shown: Default::default(),
61            #[cfg(test)]
62            shown_builds: Default::default(),
63        }
64    }
65}
66
67impl SortModal {
68    /// The columns the find text matches, in display order. Asked several times a key,
69    /// so recomputed only when a fingerprint of the find text, names and order changes.
70    pub fn filtered_columns(&self) -> Vec<(usize, &SortColumn)> {
71        use std::hash::{Hash, Hasher};
72        let mut hasher = std::collections::hash_map::DefaultHasher::new();
73        self.filter_input.value().hash(&mut hasher);
74        for c in &self.columns {
75            (&c.name, c.display_order).hash(&mut hasher);
76        }
77        let key = hasher.finish();
78        let mut shown = self.shown.lock().unwrap_or_else(|e| e.into_inner());
79        if shown.as_ref().is_none_or(|(at, _)| *at != key) {
80            let filter_text = self.filter_input.value().to_lowercase();
81            let mut filtered: Vec<usize> = (0..self.columns.len())
82                .filter(|&i| self.columns[i].name.to_lowercase().contains(&filter_text))
83                .collect();
84            filtered.sort_by_key(|&i| self.columns[i].display_order);
85            *shown = Some((key, filtered));
86            #[cfg(test)]
87            self.shown_builds
88                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
89        }
90        let rows = shown.as_ref().map_or(&[][..], |(_, rows)| rows);
91        rows.iter().map(|&i| (i, &self.columns[i])).collect()
92    }
93
94    pub fn get_column_order(&self) -> Vec<String> {
95        let mut cols: Vec<_> = self.columns.iter().filter(|c| c.is_visible).collect();
96        cols.sort_by_key(|c| c.display_order);
97        cols.into_iter().map(|c| c.name.clone()).collect()
98    }
99
100    /// How many visible columns the table freezes: those at or before the last locked
101    /// column in the order. A hidden column inside that span keeps its place but is
102    /// not counted, since the table never draws it.
103    pub fn get_locked_columns_count(&self) -> usize {
104        let Some(last_locked) = self
105            .columns
106            .iter()
107            .filter(|c| c.is_locked)
108            .map(|c| c.display_order)
109            .max()
110        else {
111            return 0;
112        };
113        self.columns
114            .iter()
115            .filter(|c| c.is_visible && c.display_order <= last_locked)
116            .count()
117    }
118
119    /// How many leading columns of the full order are frozen, hidden ones included.
120    pub fn get_locked_span(&self) -> usize {
121        self.columns
122            .iter()
123            .filter(|c| c.is_locked)
124            .map(|c| c.display_order + 1)
125            .max()
126            .unwrap_or(0)
127    }
128
129    /// Every column of the order: all columns, hidden included, sorted by place.
130    pub fn get_full_column_order(&self) -> Vec<String> {
131        let mut cols: Vec<_> = self.columns.iter().collect();
132        cols.sort_by_key(|c| c.display_order);
133        cols.into_iter().map(|c| c.name.clone()).collect()
134    }
135
136    pub fn get_sorted_columns(&self) -> Vec<String> {
137        self.sorted_columns_and_directions().0
138    }
139
140    /// The staged sort: column names in sort order, and per column whether it runs
141    /// descending.
142    pub fn sorted_columns_and_directions(&self) -> (Vec<String>, Vec<bool>) {
143        let mut sorted: Vec<_> = self
144            .columns
145            .iter()
146            .filter_map(|c| c.sort_order.map(|o| (o, c.name.clone(), c.sort_descending)))
147            .collect();
148        sorted.sort_by_key(|(order, _, _)| *order);
149        sorted
150            .into_iter()
151            .map(|(_, name, descending)| (name, descending))
152            .unzip()
153    }
154
155    /// Space on a column: none → ascending → descending → none. Joining the sort
156    /// appends the column at the end; leaving it renumbers the rest.
157    pub fn cycle_sort(&mut self) {
158        if let Some(idx) = self.table_state.selected() {
159            let filtered = self.filtered_columns();
160            if let Some((real_idx, _)) = filtered.get(idx) {
161                let real_idx = *real_idx;
162                match (
163                    self.columns[real_idx].sort_order,
164                    self.columns[real_idx].sort_descending,
165                ) {
166                    (None, _) => {
167                        let max_order = self
168                            .columns
169                            .iter()
170                            .filter_map(|c| c.sort_order)
171                            .max()
172                            .unwrap_or(0);
173                        self.columns[real_idx].sort_order = Some(max_order + 1);
174                        self.columns[real_idx].sort_descending = false;
175                    }
176                    (Some(_), false) => {
177                        self.columns[real_idx].sort_descending = true;
178                    }
179                    (Some(_), true) => self.unsort_index(real_idx),
180                }
181                self.has_unapplied_changes = true;
182            }
183        }
184    }
185
186    /// ← on a column: the cycle of Space backwards, none → descending → ascending →
187    /// none.
188    pub fn cycle_sort_back(&mut self) {
189        let Some(idx) = self.table_state.selected() else {
190            return;
191        };
192        let Some(&(real_idx, _)) = self.filtered_columns().get(idx) else {
193            return;
194        };
195        let col = &self.columns[real_idx];
196        match (col.sort_order, col.sort_descending) {
197            (None, _) => {
198                let max_order = self.columns.iter().filter_map(|c| c.sort_order).max();
199                self.columns[real_idx].sort_order = Some(max_order.unwrap_or(0) + 1);
200                self.columns[real_idx].sort_descending = true;
201            }
202            (Some(_), true) => self.columns[real_idx].sort_descending = false,
203            (Some(_), false) => self.unsort_index(real_idx),
204        }
205        self.has_unapplied_changes = true;
206    }
207
208    /// The sort, as indices into `columns`, first key first.
209    pub fn sort_entries(&self) -> Vec<usize> {
210        let mut entries: Vec<(usize, usize)> = self
211            .columns
212            .iter()
213            .enumerate()
214            .filter_map(|(i, c)| c.sort_order.map(|o| (o, i)))
215            .collect();
216        entries.sort_unstable();
217        entries.into_iter().map(|(_, i)| i).collect()
218    }
219
220    /// Flip the direction of the sort's `entry`th key.
221    pub fn flip_sort(&mut self, entry: usize) {
222        if let Some(&i) = self.sort_entries().get(entry) {
223            self.columns[i].sort_descending = !self.columns[i].sort_descending;
224            self.has_unapplied_changes = true;
225        }
226    }
227
228    /// Drop the sort's `entry`th key; the keys after it move up.
229    pub fn remove_sort_entry(&mut self, entry: usize) {
230        if let Some(&i) = self.sort_entries().get(entry) {
231            self.unsort_index(i);
232            self.has_unapplied_changes = true;
233        }
234    }
235
236    /// Move the sort's `entry`th key one place earlier or later. Returns where it is
237    /// now.
238    pub fn move_sort_entry(&mut self, entry: usize, earlier: bool) -> usize {
239        let entries = self.sort_entries();
240        let to = if earlier {
241            entry.checked_sub(1)
242        } else {
243            Some(entry + 1).filter(|to| *to < entries.len())
244        };
245        let (Some(to), Some(&from_i)) = (to, entries.get(entry)) else {
246            return entry;
247        };
248        let to_i = entries[to];
249        let a = self.columns[from_i].sort_order;
250        self.columns[from_i].sort_order = self.columns[to_i].sort_order;
251        self.columns[to_i].sort_order = a;
252        self.has_unapplied_changes = true;
253        to
254    }
255
256    /// Add the column named `name` as the sort's last key, ascending. Returns its
257    /// place in the sort; `None` when no column has that name. A column already
258    /// sorted keeps its place.
259    pub fn add_sort(&mut self, name: &str) -> Option<usize> {
260        let i = self.columns.iter().position(|c| c.name == name)?;
261        if self.columns[i].sort_order.is_none() {
262            let max_order = self.columns.iter().filter_map(|c| c.sort_order).max();
263            self.columns[i].sort_order = Some(max_order.unwrap_or(0) + 1);
264            self.columns[i].sort_descending = false;
265            self.has_unapplied_changes = true;
266        }
267        self.sort_entries().iter().position(|&e| e == i)
268    }
269
270    /// Del on a column: drop it from the sort outright, wherever in the cycle
271    /// it stands, and renumber the columns after it.
272    pub fn remove_sort(&mut self) {
273        if let Some(idx) = self.table_state.selected() {
274            let filtered = self.filtered_columns();
275            if let Some((real_idx, _)) = filtered.get(idx) {
276                let real_idx = *real_idx;
277                if self.columns[real_idx].sort_order.is_some() {
278                    self.unsort_index(real_idx);
279                    self.has_unapplied_changes = true;
280                }
281            }
282        }
283    }
284
285    fn unsort_index(&mut self, real_idx: usize) {
286        let Some(old_order) = self.columns[real_idx].sort_order else {
287            return;
288        };
289        self.columns[real_idx].sort_order = None;
290        self.columns[real_idx].sort_descending = false;
291        for col in &mut self.columns {
292            if let Some(order) = col.sort_order
293                && order > old_order
294            {
295                col.sort_order = Some(order - 1);
296            }
297        }
298    }
299
300    // Move column up in display order (left)
301    pub fn move_column_display_up(&mut self) {
302        if let Some(idx) = self.table_state.selected() {
303            let filtered = self.filtered_columns();
304            if let Some((real_idx, _)) = filtered.get(idx) {
305                let real_idx = *real_idx;
306                let current_display_order = self.columns[real_idx].display_order;
307                let was_locked = self.columns[real_idx].is_locked;
308                if current_display_order > 0 {
309                    // Find the column with display_order one less (the column we're moving above)
310                    let mut target_col_locked = false;
311                    let mut target_col_to_be_locked = false;
312                    for col in &self.columns {
313                        if col.display_order == current_display_order - 1 {
314                            target_col_locked = col.is_locked;
315                            target_col_to_be_locked = col.is_to_be_locked;
316                            break;
317                        }
318                    }
319
320                    // Swap display orders
321                    for col in &mut self.columns {
322                        if col.display_order == current_display_order - 1 {
323                            col.display_order = current_display_order;
324                            break;
325                        }
326                    }
327                    let new_display_order = current_display_order - 1;
328                    let was_to_be_locked = self.columns[real_idx].is_to_be_locked;
329
330                    // Find the last column in the lock/to-be-locked section BEFORE the move
331                    // (needed to check if this is the last one)
332                    let last_locked_or_to_be_order = self
333                        .columns
334                        .iter()
335                        .filter(|c| c.is_locked || c.is_to_be_locked)
336                        .map(|c| c.display_order)
337                        .max()
338                        .unwrap_or(0);
339
340                    // Swap display orders
341                    self.columns[real_idx].display_order = new_display_order;
342
343                    // If moving an unlocked column into a locked region, inherit the lock status
344                    if !was_locked && !was_to_be_locked {
345                        if target_col_locked || target_col_to_be_locked {
346                            // The column we moved above is locked or to-be-locked, so this column should match
347                            self.columns[real_idx].is_locked = target_col_locked;
348                            self.columns[real_idx].is_to_be_locked = target_col_to_be_locked;
349                        }
350                    } else {
351                        // When moving a locked or to-be-locked column up, check if it's the last one in the lock/to-be-locked section
352                        // Only clear to-be-locked if this is the last column in the lock/to-be-locked section
353                        if (was_locked || was_to_be_locked)
354                            && current_display_order == last_locked_or_to_be_order
355                        {
356                            // Clear to-be-locked (never real locks) on the columns now between the new
357                            // position (exclusive) and the old (inclusive).
358                            for col in &mut self.columns {
359                                if col.display_order > new_display_order
360                                    && col.display_order <= current_display_order
361                                    && col.is_to_be_locked
362                                {
363                                    col.is_to_be_locked = false;
364                                }
365                            }
366                        }
367                    }
368
369                    self.has_unapplied_changes = true;
370                    if let Some(new_selected_idx) = self
371                        .filtered_columns()
372                        .iter()
373                        .position(|&(idx, _)| idx == real_idx)
374                    {
375                        self.table_state.select(Some(new_selected_idx));
376                    }
377                }
378            }
379        }
380    }
381
382    // Move column down in display order (right)
383    pub fn move_column_display_down(&mut self) {
384        if let Some(idx) = self.table_state.selected() {
385            let filtered = self.filtered_columns();
386            if let Some((real_idx, _)) = filtered.get(idx) {
387                let real_idx = *real_idx;
388                let max_display_order = self
389                    .columns
390                    .iter()
391                    .map(|c| c.display_order)
392                    .max()
393                    .unwrap_or(0);
394                let current_display_order = self.columns[real_idx].display_order;
395                let was_locked = self.columns[real_idx].is_locked;
396                if current_display_order < max_display_order {
397                    // Find the column with display_order one more
398                    for col in &mut self.columns {
399                        if col.display_order == current_display_order + 1 {
400                            col.display_order = current_display_order;
401                            break;
402                        }
403                    }
404                    let new_display_order = current_display_order + 1;
405                    let was_to_be_locked = self.columns[real_idx].is_to_be_locked;
406
407                    // Swap display orders first
408                    self.columns[real_idx].display_order = new_display_order;
409
410                    // A locked or to-be-locked column moved down marks the unlocked columns it crossed
411                    // (old position inclusive to new exclusive, not itself) as to-be-locked.
412                    if was_locked || was_to_be_locked {
413                        for (idx, col) in self.columns.iter_mut().enumerate() {
414                            // Mark columns that are now at positions from old position (inclusive) to new position (exclusive)
415                            // Exclude the moved column itself
416                            if idx != real_idx
417                                && col.display_order >= current_display_order
418                                && col.display_order < new_display_order
419                                && !col.is_locked
420                            {
421                                col.is_to_be_locked = true;
422                            }
423                        }
424                    }
425
426                    self.has_unapplied_changes = true;
427                    if let Some(new_selected_idx) = self
428                        .filtered_columns()
429                        .iter()
430                        .position(|&(idx, _)| idx == real_idx)
431                    {
432                        self.table_state.select(Some(new_selected_idx));
433                    }
434                }
435            }
436        }
437    }
438
439    // Toggle lock at this column (lock all columns up to and including this one)
440    pub fn toggle_lock_at_column(&mut self) {
441        if let Some(idx) = self.table_state.selected() {
442            let filtered = self.filtered_columns();
443            if let Some((real_idx, _)) = filtered.get(idx) {
444                let real_idx = *real_idx;
445                let target_display_order = self.columns[real_idx].display_order;
446
447                // Count how many columns are currently locked
448                let current_locked_count = self.columns.iter().filter(|c| c.is_locked).count();
449
450                // If clicking on a locked column or the first unlocked column, toggle lock boundary
451                if target_display_order < current_locked_count {
452                    // Unlock: set locked count to target_display_order
453                    for col in &mut self.columns {
454                        col.is_locked = col.display_order < target_display_order;
455                        col.is_to_be_locked = false; // Clear to-be-locked when unlocking
456                    }
457                } else {
458                    // Lock: set locked count to target_display_order + 1
459                    for col in &mut self.columns {
460                        col.is_locked = col.display_order <= target_display_order;
461                        col.is_to_be_locked = false; // Clear to-be-locked when applying locks
462                    }
463                }
464                self.has_unapplied_changes = true;
465            }
466        }
467    }
468
469    pub fn move_selection_up(&mut self) {
470        if let Some(idx) = self.table_state.selected() {
471            let filtered = self.filtered_columns();
472            if let Some((real_idx, _)) = filtered.get(idx) {
473                let real_idx = *real_idx;
474                if let Some(current_order) = self.columns[real_idx].sort_order
475                    && current_order > 1
476                {
477                    for col in &mut self.columns {
478                        if col.sort_order == Some(current_order - 1) {
479                            col.sort_order = Some(current_order);
480                            break;
481                        }
482                    }
483                    self.columns[real_idx].sort_order = Some(current_order - 1);
484                    self.has_unapplied_changes = true;
485                    if let Some(new_selected_idx) = self
486                        .filtered_columns()
487                        .iter()
488                        .position(|&(idx, _)| idx == real_idx)
489                    {
490                        self.table_state.select(Some(new_selected_idx));
491                    }
492                }
493            }
494        }
495    }
496
497    pub fn move_selection_down(&mut self) {
498        if let Some(idx) = self.table_state.selected() {
499            let filtered = self.filtered_columns();
500            if let Some((real_idx, _)) = filtered.get(idx) {
501                let real_idx = *real_idx;
502                let max_order = self
503                    .columns
504                    .iter()
505                    .filter_map(|c| c.sort_order)
506                    .max()
507                    .unwrap_or(0);
508                if let Some(current_order) = self.columns[real_idx].sort_order
509                    && current_order < max_order
510                {
511                    for col in &mut self.columns {
512                        if col.sort_order == Some(current_order + 1) {
513                            col.sort_order = Some(current_order);
514                            break;
515                        }
516                    }
517                    self.columns[real_idx].sort_order = Some(current_order + 1);
518                    if let Some(new_selected_idx) = self
519                        .filtered_columns()
520                        .iter()
521                        .position(|&(idx, _)| idx == real_idx)
522                    {
523                        self.table_state.select(Some(new_selected_idx));
524                    }
525                    self.has_unapplied_changes = true;
526                }
527            }
528        }
529    }
530
531    pub fn clear_selection(&mut self) {
532        for (idx, col) in self.columns.iter_mut().enumerate() {
533            col.sort_order = None;
534            col.sort_descending = false;
535            col.is_locked = false;
536            col.is_to_be_locked = false;
537            col.display_order = idx; // Reset to natural order (0, 1, 2, ...)
538            col.is_visible = true; // Make all columns visible
539            col.width = WidthChoice::Auto;
540        }
541        self.has_unapplied_changes = true;
542    }
543
544    /// Change how the width of the column under the cursor is chosen, from what is
545    /// staged: `<` and `>` step it, `f` fits it to the rows on screen, `w` returns
546    /// it to automatic.
547    pub fn change_width(&mut self, change: impl FnOnce(WidthChoice, Option<u16>) -> WidthChoice) {
548        let Some(idx) = self.table_state.selected() else {
549            return;
550        };
551        let Some(&(real_idx, _)) = self.filtered_columns().get(idx) else {
552            return;
553        };
554        let col = &mut self.columns[real_idx];
555        let width = change(col.width, col.shown_width);
556        if width != col.width {
557            col.width = width;
558            self.has_unapplied_changes = true;
559        }
560    }
561
562    /// Every column's width choice, as staged, for the table to apply.
563    pub fn width_choices(&self) -> Vec<(String, WidthChoice)> {
564        self.columns
565            .iter()
566            .map(|c| (c.name.clone(), c.width))
567            .collect()
568    }
569
570    /// Hide or show the column under the cursor. Visibility only: the column keeps
571    /// its place in the order and its lock, so showing it puts it back where it was.
572    pub fn toggle_visibility(&mut self) {
573        if let Some(idx) = self.table_state.selected() {
574            let filtered = self.filtered_columns();
575            if let Some(&(real_idx, _)) = filtered.get(idx) {
576                let col = &mut self.columns[real_idx];
577                col.is_visible = !col.is_visible;
578                self.has_unapplied_changes = true;
579            }
580        }
581    }
582
583    pub fn jump_selection_to_order(&mut self, new_order: usize) {
584        if let Some(idx) = self.table_state.selected() {
585            let filtered = self.filtered_columns();
586            if let Some((real_idx, _)) = filtered.get(idx) {
587                let real_idx = *real_idx;
588                let max_order = self
589                    .columns
590                    .iter()
591                    .filter_map(|c| c.sort_order)
592                    .max()
593                    .unwrap_or(0);
594                let old_order = self.columns[real_idx].sort_order;
595                // A sorted column moves among the places already taken; an
596                // unsorted one may also join at the end.
597                let last = if old_order.is_some() {
598                    max_order
599                } else {
600                    max_order + 1
601                };
602
603                if new_order > 0 && new_order <= last {
604                    let selected_column_name = self.columns[real_idx].name.clone();
605
606                    // Adjust existing orders
607                    for col in &mut self.columns {
608                        if col.name == selected_column_name {
609                            continue; // Skip the selected column for now
610                        }
611                        if let Some(order) = col.sort_order {
612                            if let Some(old) = old_order {
613                                if new_order < old && order >= new_order && order < old {
614                                    col.sort_order = Some(order + 1);
615                                } else if new_order > old && order <= new_order && order > old {
616                                    col.sort_order = Some(order - 1);
617                                }
618                            } else {
619                                // If the selected column was not sorted before
620                                if order >= new_order {
621                                    col.sort_order = Some(order + 1);
622                                }
623                            }
624                        }
625                    }
626                    self.columns[real_idx].sort_order = Some(new_order);
627
628                    // Re-number to ensure continuous sequence if a gap was created or an item was removed
629                    let mut current_sorted_cols: Vec<(&mut SortColumn, usize)> = self
630                        .columns
631                        .iter_mut()
632                        .filter_map(|c| c.sort_order.map(|o| (c, o)))
633                        .collect();
634                    current_sorted_cols.sort_by_key(|(_, o)| *o);
635
636                    for (i, (col, _)) in current_sorted_cols.into_iter().enumerate() {
637                        col.sort_order = Some(i + 1);
638                    }
639
640                    if let Some(new_selected_idx) = self
641                        .filtered_columns()
642                        .iter()
643                        .position(|&(r_idx, _)| r_idx == real_idx)
644                    {
645                        self.table_state.select(Some(new_selected_idx));
646                    }
647                    if old_order != Some(new_order) {
648                        self.has_unapplied_changes = true;
649                    }
650                } else if new_order == 0 {
651                    // User wants to unset sort order
652                    if self.columns[real_idx].sort_order.take().is_some() {
653                        self.has_unapplied_changes = true;
654                    }
655                    // Re-number to ensure continuous sequence
656                    let mut current_sorted_cols: Vec<(&mut SortColumn, usize)> = self
657                        .columns
658                        .iter_mut()
659                        .filter_map(|c| c.sort_order.map(|o| (c, o)))
660                        .collect();
661                    current_sorted_cols.sort_by_key(|(_, o)| *o);
662
663                    for (i, (col, _)) in current_sorted_cols.into_iter().enumerate() {
664                        col.sort_order = Some(i + 1);
665                    }
666                    // Selection should remain on the same column even if its sort order is removed
667                    if let Some(new_selected_idx) = self
668                        .filtered_columns()
669                        .iter()
670                        .position(|&(r_idx, _)| r_idx == real_idx)
671                    {
672                        self.table_state.select(Some(new_selected_idx));
673                    }
674                } else {
675                    // Past the end of the order: say so rather than doing nothing.
676                    let range = if last == 1 {
677                        "1".to_string()
678                    } else {
679                        format!("1-{last}")
680                    };
681                    self.status = Some(format!(
682                        "Position {new_order} is past the end; use {range}."
683                    ));
684                }
685            }
686        }
687    }
688}
689
690/// The sidebar's full order: `visible` as the table applies it, with each column of
691/// `all` it leaves out (a hidden one) put back right after the column it followed in
692/// `reference`, the order last applied, or in `all` when `reference` does not name
693/// it. A hidden column with nothing before it goes first.
694pub fn order_with_hidden(visible: &[String], all: &[String], reference: &[String]) -> Vec<String> {
695    use std::collections::HashSet;
696    let mut order = visible.to_vec();
697    let mut placed: HashSet<&str> = visible.iter().map(String::as_str).collect();
698    for name in all {
699        if placed.contains(name.as_str()) {
700            continue;
701        }
702        let earlier = match reference.iter().position(|r| r == name) {
703            Some(i) => &reference[..i],
704            None => &all[..all.iter().position(|a| a == name).unwrap_or(0)],
705        };
706        let at = earlier
707            .iter()
708            .rev()
709            .find(|e| placed.contains(e.as_str()))
710            .and_then(|e| order.iter().position(|o| o == e))
711            .map_or(0, |p| p + 1);
712        order.insert(at, name.clone());
713        placed.insert(name.as_str());
714    }
715    order
716}
717
718#[cfg(test)]
719mod tests;