use std::ops::Range;
use teksilo_canvas::Rect;
pub(crate) const BUFFER_ROWS: usize = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScrollAnchor {
#[default]
Auto,
Start,
Center,
End,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GridSizing {
Fixed { width: f32, height: f32 },
FixedColumnCount { count: usize, height: f32 },
Adaptive {
min_width: f32,
max_width: Option<f32>,
height: f32,
},
}
impl GridSizing {
pub(crate) fn tile_height(&self) -> f32 {
match *self {
GridSizing::Fixed { height, .. }
| GridSizing::FixedColumnCount { height, .. }
| GridSizing::Adaptive { height, .. } => height,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TileRect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VisibleTileRange {
pub start: usize,
pub end: usize,
}
pub(crate) trait GridLayoutStrategy: std::fmt::Debug + 'static {
fn column_count(&self, viewport_width: f32) -> usize;
fn column_x(&self, col: usize, viewport_width: f32) -> (f32, f32);
fn total_content_height(&self, item_count: usize, viewport_width: f32) -> f32;
fn total_content_width(&self, viewport_width: f32) -> f32 {
viewport_width
}
fn visible_range(
&self,
scroll_y: f32,
viewport_height: f32,
viewport_width: f32,
item_count: usize,
) -> VisibleTileRange;
fn tile_rect(&self, index: usize, viewport_width: f32) -> TileRect;
fn estimated_row_height(&self) -> f32;
fn measures_tiles(&self) -> bool {
false
}
fn observe_measured(
&self,
_measured: &[(usize, f32)],
_scroll_y: f32,
_viewport_width: f32,
) -> f32 {
0.0
}
fn invalidate_rows(&self, _item_range: Range<usize>) {}
fn resize(&self, _item_count: usize) {}
fn scroll_delta_to_reveal(
&self,
index: usize,
scroll_y: f32,
viewport_height: f32,
viewport_width: f32,
anchor: ScrollAnchor,
) -> f32 {
let r = self.tile_rect(index, viewport_width);
let tile_top = r.y;
let tile_bot = r.y + r.height;
match anchor {
ScrollAnchor::Start => tile_top - scroll_y,
ScrollAnchor::End => tile_bot - viewport_height - scroll_y,
ScrollAnchor::Center => (tile_top + r.height * 0.5) - viewport_height * 0.5 - scroll_y,
ScrollAnchor::Auto => {
if tile_top < scroll_y {
tile_top - scroll_y
} else if tile_bot > scroll_y + viewport_height {
tile_bot - (scroll_y + viewport_height)
} else {
0.0
}
}
}
}
fn hit_indices_in_rect(
&self,
content_rect: Rect,
item_count: usize,
viewport_width: f32,
) -> Vec<usize> {
let mut hits = Vec::new();
for i in 0..item_count {
let r = self.tile_rect(i, viewport_width);
let tile = Rect::new(r.x, r.y, r.width, r.height);
if rects_intersect(content_rect, tile) {
hits.push(i);
}
}
hits
}
fn index_at_point(
&self,
content_point: teksilo_canvas::Point,
item_count: usize,
viewport_width: f32,
) -> Option<usize> {
for i in 0..item_count {
let r = self.tile_rect(i, viewport_width);
if Rect::new(r.x, r.y, r.width, r.height).contains(content_point) {
return Some(i);
}
}
None
}
fn insertion_index_at(
&self,
content_point: teksilo_canvas::Point,
item_count: usize,
viewport_width: f32,
) -> usize {
if item_count == 0 {
return 0;
}
let last = self.tile_rect(item_count - 1, viewport_width);
if content_point.y >= last.y + last.height {
return item_count;
}
if let Some(i) = self.index_at_point(content_point, item_count, viewport_width) {
let r = self.tile_rect(i, viewport_width);
return if content_point.x > r.x + r.width * 0.5 {
(i + 1).min(item_count)
} else {
i
};
}
let mut best = 0usize;
let mut best_dy = f32::MAX;
let mut best_dx = f32::MAX;
for i in 0..item_count {
let r = self.tile_rect(i, viewport_width);
let dy = edge_gap(content_point.y, r.y, r.height);
let dx = edge_gap(content_point.x, r.x, r.width);
if dy < best_dy - 0.01 || ((dy - best_dy).abs() <= 0.01 && dx < best_dx) {
best = i;
best_dy = dy;
best_dx = dx;
}
}
let r = self.tile_rect(best, viewport_width);
if content_point.x > r.x + r.width * 0.5 {
(best + 1).min(item_count)
} else {
best
}
}
fn tile_row_col(&self, index: usize, viewport_width: f32) -> (usize, usize) {
let cols = self.column_count(viewport_width).max(1);
(index / cols, index % cols)
}
fn headers_in_range(
&self,
_scroll_y: f32,
_viewport_height: f32,
_viewport_width: f32,
) -> Vec<(usize, TileRect)> {
Vec::new()
}
fn current_section(&self, _scroll_y: f32, _viewport_width: f32) -> Option<usize> {
None
}
fn header_rect(&self, _section: usize, _viewport_width: f32) -> Option<TileRect> {
None
}
}
pub(crate) fn rects_intersect(a: Rect, b: Rect) -> bool {
a.x < b.right() && b.x < a.right() && a.y < b.bottom() && b.y < a.bottom()
}
fn edge_gap(p: f32, origin: f32, extent: f32) -> f32 {
if p < origin {
origin - p
} else if p > origin + extent {
p - (origin + extent)
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grid_view::layout::uniform::UniformGrid;
use teksilo_canvas::{EdgeInsets, Point};
fn grid() -> UniformGrid {
UniformGrid::new(
GridSizing::Fixed {
width: 100.0,
height: 50.0,
},
0.0,
0.0,
EdgeInsets::ZERO,
)
}
fn gapped_grid() -> UniformGrid {
UniformGrid::new(
GridSizing::Fixed {
width: 100.0,
height: 50.0,
},
10.0,
10.0,
EdgeInsets::ZERO,
)
}
#[test]
fn hit_indices_in_rect_selects_intersecting_tiles() {
let g = grid();
let rect = Rect::new(10.0, 10.0, 150.0, 60.0);
let mut hits = g.hit_indices_in_rect(rect, 40, 400.0);
hits.sort();
assert_eq!(hits, vec![0, 1, 4, 5]);
}
#[test]
fn index_at_point_finds_tile_and_gap() {
let g = grid();
assert_eq!(
g.index_at_point(Point::new(250.0, 25.0), 40, 400.0),
Some(2)
);
assert_eq!(g.index_at_point(Point::new(10.0, 60.0), 40, 400.0), Some(4));
assert_eq!(g.index_at_point(Point::new(10.0, 9000.0), 40, 400.0), None);
}
#[test]
fn insertion_index_at_row_gap_does_not_fall_through_to_len() {
let g = gapped_grid();
let idx = g.insertion_index_at(Point::new(50.0, 53.0), 12, 430.0);
assert!(
idx < 12,
"a mid-grid row-gap point must not fall through to len, got {idx}"
);
}
#[test]
fn insertion_index_at_col_gap_yields_next_tile() {
let g = gapped_grid();
let idx = g.insertion_index_at(Point::new(105.0, 25.0), 12, 430.0);
assert_eq!(
idx, 1,
"a point in the col-gap between tiles 0 and 1 should insert before tile 1"
);
}
#[test]
fn insertion_index_at_past_last_tile_yields_len() {
let g = gapped_grid();
let idx = g.insertion_index_at(Point::new(50.0, 9000.0), 12, 430.0);
assert_eq!(
idx, 12,
"a point past the last tile should append at the end"
);
}
}