Krolyxon
Commit e515e5a

Branches

Tags

Refs →

remagesrcstategrid.rs

347 lines · 10.9 KB · Rust

1//! Grid layout, cursor, scrolling and the visual-mode range.
2//!
3//! All sizes are physical pixels. Content coordinates start at the top of the first row's
4//! gap; the scroll offset is how far the content has moved up.
5
6use std::ops::Range;
7use std::time::{Duration, Instant};
8
9use super::view::Dir;
10use crate::anim::Tween;
11
12/// One frame's grid geometry.
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct Layout {
15 pub count: usize,
16 pub cols: usize,
17 /// Side of the square each thumbnail fits in.
18 pub cell: f32,
19 pub gap: f32,
20 /// Room under each cell for its name (0 without names).
21 pub label: f32,
22 /// x of the first column; the grid is centred.
23 pub left: f32,
24 pub viewport: [f32; 2],
25}
26
27impl Layout {
28 pub fn new(count: usize, viewport: [f32; 2], cell: f32, gap: f32, label: f32) -> Self {
29 let cell = cell.max(1.0);
30 let cols = ((viewport[0] - gap) / (cell + gap)).floor().max(1.0) as usize;
31 let used = cols as f32 * cell + (cols - 1) as f32 * gap;
32 Self {
33 count,
34 cols,
35 cell,
36 gap,
37 label,
38 left: ((viewport[0] - used) * 0.5).max(0.0).round(),
39 viewport,
40 }
41 }
42
43 pub fn row_height(&self) -> f32 {
44 self.cell + self.label + self.gap
45 }
46
47 pub fn rows(&self) -> usize {
48 self.count.div_ceil(self.cols)
49 }
50
51 pub fn row_of(&self, i: usize) -> usize {
52 i / self.cols
53 }
54
55 pub fn content_height(&self) -> f32 {
56 self.gap + self.rows() as f32 * self.row_height()
57 }
58
59 pub fn max_scroll(&self) -> f32 {
60 (self.content_height() - self.viewport[1]).max(0.0)
61 }
62
63 pub fn clamp_scroll(&self, s: f32) -> f32 {
64 s.clamp(0.0, self.max_scroll())
65 }
66
67 /// Top-left of cell `i` in content coordinates.
68 pub fn origin(&self, i: usize) -> [f32; 2] {
69 let (row, col) = (i / self.cols, i % self.cols);
70 [
71 self.left + col as f32 * (self.cell + self.gap),
72 self.gap + row as f32 * self.row_height(),
73 ]
74 }
75
76 /// Cells at least partly on screen at `scroll`.
77 pub fn visible(&self, scroll: f32) -> Range<usize> {
78 self.rows_between(scroll, scroll + self.viewport[1])
79 }
80
81 /// Cells whose rows overlap content `top..bottom`.
82 pub fn rows_between(&self, top: f32, bottom: f32) -> Range<usize> {
83 let rh = self.row_height();
84 let first = ((top - self.gap - self.cell - self.label) / rh)
85 .ceil()
86 .max(0.0) as usize;
87 let last = ((bottom - self.gap) / rh).ceil().max(0.0) as usize;
88 let start = (first * self.cols).min(self.count);
89 let end = (last * self.cols).min(self.count);
90 start..end.max(start)
91 }
92
93 /// Whole rows that fit on screen (at least one).
94 pub fn page_rows(&self) -> usize {
95 ((self.viewport[1] - self.gap) / self.row_height())
96 .floor()
97 .max(1.0) as usize
98 }
99
100 /// The cell under a viewport point.
101 pub fn hit(&self, p: [f32; 2], scroll: f32) -> Option<usize> {
102 let y = p[1] + scroll - self.gap;
103 let x = p[0] - self.left;
104 if x < 0.0 || y < 0.0 {
105 return None;
106 }
107 let (col, row) = (
108 (x / (self.cell + self.gap)) as usize,
109 (y / self.row_height()) as usize,
110 );
111 let in_cell = x - col as f32 * (self.cell + self.gap) <= self.cell
112 && y - row as f32 * self.row_height() <= self.cell + self.label;
113 let i = row * self.cols + col;
114 (col < self.cols && in_cell && i < self.count).then_some(i)
115 }
116
117 /// The scroll offset nearest to `scroll` that shows cell `i`'s row whole, gaps included.
118 pub fn follow(&self, i: usize, scroll: f32) -> f32 {
119 let top = self.row_of(i) as f32 * self.row_height();
120 let bottom = top + self.row_height() + self.gap;
121 let s = if top < scroll {
122 top
123 } else if bottom > scroll + self.viewport[1] {
124 bottom - self.viewport[1]
125 } else {
126 scroll
127 };
128 self.clamp_scroll(s)
129 }
130
131 /// Move the cursor `n` cells (left/right) or rows (up/down). Down onto a short last row
132 /// lands on the last cell.
133 pub fn step(&self, i: usize, dir: Dir, n: usize) -> usize {
134 if self.count == 0 {
135 return 0;
136 }
137 let last = self.count - 1;
138 match dir {
139 Dir::Left => i.saturating_sub(n),
140 Dir::Right => i.saturating_add(n).min(last),
141 Dir::Up => i - n.min(self.row_of(i)) * self.cols,
142 Dir::Down => {
143 let rows = n.min(self.row_of(last) - self.row_of(i));
144 (i + rows * self.cols).min(last)
145 }
146 }
147 }
148
149 /// Move by whole rows, clamping into the first or last row instead of stopping.
150 pub fn step_rows(&self, i: usize, rows: isize) -> usize {
151 if self.count == 0 {
152 return 0;
153 }
154 let target = i as isize + rows * self.cols as isize;
155 target.clamp(0, self.count as isize - 1) as usize
156 }
157
158 pub fn row_start(&self, i: usize) -> usize {
159 i - i % self.cols
160 }
161
162 pub fn row_end(&self, i: usize) -> usize {
163 (self.row_start(i) + self.cols - 1).min(self.count.saturating_sub(1))
164 }
165
166 /// Bring the cursor onto the screen at `scroll`, keeping its column.
167 pub fn pull_into_view(&self, i: usize, scroll: f32) -> usize {
168 if self.count == 0 {
169 return 0;
170 }
171 let rh = self.row_height();
172 let row = self.row_of(i) as f32;
173 // First and last rows fully on screen.
174 let first = (scroll / rh).ceil();
175 let last = ((scroll + self.viewport[1] - self.gap) / rh).floor() - 1.0;
176 let target = if row < first {
177 first
178 } else if row > last && last >= first {
179 last
180 } else {
181 return i;
182 };
183 let j = target as usize * self.cols + i % self.cols;
184 j.min(self.count - 1)
185 }
186}
187
188/// Grid view state that outlives a frame: size, scroll and the visual anchor.
189#[derive(Clone, Debug)]
190pub struct Grid {
191 /// Logical thumbnail size, one of `grid.sizes`.
192 pub size: u32,
193 scroll: Tween<f32>,
194 /// Visual mode's fixed end.
195 pub anchor: Option<usize>,
196}
197
198impl Grid {
199 pub fn new(size: u32, now: Instant) -> Self {
200 Self {
201 size,
202 scroll: Tween::new(0.0, now),
203 anchor: None,
204 }
205 }
206
207 /// The scroll offset to draw at `now`.
208 pub fn scroll(&self, layout: &Layout, now: Instant) -> f32 {
209 layout.clamp_scroll(self.scroll.value(now))
210 }
211
212 /// Where scrolling is heading.
213 pub fn target(&self, layout: &Layout) -> f32 {
214 layout.clamp_scroll(self.scroll.target())
215 }
216
217 pub fn scroll_to(&mut self, to: f32, now: Instant, dur: Duration) {
218 if (self.scroll.target() - to).abs() > 0.01 {
219 self.scroll.retarget(to, now, dur);
220 }
221 }
222
223 pub fn jump_to(&mut self, to: f32) {
224 self.scroll.set(to);
225 }
226
227 pub fn is_animating(&self, now: Instant) -> bool {
228 self.scroll.is_active(now)
229 }
230
231 /// The visual range, inclusive, given the cursor.
232 pub fn range(&self, cursor: usize) -> Option<Range<usize>> {
233 self.anchor.map(|a| a.min(cursor)..a.max(cursor) + 1)
234 }
235}
236
237/// Step through `sizes` from the entry nearest `current`.
238pub fn next_size(sizes: &[u32], current: u32, delta: i32) -> u32 {
239 if sizes.is_empty() {
240 return current;
241 }
242 let i = sizes
243 .iter()
244 .enumerate()
245 .min_by_key(|(_, s)| s.abs_diff(current))
246 .map_or(0, |(i, _)| i);
247 let j = (i as i64 + delta as i64).clamp(0, sizes.len() as i64 - 1) as usize;
248 sizes[j]
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 /// 10 cells of 100 with gap 10 in 450×300: 4 columns, 3 rows.
256 fn layout(count: usize) -> Layout {
257 Layout::new(count, [450.0, 300.0], 100.0, 10.0, 0.0)
258 }
259
260 #[test]
261 fn columns_are_centred() {
262 let l = layout(10);
263 assert_eq!(l.cols, 4);
264 assert_eq!(l.left, 10.0);
265 assert_eq!(l.rows(), 3);
266 assert_eq!(l.origin(5), [120.0, 120.0]);
267 assert_eq!(l.content_height(), 340.0);
268 assert_eq!(l.max_scroll(), 40.0);
269 // Never fewer than one column.
270 assert_eq!(Layout::new(3, [50.0, 50.0], 100.0, 10.0, 0.0).cols, 1);
271 }
272
273 #[test]
274 fn cursor_moves_like_nsxiv() {
275 let l = layout(10);
276 assert_eq!(l.step(0, Dir::Left, 1), 0);
277 assert_eq!(l.step(4, Dir::Left, 1), 3);
278 assert_eq!(l.step(3, Dir::Right, 1), 4);
279 assert_eq!(l.step(9, Dir::Right, 5), 9);
280 assert_eq!(l.step(5, Dir::Up, 1), 1);
281 assert_eq!(l.step(1, Dir::Up, 1), 1);
282 assert_eq!(l.step(5, Dir::Up, 9), 1);
283 assert_eq!(l.step(1, Dir::Down, 1), 5);
284 // Onto the short last row: the last cell.
285 assert_eq!(l.step(7, Dir::Down, 1), 9);
286 assert_eq!(l.step(9, Dir::Down, 1), 9);
287 assert_eq!(l.step(2, Dir::Down, 5), 9);
288 assert_eq!(l.row_start(6), 4);
289 assert_eq!(l.row_end(6), 7);
290 assert_eq!(l.row_end(8), 9);
291 assert_eq!(l.step_rows(2, 5), 9);
292 assert_eq!(l.step_rows(6, -5), 0);
293 assert_eq!(layout(0).step(0, Dir::Down, 1), 0);
294 }
295
296 #[test]
297 fn visibility_and_following() {
298 let l = layout(40); // 10 rows, content 1110 high
299 assert_eq!(l.visible(0.0), 0..12);
300 assert_eq!(l.visible(110.0), 0..16);
301 assert_eq!(l.visible(115.0), 4..16);
302 // Row 5 spans 550..670 including the gaps; a 300 viewport must scroll to 370.
303 assert_eq!(l.follow(20, 0.0), 370.0);
304 assert_eq!(l.follow(20, 400.0), 400.0);
305 assert_eq!(l.follow(0, 400.0), 0.0);
306 assert_eq!(l.page_rows(), 2);
307 }
308
309 #[test]
310 fn hit_testing() {
311 let l = layout(10);
312 assert_eq!(l.hit([15.0, 15.0], 0.0), Some(0));
313 assert_eq!(l.hit([125.0, 125.0], 0.0), Some(5));
314 // In a gap.
315 assert_eq!(l.hit([115.0, 15.0], 0.0), None);
316 // Past the last cell.
317 assert_eq!(l.hit([345.0, 235.0], 0.0), None);
318 assert_eq!(l.hit([15.0, 15.0], 110.0), Some(4));
319 }
320
321 #[test]
322 fn pulling_the_cursor_into_view() {
323 let l = layout(40);
324 assert_eq!(l.pull_into_view(1, 0.0), 1);
325 // Scrolled so rows 2 and 3 are whole: row 0 comes down to row 2.
326 assert_eq!(l.pull_into_view(1, 220.0), 9);
327 assert_eq!(l.pull_into_view(37, 220.0), 13);
328 }
329
330 #[test]
331 fn sizes_step_and_clamp() {
332 let s = [96, 128, 160, 224, 320];
333 assert_eq!(next_size(&s, 160, 1), 224);
334 assert_eq!(next_size(&s, 160, -2), 96);
335 assert_eq!(next_size(&s, 320, 1), 320);
336 assert_eq!(next_size(&s, 150, 0), 160);
337 }
338
339 #[test]
340 fn visual_range_is_inclusive() {
341 let mut g = Grid::new(160, Instant::now());
342 assert_eq!(g.range(3), None);
343 g.anchor = Some(5);
344 assert_eq!(g.range(2), Some(2..6));
345 assert_eq!(g.range(7), Some(5..8));
346 }
347}