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