Skip to main content

datui_lib/widgets/
column_paging.rs

1//! Sideways moves across the main table's scrolling columns, planned from the widths
2//! the columns are drawn at.
3//!
4//! A plan only adds up widths: it formats and reads nothing. A width the table has not
5//! drawn in this view (since its widths were last relearned) is unknown, and a plan
6//! that needs one says so with `None`; the next draw then measures that column from
7//! the rows on screen and plans again. Indices here are scrolling indices: 0 is the
8//! first column right of the frozen ones.
9
10/// The room the scrolling columns are laid out in, as the table last drew it.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub struct Room {
13    /// Cells from the frozen separator (or the left edge) to the right edge.
14    pub width: u16,
15    /// Cells before the first column: the gap after the frozen separator.
16    pub lead: u16,
17    /// Cells between two columns.
18    pub padding: u16,
19}
20
21/// Which shown columns the table drew, counted from 1 in the table's order: frozen
22/// columns first, hidden columns not at all. `first` and `last` are the scrolling
23/// columns on screen; the frozen ones are always on screen before them. `cursor` is
24/// the column cursor's column.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct OnScreen {
27    pub first: usize,
28    pub last: usize,
29    pub cursor: usize,
30    pub total: usize,
31}
32
33impl OnScreen {
34    /// `col 43 of 300`, or `col 43/300` where the bar is short of room.
35    pub fn label(&self, compact: bool) -> String {
36        if compact {
37            format!("col {}/{}", self.cursor, self.total)
38        } else {
39            format!("col {} of {}", self.cursor, self.total)
40        }
41    }
42}
43
44/// A sideways move. The pages need the columns' widths to land; the rest do not, and
45/// are moves here so that one typed behind a page waiting on a draw lands after it.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ColumnMove {
48    /// One column left: tests step the view; the app moves the cursor instead.
49    #[cfg(test)]
50    StepLeft,
51    /// One column right.
52    #[cfg(test)]
53    StepRight,
54    /// The first scrolling column.
55    First,
56    /// The page before: the column left of the first one shown ends it.
57    PageLeft,
58    /// The page after: the first column not shown whole starts it, or the last page
59    /// starts it when that is nearer, so the last page is never short.
60    PageRight,
61    /// The last page: the last column ends it, with as many before it as fit.
62    Last,
63    /// Show this column: left where it is when already whole on screen, else first,
64    /// or on the last page when it is on that page.
65    Reveal(usize),
66    /// Keep this column whole on screen, scrolling as little as it takes: first when
67    /// it is left of the screen, last when it is right of it.
68    Keep(usize),
69}
70
71/// A move of the column cursor. The view follows only as far as the cursor needs:
72/// it scrolls when the cursor would leave the screen, and pages move both.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum CursorMove {
75    /// The shown column before the cursor's, frozen ones included.
76    Left,
77    /// The shown column after the cursor's.
78    Right,
79    /// The first shown column, and the view back to the start.
80    First,
81    /// The last shown column, on the last page.
82    Last,
83    /// A page left, the cursor on its first column.
84    PageLeft,
85    /// A page right, the cursor on its first column.
86    PageRight,
87}
88
89/// How many columns from `start` are drawn whole, the way the table lays them out:
90/// left to right, each whole while it fits in what is left.
91pub fn whole_from(
92    start: usize,
93    count: usize,
94    room: Room,
95    width: &mut impl FnMut(usize) -> Option<u16>,
96) -> Option<usize> {
97    let mut used = room.lead;
98    let mut whole = 0;
99    for i in start..count {
100        let w = width(i)?;
101        if used.saturating_add(w) > room.width {
102            break;
103        }
104        used = used.saturating_add(w).saturating_add(room.padding);
105        whole += 1;
106    }
107    Some(whole)
108}
109
110/// The first column of the page that ends with `end` drawn whole: as many columns
111/// before it as fit, and `end` alone when nothing else does.
112pub fn start_ending_at(
113    end: usize,
114    room: Room,
115    width: &mut impl FnMut(usize) -> Option<u16>,
116) -> Option<usize> {
117    let mut used = room.lead.saturating_add(width(end)?);
118    let mut start = end;
119    while start > 0 {
120        let next = used
121            .saturating_add(room.padding)
122            .saturating_add(width(start - 1)?);
123        if next > room.width {
124            break;
125        }
126        used = next;
127        start -= 1;
128    }
129    Some(start)
130}
131
132/// Where the scrolling columns start after `mv`, from `current` among `count`. Every
133/// move that can go somewhere does: a page moves at least one column, even past a
134/// column wider than the room. `None` when a width it needs has not been drawn.
135pub fn plan(
136    mv: ColumnMove,
137    current: usize,
138    count: usize,
139    room: Room,
140    mut width: impl FnMut(usize) -> Option<u16>,
141) -> Option<usize> {
142    let Some(last) = count.checked_sub(1) else {
143        return Some(0);
144    };
145    let current = current.min(last);
146    match mv {
147        #[cfg(test)]
148        ColumnMove::StepLeft => Some(current.saturating_sub(1)),
149        #[cfg(test)]
150        ColumnMove::StepRight => Some((current + 1).min(last)),
151        ColumnMove::First => Some(0),
152        ColumnMove::PageLeft if current == 0 => Some(0),
153        ColumnMove::PageLeft => start_ending_at(current - 1, room, &mut width),
154        ColumnMove::PageRight if current == last => Some(current),
155        ColumnMove::PageRight => {
156            let whole = whole_from(current, count, room, &mut width)?;
157            if current + whole >= count {
158                return Some(current);
159            }
160            let next = current + whole.max(1);
161            let last_page = start_ending_at(last, room, &mut width)?;
162            Some(next.min(last_page).max(current + 1))
163        }
164        ColumnMove::Last => start_ending_at(last, room, &mut width),
165        ColumnMove::Reveal(column) => {
166            let column = column.min(last);
167            if column >= current && column - current < whole_from(current, count, room, &mut width)?
168            {
169                return Some(current);
170            }
171            Some(column.min(start_ending_at(last, room, &mut width)?))
172        }
173        ColumnMove::Keep(column) => {
174            let column = column.min(last);
175            if column <= current {
176                return Some(column);
177            }
178            // Only the columns up to this one are measured: the rest cannot change it.
179            if column - current < whole_from(current, column + 1, room, &mut width)? {
180                return Some(current);
181            }
182            start_ending_at(column, room, &mut width)
183        }
184    }
185}
186
187#[cfg(test)]
188mod tests;