teksilo_widgets/grid_view/layout/
uniform.rs1use std::cell::Cell;
12
13use teksilo_canvas::{EdgeInsets, Point};
14
15use super::columns::{ColumnGeometry, ColumnOptions, column_at, geometry_for, with_options};
16use super::strategy::{BUFFER_ROWS, GridLayoutStrategy, GridSizing, TileRect, VisibleTileRange};
17
18#[derive(Debug, Clone)]
20pub struct UniformGrid {
21 columns: Cell<ColumnGeometry>,
22 tile_height: f32,
23 row_gap: f32,
24 inset: EdgeInsets,
25}
26
27impl UniformGrid {
28 pub(crate) fn with_columns(self, sizing: GridSizing, options: ColumnOptions) -> Self {
31 self.columns
32 .set(with_options(self.columns.get(), sizing, options));
33 self
34 }
35
36 pub(crate) fn new(sizing: GridSizing, col_gap: f32, row_gap: f32, inset: EdgeInsets) -> Self {
38 Self {
39 columns: Cell::new(geometry_for(sizing, col_gap, inset)),
40 tile_height: sizing.tile_height().max(0.0),
41 row_gap: row_gap.max(0.0),
42 inset,
43 }
44 }
45
46 fn row_step(&self) -> f32 {
47 self.tile_height + self.row_gap
48 }
49
50 fn row_count(&self, item_count: usize, viewport_width: f32) -> usize {
51 if item_count == 0 {
52 return 0;
53 }
54 item_count.div_ceil(self.column_count(viewport_width).max(1))
55 }
56}
57
58impl GridLayoutStrategy for UniformGrid {
59 fn set_rtl(&self, rtl: bool) {
60 self.columns.set(self.columns.get().with_rtl(rtl));
61 }
62
63 fn column_count(&self, viewport_width: f32) -> usize {
64 self.columns.get().column_count(viewport_width)
65 }
66
67 fn column_x(&self, col: usize, viewport_width: f32) -> (f32, f32) {
68 self.columns.get().column_x(col, viewport_width)
69 }
70
71 fn total_content_height(&self, item_count: usize, viewport_width: f32) -> f32 {
72 let rows = self.row_count(item_count, viewport_width);
73 if rows == 0 {
74 return 0.0;
75 }
76 self.inset.top + rows as f32 * self.row_step() - self.row_gap + self.inset.bottom
77 }
78
79 fn visible_range(
80 &self,
81 scroll_y: f32,
82 viewport_height: f32,
83 viewport_width: f32,
84 item_count: usize,
85 ) -> VisibleTileRange {
86 if item_count == 0 || self.row_step() <= 0.0 {
87 return VisibleTileRange { start: 0, end: 0 };
88 }
89 let cols = self.column_count(viewport_width).max(1);
90 let row_step = self.row_step();
91 let content_scroll = (scroll_y - self.inset.top).max(0.0);
92 let first_row = (content_scroll / row_step).floor() as usize;
93 let last_row = ((content_scroll + viewport_height) / row_step).ceil() as usize;
94 let start_row = first_row.saturating_sub(BUFFER_ROWS);
95 let end_row = last_row + BUFFER_ROWS;
96 let start = (start_row * cols).min(item_count);
97 let end = (end_row.saturating_add(1).saturating_mul(cols)).min(item_count);
98 VisibleTileRange { start, end }
99 }
100
101 fn tile_rect(&self, index: usize, viewport_width: f32) -> TileRect {
102 let cols = self.column_count(viewport_width).max(1);
103 let row = index / cols;
104 let col = index % cols;
105 let (x, width) = self.column_x(col, viewport_width);
106 let y = self.inset.top + row as f32 * self.row_step();
107 TileRect {
108 x,
109 y,
110 width,
111 height: self.tile_height,
112 }
113 }
114
115 fn estimated_row_height(&self) -> f32 {
116 self.tile_height
117 }
118
119 fn index_at_point(
120 &self,
121 content_point: Point,
122 item_count: usize,
123 viewport_width: f32,
124 ) -> Option<usize> {
125 if item_count == 0 || self.row_step() <= 0.0 {
126 return None;
127 }
128 let y = content_point.y - self.inset.top;
129 if y < 0.0 {
130 return None;
131 }
132 let row = (y / self.row_step()) as usize;
133 if y - row as f32 * self.row_step() > self.tile_height {
134 return None; }
136 let col = column_at(&self.columns.get(), content_point.x, viewport_width)?;
137 let cols = self.column_count(viewport_width);
138 let idx = row * cols + col;
139 (idx < item_count).then_some(idx)
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 fn grid() -> UniformGrid {
148 UniformGrid::new(
150 GridSizing::Fixed {
151 width: 100.0,
152 height: 50.0,
153 },
154 10.0,
155 10.0,
156 EdgeInsets::ZERO,
157 )
158 }
159
160 #[test]
161 fn index_at_point_closed_form_matches_exact_edges() {
162 let g = grid();
163 assert_eq!(g.index_at_point(Point::new(0.0, 0.0), 12, 430.0), Some(0));
165 assert_eq!(
166 g.index_at_point(Point::new(100.0, 50.0), 12, 430.0),
167 Some(0)
168 );
169 assert_eq!(g.index_at_point(Point::new(0.0, 60.0), 12, 430.0), Some(4));
171 }
172
173 #[test]
174 fn index_at_point_closed_form_returns_none_in_gaps() {
175 let g = grid();
176 assert_eq!(g.index_at_point(Point::new(50.0, 55.0), 12, 430.0), None);
178 assert_eq!(g.index_at_point(Point::new(105.0, 25.0), 12, 430.0), None);
180 assert_eq!(g.index_at_point(Point::new(0.0, 9000.0), 12, 430.0), None);
182 assert_eq!(g.index_at_point(Point::new(0.0, -5.0), 12, 430.0), None);
184 }
185}