Skip to main content

datui_lib/table/
columns.rs

1//! Moving through the table: the row cursor, the column cursor, sideways paging,
2//! frozen columns and widths.
3
4use super::*;
5
6impl DataTableState {
7    /// Returns true if a buffer collect is needed after the scroll.
8    pub fn select_next(&mut self) -> bool {
9        self.table_state.select_next();
10        if let Some(selected) = self.table_state.selected()
11            && selected >= self.visible_rows
12            && self.visible_rows > 0
13        {
14            // The page moves, not the cursor: left past the last row, the draw would
15            // move the page a second row to show it.
16            self.table_state.select(Some(self.visible_rows - 1));
17            return self.slide_table(1);
18        }
19        false
20    }
21
22    /// Returns true if a buffer collect is needed after the scroll.
23    pub fn page_down(&mut self) -> bool {
24        self.slide_table(self.visible_rows as i64)
25    }
26
27    /// Returns true if a buffer collect is needed after the scroll.
28    pub fn select_previous(&mut self) -> bool {
29        if let Some(selected) = self.table_state.selected() {
30            self.table_state.select_previous();
31            if selected == 0 && self.view.start_row > 0 {
32                return self.slide_table(-1);
33            }
34        } else {
35            self.table_state.select(Some(0));
36        }
37        false
38    }
39
40    /// Returns true if a buffer collect is needed.
41    pub fn scroll_to(&mut self, index: usize) -> bool {
42        if self.view.start_row == index {
43            return false;
44        }
45        self.view.start_row = index;
46        true // caller must collect
47    }
48
49    /// Set scroll position for go-to-line (centered). Returns true if a collect is needed.
50    pub fn scroll_to_row_centered(&mut self, row_index: usize) -> bool {
51        if self.view.num_rows == 0 || self.visible_rows == 0 {
52            return false;
53        }
54        let center_offset = self.visible_rows / 2;
55        let mut start_row = row_index.saturating_sub(center_offset);
56        let max_start = self.view.num_rows.saturating_sub(self.visible_rows);
57        start_row = start_row.min(max_start);
58
59        if self.view.start_row == start_row {
60            let display_idx = row_index
61                .saturating_sub(start_row)
62                .min(self.visible_rows.saturating_sub(1));
63            self.table_state.select(Some(display_idx));
64            return false;
65        }
66
67        self.view.start_row = start_row;
68        let display_idx = row_index
69            .saturating_sub(start_row)
70            .min(self.visible_rows.saturating_sub(1));
71        self.table_state.select(Some(display_idx));
72        true // caller must collect
73    }
74
75    /// Jump to the first page. Returns true if a collect is needed.
76    pub fn scroll_to_start(&mut self) -> bool {
77        self.table_state.select(Some(0));
78        self.scroll_to(0)
79    }
80
81    /// Jump to the last page. Returns true if a collect is needed.
82    pub fn scroll_to_end(&mut self) -> bool {
83        if self.view.num_rows == 0 {
84            self.view.start_row = 0;
85            self.view.buffered_start_row = 0;
86            self.view.buffered_end_row = 0;
87            return false;
88        }
89        let end_start = self.view.num_rows.saturating_sub(self.visible_rows);
90        if self.view.start_row == end_start {
91            self.select_last_visible_row();
92            return false;
93        }
94        self.view.start_row = end_start;
95        self.select_last_visible_row();
96        true // caller must collect
97    }
98
99    /// Set table selection to the last row in the current view (for use after scroll_to_end).
100    fn select_last_visible_row(&mut self) {
101        if self.view.num_rows == 0 {
102            return;
103        }
104        let last_row_display_idx = (self.view.num_rows - 1).saturating_sub(self.view.start_row);
105        let sel = last_row_display_idx.min(self.visible_rows.saturating_sub(1));
106        self.table_state.select(Some(sel));
107    }
108
109    /// Returns true if a buffer collect is needed after the scroll.
110    pub fn half_page_down(&mut self) -> bool {
111        let half = (self.visible_rows / 2).max(1) as i64;
112        self.slide_table(half)
113    }
114
115    /// Returns true if a buffer collect is needed after the scroll.
116    pub fn half_page_up(&mut self) -> bool {
117        if self.view.start_row == 0 {
118            return false;
119        }
120        let half = (self.visible_rows / 2).max(1) as i64;
121        self.slide_table(-half)
122    }
123
124    /// Returns true if a buffer collect is needed after the scroll.
125    pub fn page_up(&mut self) -> bool {
126        if self.view.start_row == 0 {
127            return false;
128        }
129        self.slide_table(-(self.visible_rows as i64))
130    }
131
132    #[cfg(test)]
133    pub(crate) fn scroll_right(&mut self) {
134        self.scroll_columns(ColumnMove::StepRight);
135    }
136
137    #[cfg(test)]
138    pub(crate) fn scroll_left(&mut self) {
139        self.scroll_columns(ColumnMove::StepLeft);
140    }
141
142    /// Which shown columns the table last drew, and the cursor's: what the control
143    /// bar's column position says.
144    pub fn columns_on_screen(&self) -> Option<OnScreen> {
145        self.on_screen
146    }
147
148    /// How many columns scroll: the shown ones right of those drawn frozen.
149    fn scroll_count(&self) -> usize {
150        self.view
151            .column_order
152            .len()
153            .saturating_sub(self.frozen_shown())
154    }
155
156    /// Move the view sideways, the cursor staying unless left behind on the left. Planned
157    /// from last-drawn widths, reading nothing; a page needing an undrawn column waits for
158    /// the next draw (which measures it from rows on hand), and moves typed behind it wait
159    /// too, in order.
160    pub(super) fn scroll_columns(&mut self, mv: ColumnMove) {
161        if matches!(
162            mv,
163            ColumnMove::First | ColumnMove::Last | ColumnMove::Reveal(_)
164        ) {
165            // Where these go does not depend on where the moves before them went.
166            self.column_moves.clear();
167        }
168        if self.column_moves.is_empty()
169            && let Some(start) = self.plan_known(mv)
170        {
171            self.apply_column_move(mv, start);
172        } else {
173            self.wait(WaitingMove::View(mv));
174        }
175    }
176
177    /// Move the column cursor (`h` `l` `[` `]` `{` `}`), the view following only when the
178    /// cursor would leave the screen. Reads nothing; moves needing undrawn columns wait as
179    /// in `Self::scroll_columns`.
180    pub fn move_cursor(&mut self, mv: CursorMove) {
181        if matches!(mv, CursorMove::First | CursorMove::Last) {
182            self.column_moves.clear();
183        }
184        if !self.column_moves.is_empty()
185            || !self.land_cursor_move(mv, &mut |state: &mut Self, view| state.plan_known(view))
186        {
187            self.wait(WaitingMove::Cursor(mv));
188        }
189    }
190
191    /// Put the cursor on the shown column `name` and show it as `g` does: left where
192    /// it is when already whole on screen, else first after the frozen columns, or on
193    /// the last page when it is there. A frozen column is on screen already.
194    pub fn go_to_column(&mut self, name: &str) {
195        let Some(at) = self.view.column_order.iter().position(|c| c == name) else {
196            return;
197        };
198        self.column_moves.clear();
199        self.place_cursor_at(at);
200        if let Some(index) = at.checked_sub(self.frozen_shown()) {
201            self.scroll_columns(ColumnMove::Reveal(index));
202        }
203    }
204
205    /// Put the cursor on the shown column `name`, scrolling as little as it takes to
206    /// show it.
207    pub fn set_current_column(&mut self, name: &str) {
208        let Some(at) = self.view.column_order.iter().position(|c| c == name) else {
209            return;
210        };
211        self.column_moves.clear();
212        self.place_cursor_at(at);
213        self.follow_cursor(&mut |state: &mut Self, view| state.plan_known(view));
214    }
215
216    /// The column cursor's column: the one the per-column keys act on (value counts,
217    /// copying a cell, the sidebar and inspector opening on it, a find in one column).
218    /// The first shown column until the cursor moves; `None` with no columns shown.
219    pub fn current_column(&self) -> Option<&str> {
220        self.cursor_index()
221            .map(|at| self.view.column_order[at].as_str())
222    }
223
224    /// The cursor's place among the shown columns, from 0, frozen ones first.
225    pub fn current_column_index(&self) -> Option<usize> {
226        self.cursor_index()
227    }
228
229    pub(crate) fn cursor_index(&self) -> Option<usize> {
230        let last = self.view.column_order.len().checked_sub(1)?;
231        Some(
232            self.view
233                .cursor_column
234                .as_deref()
235                .and_then(|name| self.view.column_order.iter().position(|c| c == name))
236                .unwrap_or(self.view.cursor_at.min(last)),
237        )
238    }
239
240    pub(super) fn place_cursor_at(&mut self, at: usize) {
241        self.view.cursor_column = self.view.column_order.get(at).cloned();
242        self.view.cursor_at = at;
243    }
244
245    /// After the shown columns changed: the cursor stays on its column by name, or,
246    /// where that was hidden, takes the one now in its place; the next draw shows it.
247    pub(super) fn settle_cursor(&mut self) {
248        let at = self.cursor_index().unwrap_or(0);
249        self.place_cursor_at(at);
250        self.reveal_cursor = true;
251    }
252
253    /// Queue a move for the next draw, behind any already waiting.
254    fn wait(&mut self, mv: WaitingMove) {
255        if self.column_moves.len() < MAX_WAITING_MOVES {
256            self.column_moves.push(mv);
257        }
258    }
259
260    /// Scroll as little as it takes to show the cursor's column whole, with `plan`;
261    /// a plan that needs a width not drawn yet waits for the next draw.
262    fn follow_cursor(&mut self, plan: &mut impl FnMut(&mut Self, ColumnMove) -> Option<usize>) {
263        let Some(index) = self
264            .cursor_index()
265            .and_then(|at| at.checked_sub(self.frozen_shown()))
266        else {
267            return;
268        };
269        let view = ColumnMove::Keep(index);
270        match plan(self, view) {
271            // On screen already: nothing moves, and the trail `[` retraces stays.
272            Some(start) if start == self.termcol_index => {}
273            Some(start) => self.apply_column_move(view, start),
274            None => self.wait(WaitingMove::View(view)),
275        }
276    }
277
278    /// Land a cursor move, the view planned with `plan`. Returns false, changing
279    /// nothing, when a page cannot be planned yet: where it lands decides the cursor.
280    fn land_cursor_move(
281        &mut self,
282        mv: CursorMove,
283        plan: &mut impl FnMut(&mut Self, ColumnMove) -> Option<usize>,
284    ) -> bool {
285        let Some(cursor) = self.cursor_index() else {
286            return true;
287        };
288        let last = self.view.column_order.len() - 1;
289        let frozen = self.frozen_shown();
290        match mv {
291            CursorMove::Left | CursorMove::Right => {
292                let at = if mv == CursorMove::Left {
293                    cursor.saturating_sub(1)
294                } else {
295                    (cursor + 1).min(last)
296                };
297                self.place_cursor_at(at);
298                self.follow_cursor(plan);
299            }
300            CursorMove::First | CursorMove::Last => {
301                let (at, view) = if mv == CursorMove::First {
302                    (0, ColumnMove::First)
303                } else {
304                    (last, ColumnMove::Last)
305                };
306                self.place_cursor_at(at);
307                match plan(self, view) {
308                    Some(start) => self.apply_column_move(view, start),
309                    None => self.wait(WaitingMove::View(view)),
310                }
311            }
312            CursorMove::PageLeft | CursorMove::PageRight => {
313                let view = if mv == CursorMove::PageLeft {
314                    ColumnMove::PageLeft
315                } else {
316                    ColumnMove::PageRight
317                };
318                let Some(start) = plan(self, view) else {
319                    return false;
320                };
321                let from = self.termcol_index;
322                self.apply_column_move(view, start);
323                let at = if self.termcol_index != from {
324                    // The new page, from its first column.
325                    frozen + self.termcol_index
326                } else if mv == CursorMove::PageRight {
327                    // On the last page already: its last column.
328                    last
329                } else if cursor > frozen {
330                    // On the first page: its first column, then the first of all.
331                    frozen
332                } else {
333                    0
334                };
335                self.place_cursor_at(at.min(last));
336            }
337        }
338        true
339    }
340
341    /// The scrolling columns, by name.
342    pub(super) fn scrolling_names(&self) -> &[String] {
343        &self.view.column_order[self.frozen_shown().min(self.view.column_order.len())..]
344    }
345
346    /// `[` straight after the `]` that came here goes back where that one started,
347    /// whatever the widths say, so a page and back is the page left.
348    fn retrace(&self, mv: ColumnMove) -> Option<usize> {
349        let &(back, to) = self.page_trail.last()?;
350        (mv == ColumnMove::PageLeft && to == self.termcol_index).then_some(back)
351    }
352
353    /// Where `mv` lands on the widths drawn in this view, or `None` when it needs one
354    /// not drawn yet (or the room, before the first draw).
355    fn plan_known(&self, mv: ColumnMove) -> Option<usize> {
356        if let Some(back) = self.retrace(mv) {
357            return Some(back);
358        }
359        let needs_widths = match mv {
360            #[cfg(test)]
361            ColumnMove::StepLeft | ColumnMove::StepRight => false,
362            ColumnMove::First => false,
363            // Back to a column at or left of the first shown needs no width.
364            ColumnMove::Keep(column) => column > self.termcol_index,
365            _ => true,
366        };
367        let room = match self.scroll_room {
368            Some(room) => room,
369            None if needs_widths => return None,
370            None => Room::default(),
371        };
372        let names = self.scrolling_names();
373        crate::widgets::column_paging::plan(mv, self.termcol_index, names.len(), room, |i| {
374            self.drawn_width(&names[i])
375        })
376    }
377
378    /// Land `mv` at `start`, keeping the trail `[` retraces. A cursor the view leaves
379    /// behind on the left comes along, to the first column shown.
380    fn apply_column_move(&mut self, mv: ColumnMove, start: usize) {
381        let from = self.termcol_index;
382        let start = start.min(self.scroll_count().saturating_sub(1));
383        match mv {
384            ColumnMove::PageRight => {
385                if start > from {
386                    self.page_trail.push((from, start));
387                }
388            }
389            ColumnMove::PageLeft if self.retrace(mv) == Some(start) => {
390                self.page_trail.pop();
391            }
392            _ => self.page_trail.clear(),
393        }
394        self.scroll_columns_to(start);
395        let frozen = self.frozen_shown();
396        if let Some(cursor) = self.cursor_index()
397            && cursor >= frozen
398            && cursor < frozen + self.termcol_index
399        {
400            self.place_cursor_at(frozen + self.termcol_index);
401        }
402    }
403
404    /// Forget sideways moves waiting on a draw and the trail `[` retraces: the
405    /// columns they were counted over are gone.
406    pub(super) fn clear_column_moves(&mut self) {
407        self.column_moves.clear();
408        self.page_trail.clear();
409    }
410
411    /// Start the scrolling columns at `start`, re-slicing the buffer held.
412    fn scroll_columns_to(&mut self, start: usize) {
413        let start = start.min(self.scroll_count().saturating_sub(1));
414        if start != self.termcol_index {
415            self.termcol_index = start;
416            self.rescroll_columns();
417        }
418    }
419
420    /// While drawing, before the scrolling columns: record the scrolling side's layout,
421    /// land waiting moves in order, and bring the cursor back on screen, measuring undrawn
422    /// columns a move crosses with `width` from rows on hand. Reads nothing; without rows
423    /// the moves wait.
424    pub(crate) fn land_column_moves(
425        &mut self,
426        room: Room,
427        mut width: impl FnMut(&mut Self, &str) -> u16,
428    ) {
429        if self.scroll_room != Some(room) {
430            // A resize, or a frozen column given back: the cursor may be off screen.
431            self.reveal_cursor = true;
432        }
433        self.scroll_room = Some(room);
434        if (self.column_moves.is_empty() && !self.reveal_cursor)
435            || !self.buffer_on_hand()
436            || self.defer_collect
437        {
438            return;
439        }
440        let mut plan = |state: &mut Self, mv: ColumnMove| -> Option<usize> {
441            if let Some(back) = state.retrace(mv) {
442                return Some(back);
443            }
444            let from = state.termcol_index;
445            let count = state.scroll_count();
446            Some(
447                crate::widgets::column_paging::plan(mv, from, count, room, |i| {
448                    let name = state.scrolling_names()[i].clone();
449                    Some(width(state, &name))
450                })
451                .unwrap_or(from),
452            )
453        };
454        for mv in std::mem::take(&mut self.column_moves) {
455            match mv {
456                WaitingMove::View(mv) => {
457                    let start = plan(self, mv).unwrap_or(self.termcol_index);
458                    self.apply_column_move(mv, start);
459                }
460                WaitingMove::Cursor(mv) => {
461                    self.land_cursor_move(mv, &mut plan);
462                }
463            }
464        }
465        if std::mem::take(&mut self.reveal_cursor) {
466            self.follow_cursor(&mut plan);
467        }
468    }
469
470    /// Show the new column window by re-slicing the held buffer, never via
471    /// [`collect`]: this runs inline on the key thread (and Left/Right act while busy),
472    /// where `collect`'s row count or page reload could freeze on a cloud hive. Draws
473    /// nothing without a matching buffer; the index still moves, so the first drawn frame
474    /// is already scrolled.
475    ///
476    /// [`collect`]: Self::collect
477    fn rescroll_columns(&mut self) {
478        if self.defer_collect || !self.buffer_on_hand() {
479            return;
480        }
481        self.slice_buffer_into_display();
482        if self.table_state.selected().is_none() {
483            self.table_state.select(Some(0));
484        }
485    }
486
487    pub fn headers(&self) -> Vec<String> {
488        self.view.column_order.clone()
489    }
490
491    pub fn set_column_order(&mut self, order: Vec<String>) {
492        self.view.column_order = order;
493        self.clear_column_moves();
494        // Fewer columns shown may leave the scroll past the last; keep one on screen.
495        self.termcol_index = self
496            .termcol_index
497            .min(self.scroll_count().saturating_sub(1));
498        self.drop_buffer();
499        self.settle_cursor();
500        self.collect();
501    }
502
503    pub fn set_locked_columns(&mut self, count: usize) {
504        self.view.locked_columns_count = count.min(self.view.column_order.len());
505        self.clear_column_moves();
506        self.settle_cursor();
507        self.termcol_index = self
508            .termcol_index
509            .min(self.scroll_count().saturating_sub(1));
510        self.drop_buffer();
511        self.collect();
512    }
513
514    pub fn locked_columns_count(&self) -> usize {
515        self.view.locked_columns_count
516    }
517
518    /// How many columns are drawn frozen: the count asked for, or fewer while the
519    /// last layout could not fit them all beside a usable scrolling column. The ones
520    /// left out lead the scrolling columns, so every column stays reachable.
521    pub fn frozen_shown(&self) -> usize {
522        let (asked, shown) = self.view.frozen_fit;
523        if asked == self.view.locked_columns_count {
524            shown.min(asked)
525        } else {
526            self.view.locked_columns_count
527        }
528    }
529
530    /// Take the layout's count of frozen columns that fit and re-slice the scrolling
531    /// columns after them: unscrolled, left-out frozen columns lead; scrolled, the first
532    /// scrolled column stays first where it can, so a resize does not move the view. Reads
533    /// nothing; called while drawing.
534    pub(crate) fn fit_frozen(&mut self, shown: usize) {
535        let before = self.frozen_shown();
536        let shown = shown.min(self.view.locked_columns_count);
537        if shown == before {
538            self.view.frozen_fit = (self.view.locked_columns_count, shown);
539            return;
540        }
541        if self.defer_collect || !self.buffer_on_hand() {
542            return;
543        }
544        self.view.frozen_fit = (self.view.locked_columns_count, shown);
545        // The scrolling indices the trail was kept in shift with the frozen count.
546        self.page_trail.clear();
547        if self.termcol_index > 0 {
548            let first = before + self.termcol_index;
549            let last = self.view.column_order.len().saturating_sub(1);
550            self.termcol_index = first.min(last).saturating_sub(shown);
551        }
552        self.slice_buffer_into_display();
553    }
554
555    /// The type a column has in the frame on screen: with its name, the identity its
556    /// width is kept under.
557    pub(crate) fn width_dtype(&self, name: &str) -> DataType {
558        self.view
559            .schema
560            .get(name)
561            .cloned()
562            .unwrap_or(DataType::Null)
563    }
564
565    /// How a column's width is chosen.
566    pub fn width_choice(&self, name: &str) -> WidthChoice {
567        self.widths.choice(name, &self.width_dtype(name))
568    }
569
570    /// The width a column was last drawn at, if it has been drawn.
571    pub fn shown_width(&self, name: &str) -> Option<u16> {
572        self.widths.shown(name, &self.width_dtype(name))
573    }
574
575    /// The width the column takes on screen, the room it filled at the right edge
576    /// included.
577    pub fn on_screen_width(&self, name: &str) -> Option<u16> {
578        self.widths.on_screen(name, &self.width_dtype(name))
579    }
580
581    /// The width a column draws at in this view, if it has been drawn since the
582    /// widths were last relearned. What a sideways page is planned with.
583    pub(crate) fn drawn_width(&self, name: &str) -> Option<u16> {
584        self.widths.drawn(name, &self.width_dtype(name))
585    }
586
587    /// Set how each named column's width is chosen. Reads nothing: a fit is taken
588    /// from the rows on screen when the table is next drawn.
589    pub fn set_width_choices(&mut self, choices: impl IntoIterator<Item = (String, WidthChoice)>) {
590        for (name, choice) in choices {
591            let dtype = self.width_dtype(&name);
592            self.widths.set_choice(&name, &dtype, choice);
593        }
594    }
595
596    /// One column's rows on screen, from the buffer already held, as the table draws
597    /// them. For fitting a column that may be scrolled out of view.
598    pub(crate) fn page_column(&self, name: &str, offset: usize, len: usize) -> Option<DataFrame> {
599        let column = self.view.buffered_df.as_ref()?.select([name]).ok()?;
600        visible_slice(&column, offset, len)
601    }
602}