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;