1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub struct Room {
13 pub width: u16,
15 pub lead: u16,
17 pub padding: u16,
19}
20
21#[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 pub fn widest_label(&self, compact: bool) -> String {
37 Self {
38 cursor: self.total,
39 ..*self
40 }
41 .label(compact)
42 }
43
44 pub fn label(&self, compact: bool) -> String {
46 if compact {
47 format!("col {}/{}", self.cursor, self.total)
48 } else {
49 format!("col {} of {}", self.cursor, self.total)
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum ColumnMove {
58 StepLeft,
60 StepRight,
62 First,
64 PageLeft,
66 PageRight,
69 Last,
71 Reveal(usize),
74 Keep(usize),
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum CursorMove {
83 Left,
85 Right,
87 First,
89 Last,
91 PageLeft,
93 PageRight,
95}
96
97pub fn whole_from(
100 start: usize,
101 count: usize,
102 room: Room,
103 width: &mut impl FnMut(usize) -> Option<u16>,
104) -> Option<usize> {
105 let mut used = room.lead;
106 let mut whole = 0;
107 for i in start..count {
108 let w = width(i)?;
109 if used.saturating_add(w) > room.width {
110 break;
111 }
112 used = used.saturating_add(w).saturating_add(room.padding);
113 whole += 1;
114 }
115 Some(whole)
116}
117
118pub fn start_ending_at(
121 end: usize,
122 room: Room,
123 width: &mut impl FnMut(usize) -> Option<u16>,
124) -> Option<usize> {
125 let mut used = room.lead.saturating_add(width(end)?);
126 let mut start = end;
127 while start > 0 {
128 let next = used
129 .saturating_add(room.padding)
130 .saturating_add(width(start - 1)?);
131 if next > room.width {
132 break;
133 }
134 used = next;
135 start -= 1;
136 }
137 Some(start)
138}
139
140pub fn plan(
144 mv: ColumnMove,
145 current: usize,
146 count: usize,
147 room: Room,
148 mut width: impl FnMut(usize) -> Option<u16>,
149) -> Option<usize> {
150 let Some(last) = count.checked_sub(1) else {
151 return Some(0);
152 };
153 let current = current.min(last);
154 match mv {
155 ColumnMove::StepLeft => Some(current.saturating_sub(1)),
156 ColumnMove::StepRight => Some((current + 1).min(last)),
157 ColumnMove::First => Some(0),
158 ColumnMove::PageLeft if current == 0 => Some(0),
159 ColumnMove::PageLeft => start_ending_at(current - 1, room, &mut width),
160 ColumnMove::PageRight if current == last => Some(current),
161 ColumnMove::PageRight => {
162 let whole = whole_from(current, count, room, &mut width)?;
163 if current + whole >= count {
164 return Some(current);
165 }
166 let next = current + whole.max(1);
167 let last_page = start_ending_at(last, room, &mut width)?;
168 Some(next.min(last_page).max(current + 1))
169 }
170 ColumnMove::Last => start_ending_at(last, room, &mut width),
171 ColumnMove::Reveal(column) => {
172 let column = column.min(last);
173 if column >= current && column - current < whole_from(current, count, room, &mut width)?
174 {
175 return Some(current);
176 }
177 Some(column.min(start_ending_at(last, room, &mut width)?))
178 }
179 ColumnMove::Keep(column) => {
180 let column = column.min(last);
181 if column <= current {
182 return Some(column);
183 }
184 if column - current < whole_from(current, column + 1, room, &mut width)? {
186 return Some(current);
187 }
188 start_ending_at(column, room, &mut width)
189 }
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 const ROOM: Room = Room {
198 width: 30,
199 lead: 0,
200 padding: 2,
201 };
202
203 fn widths(ws: &[u16]) -> impl FnMut(usize) -> Option<u16> + '_ {
204 |i| ws.get(i).copied()
205 }
206
207 #[test]
208 fn on_screen_labels() {
209 let on = OnScreen {
210 first: 41,
211 last: 47,
212 cursor: 43,
213 total: 300,
214 };
215 assert_eq!(on.label(false), "col 43 of 300");
216 assert_eq!(on.label(true), "col 43/300");
217 assert_eq!(on.widest_label(true), "col 300/300");
218 }
219
220 #[test]
221 fn whole_columns_count_padding_and_lead() {
222 let ws = [8, 8, 8, 8];
224 assert_eq!(whole_from(0, 4, ROOM, &mut widths(&ws)), Some(3));
225 let lead = Room { lead: 3, ..ROOM };
226 assert_eq!(whole_from(0, 4, lead, &mut widths(&ws)), Some(2));
227 assert_eq!(whole_from(0, 1, ROOM, &mut widths(&[30])), Some(1));
229 assert_eq!(whole_from(0, 1, ROOM, &mut widths(&[31])), Some(0));
230 }
231
232 #[test]
233 fn page_right_starts_at_the_first_column_not_whole() {
234 let ws = [8; 12];
235 assert_eq!(
237 plan(ColumnMove::PageRight, 0, 12, ROOM, widths(&ws)),
238 Some(3)
239 );
240 assert_eq!(
241 plan(ColumnMove::PageRight, 3, 12, ROOM, widths(&ws)),
242 Some(6)
243 );
244 let ws = [10, 10, 15, 4, 4];
246 assert_eq!(
247 plan(ColumnMove::PageRight, 0, 5, ROOM, widths(&ws)),
248 Some(2)
249 );
250 }
251
252 #[test]
253 fn page_right_never_leaves_a_short_last_page() {
254 let ws = [8; 10];
255 assert_eq!(
258 plan(ColumnMove::PageRight, 6, 10, ROOM, widths(&ws)),
259 Some(7)
260 );
261 assert_eq!(
263 plan(ColumnMove::PageRight, 7, 10, ROOM, widths(&ws)),
264 Some(7)
265 );
266 }
267
268 #[test]
269 fn a_column_wider_than_the_room_still_moves_one_at_a_time() {
270 let ws = [50, 50, 50];
271 assert_eq!(
272 plan(ColumnMove::PageRight, 0, 3, ROOM, widths(&ws)),
273 Some(1)
274 );
275 assert_eq!(
276 plan(ColumnMove::PageRight, 1, 3, ROOM, widths(&ws)),
277 Some(2)
278 );
279 assert_eq!(plan(ColumnMove::PageLeft, 2, 3, ROOM, widths(&ws)), Some(1));
280 assert_eq!(plan(ColumnMove::Last, 0, 3, ROOM, widths(&ws)), Some(2));
281 }
282
283 #[test]
284 fn page_left_ends_at_the_column_before_the_first() {
285 let ws = [8; 12];
286 assert_eq!(
287 plan(ColumnMove::PageLeft, 6, 12, ROOM, widths(&ws)),
288 Some(3)
289 );
290 assert_eq!(
291 plan(ColumnMove::PageLeft, 2, 12, ROOM, widths(&ws)),
292 Some(0)
293 );
294 assert_eq!(
295 plan(ColumnMove::PageLeft, 0, 12, ROOM, widths(&ws)),
296 Some(0)
297 );
298 let ws = [2, 2, 2, 9, 2, 4, 20, 1];
300 assert_eq!(plan(ColumnMove::PageLeft, 7, 8, ROOM, widths(&ws)), Some(4));
301 }
302
303 #[test]
304 fn last_fills_the_room_ending_at_the_last_column() {
305 let ws = [8; 10];
306 assert_eq!(plan(ColumnMove::Last, 0, 10, ROOM, widths(&ws)), Some(7));
307 assert_eq!(plan(ColumnMove::Last, 9, 10, ROOM, widths(&ws)), Some(7));
309 assert_eq!(plan(ColumnMove::Last, 0, 3, ROOM, widths(&ws)), Some(0));
311 }
312
313 #[test]
314 fn reveal_keeps_a_column_already_whole_on_screen() {
315 let ws = [8; 10];
316 assert_eq!(
317 plan(ColumnMove::Reveal(2), 0, 10, ROOM, widths(&ws)),
318 Some(0)
319 );
320 assert_eq!(
321 plan(ColumnMove::Reveal(3), 0, 10, ROOM, widths(&ws)),
322 Some(3)
323 );
324 assert_eq!(
325 plan(ColumnMove::Reveal(1), 4, 10, ROOM, widths(&ws)),
326 Some(1)
327 );
328 assert_eq!(
330 plan(ColumnMove::Reveal(9), 0, 10, ROOM, widths(&ws)),
331 Some(7)
332 );
333 }
334
335 #[test]
336 fn keep_scrolls_only_as_far_as_the_column() {
337 let ws = [8; 10];
338 for column in 0..3 {
340 assert_eq!(
341 plan(ColumnMove::Keep(column), 0, 10, ROOM, widths(&ws)),
342 Some(0)
343 );
344 }
345 assert_eq!(plan(ColumnMove::Keep(3), 0, 10, ROOM, widths(&ws)), Some(1));
347 assert_eq!(plan(ColumnMove::Keep(9), 0, 10, ROOM, widths(&ws)), Some(7));
348 assert_eq!(plan(ColumnMove::Keep(2), 5, 10, ROOM, widths(&ws)), Some(2));
350 let wide = [8, 8, 50, 8];
352 assert_eq!(
353 plan(ColumnMove::Keep(2), 0, 4, ROOM, widths(&wide)),
354 Some(2)
355 );
356 let mut seen = Vec::new();
358 plan(ColumnMove::Keep(4), 2, 10, ROOM, |i| {
359 seen.push(i);
360 Some(8)
361 });
362 assert!(seen.iter().all(|&i| i <= 4), "{seen:?}");
363 }
364
365 #[test]
366 fn no_columns_and_one_column() {
367 let ws: [u16; 0] = [];
368 for mv in [
369 ColumnMove::PageLeft,
370 ColumnMove::PageRight,
371 ColumnMove::Last,
372 ] {
373 assert_eq!(plan(mv, 0, 0, ROOM, widths(&ws)), Some(0));
374 }
375 let ws = [80];
376 for mv in [
377 ColumnMove::PageLeft,
378 ColumnMove::PageRight,
379 ColumnMove::Last,
380 ] {
381 assert_eq!(plan(mv, 0, 1, ROOM, widths(&ws)), Some(0));
382 }
383 }
384
385 #[test]
386 fn an_unknown_width_defers_the_plan() {
387 let known = |i: usize| (i < 4).then_some(8);
388 assert_eq!(plan(ColumnMove::PageRight, 0, 10, ROOM, known), None);
390 assert_eq!(plan(ColumnMove::Last, 0, 10, ROOM, known), None);
391 assert_eq!(plan(ColumnMove::PageLeft, 4, 10, ROOM, known), Some(1));
393 }
394}