use std::collections::HashMap;
use std::ops::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ItemIndex(pub usize);
impl ItemIndex {
pub fn as_usize(self) -> usize {
self.0
}
}
impl From<usize> for ItemIndex {
fn from(v: usize) -> Self {
Self(v)
}
}
#[derive(Debug, Clone)]
pub struct VirtualListConfig {
pub estimated_item_height: f32,
pub overscan_count: usize,
pub variable_heights: bool,
pub initial_scroll: f32,
pub load_more_threshold: f32,
}
impl Default for VirtualListConfig {
fn default() -> Self {
Self {
estimated_item_height: 50.0,
overscan_count: 3,
variable_heights: false,
initial_scroll: 0.0,
load_more_threshold: 100.0,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct VisibleRange {
pub start: usize,
pub end: usize,
pub render_start: usize,
pub render_end: usize,
pub offset: f32,
}
impl VisibleRange {
pub fn visible_range(&self) -> Range<usize> {
self.start..self.end
}
pub fn render_range(&self) -> Range<usize> {
self.render_start..self.render_end
}
pub fn is_visible(&self, index: usize) -> bool {
index >= self.start && index < self.end
}
pub fn should_render(&self, index: usize) -> bool {
index >= self.render_start && index < self.render_end
}
pub fn visible_count(&self) -> usize {
self.end.saturating_sub(self.start)
}
pub fn render_count(&self) -> usize {
self.render_end.saturating_sub(self.render_start)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ItemLayout {
pub y: f32,
pub height: f32,
}
impl ItemLayout {
pub fn new(y: f32, height: f32) -> Self {
Self { y, height }
}
pub fn bottom(&self) -> f32 {
self.y + self.height
}
}
pub struct VirtualList {
config: VirtualListConfig,
item_count: usize,
item_heights: HashMap<usize, f32>,
item_positions: Vec<f32>,
positions_dirty: bool,
scroll_position: f32,
viewport_height: f32,
content_height: f32,
visible_range: Option<VisibleRange>,
}
impl Default for VirtualList {
fn default() -> Self {
Self::new(VirtualListConfig::default())
}
}
impl VirtualList {
pub fn new(config: VirtualListConfig) -> Self {
let initial_scroll = config.initial_scroll;
Self {
config,
item_count: 0,
item_heights: HashMap::new(),
item_positions: Vec::new(),
positions_dirty: true,
scroll_position: initial_scroll,
viewport_height: 0.0,
content_height: 0.0,
visible_range: None,
}
}
pub fn set_item_count(&mut self, count: usize) {
if count != self.item_count {
self.item_count = count;
self.positions_dirty = true;
}
}
pub fn item_count(&self) -> usize {
self.item_count
}
pub fn set_viewport_height(&mut self, height: f32) {
if (height - self.viewport_height).abs() > 0.1 {
self.viewport_height = height;
self.update_visible_range();
}
}
pub fn viewport_height(&self) -> f32 {
self.viewport_height
}
pub fn set_scroll_position(&mut self, position: f32) {
let clamped = position.max(0.0).min(self.max_scroll());
if (clamped - self.scroll_position).abs() > 0.1 {
self.scroll_position = clamped;
self.update_visible_range();
}
}
pub fn scroll_position(&self) -> f32 {
self.scroll_position
}
pub fn max_scroll(&self) -> f32 {
(self.calculate_content_height() - self.viewport_height).max(0.0)
}
fn calculate_content_height(&self) -> f32 {
if !self.config.variable_heights {
return self.item_count as f32 * self.config.estimated_item_height;
}
let mut height = 0.0;
for i in 0..self.item_count {
height += self.get_item_height(i);
}
height
}
pub fn scroll_by(&mut self, delta: f32) {
self.set_scroll_position(self.scroll_position + delta);
}
pub fn scroll_to_item(&mut self, index: usize, align: ScrollAlign) {
if index >= self.item_count {
return;
}
if self.positions_dirty {
self.recalculate_positions();
}
let item_y = self.get_item_position(index);
let item_height = self.get_item_height(index);
let new_scroll = match align {
ScrollAlign::Start => item_y,
ScrollAlign::Center => item_y - (self.viewport_height - item_height) / 2.0,
ScrollAlign::End => item_y - self.viewport_height + item_height,
ScrollAlign::Auto => {
if item_y < self.scroll_position {
item_y
} else if item_y + item_height > self.scroll_position + self.viewport_height {
item_y + item_height - self.viewport_height
} else {
self.scroll_position
}
}
};
self.set_scroll_position(new_scroll);
}
pub fn set_item_height(&mut self, index: usize, height: f32) {
if self.config.variable_heights {
self.item_heights.insert(index, height);
self.positions_dirty = true;
}
}
pub fn get_item_height(&self, index: usize) -> f32 {
if self.config.variable_heights {
self.item_heights
.get(&index)
.copied()
.unwrap_or(self.config.estimated_item_height)
} else {
self.config.estimated_item_height
}
}
pub fn get_item_position(&self, index: usize) -> f32 {
if index == 0 {
return 0.0;
}
if !self.config.variable_heights {
return index as f32 * self.config.estimated_item_height;
}
if index < self.item_positions.len() {
self.item_positions[index]
} else {
let mut y = 0.0;
for i in 0..index {
y += self.get_item_height(i);
}
y
}
}
pub fn get_item_layout(&self, index: usize) -> ItemLayout {
ItemLayout {
y: self.get_item_position(index),
height: self.get_item_height(index),
}
}
pub fn content_height(&self) -> f32 {
self.calculate_content_height()
}
pub fn visible_range(&self) -> Option<&VisibleRange> {
self.visible_range.as_ref()
}
pub fn is_near_end(&self) -> bool {
self.scroll_position + self.viewport_height + self.config.load_more_threshold
>= self.content_height
}
pub fn is_near_start(&self) -> bool {
self.scroll_position <= self.config.load_more_threshold
}
fn update_visible_range(&mut self) {
if self.positions_dirty {
self.recalculate_positions();
}
if self.item_count == 0 || self.viewport_height <= 0.0 {
self.visible_range = None;
return;
}
let start = self.find_item_at_position(self.scroll_position);
let end = self.find_item_at_position(self.scroll_position + self.viewport_height) + 1;
let end = end.min(self.item_count);
let render_start = start.saturating_sub(self.config.overscan_count);
let render_end = (end + self.config.overscan_count).min(self.item_count);
let offset = self.get_item_position(render_start);
self.visible_range = Some(VisibleRange {
start,
end,
render_start,
render_end,
offset,
});
}
fn find_item_at_position(&self, position: f32) -> usize {
if position <= 0.0 {
return 0;
}
if !self.config.variable_heights {
return (position / self.config.estimated_item_height) as usize;
}
let mut low = 0;
let mut high = self.item_count;
while low < high {
let mid = (low + high) / 2;
let item_pos = self.get_item_position(mid);
if item_pos <= position {
low = mid + 1;
} else {
high = mid;
}
}
low.saturating_sub(1)
}
fn recalculate_positions(&mut self) {
self.item_positions.clear();
self.item_positions.reserve(self.item_count);
let mut current_y = 0.0;
for i in 0..self.item_count {
self.item_positions.push(current_y);
current_y += self.get_item_height(i);
}
self.content_height = current_y;
self.positions_dirty = false;
}
pub fn reset(&mut self) {
self.scroll_position = 0.0;
self.visible_range = None;
self.update_visible_range();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollAlign {
Start,
Center,
End,
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GridCell {
pub row: usize,
pub col: usize,
}
impl GridCell {
pub fn new(row: usize, col: usize) -> Self {
Self { row, col }
}
}
#[derive(Debug, Clone)]
pub struct VirtualGridConfig {
pub columns: usize,
pub cell_width: f32,
pub cell_height: f32,
pub gap: f32,
pub overscan_rows: usize,
}
impl Default for VirtualGridConfig {
fn default() -> Self {
Self {
columns: 3,
cell_width: 100.0,
cell_height: 100.0,
gap: 8.0,
overscan_rows: 2,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct VisibleGridRange {
pub start_row: usize,
pub end_row: usize,
pub render_start_row: usize,
pub render_end_row: usize,
pub columns: usize,
pub offset: f32,
}
impl VisibleGridRange {
pub fn cells_to_render(&self, total_items: usize) -> Vec<GridCell> {
let mut cells = Vec::new();
for row in self.render_start_row..self.render_end_row {
for col in 0..self.columns {
let index = row * self.columns + col;
if index < total_items {
cells.push(GridCell::new(row, col));
}
}
}
cells
}
pub fn should_render_cell(&self, row: usize, col: usize) -> bool {
row >= self.render_start_row && row < self.render_end_row && col < self.columns
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CellLayout {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl CellLayout {
pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
x,
y,
width,
height,
}
}
}
pub struct VirtualGrid {
config: VirtualGridConfig,
item_count: usize,
scroll_position: f32,
viewport_height: f32,
visible_range: Option<VisibleGridRange>,
}
impl Default for VirtualGrid {
fn default() -> Self {
Self::new(VirtualGridConfig::default())
}
}
impl VirtualGrid {
pub fn new(config: VirtualGridConfig) -> Self {
Self {
config,
item_count: 0,
scroll_position: 0.0,
viewport_height: 0.0,
visible_range: None,
}
}
pub fn set_item_count(&mut self, count: usize) {
if count != self.item_count {
self.item_count = count;
self.update_visible_range();
}
}
pub fn item_count(&self) -> usize {
self.item_count
}
pub fn row_count(&self) -> usize {
self.item_count.div_ceil(self.config.columns)
}
pub fn set_viewport_height(&mut self, height: f32) {
if (height - self.viewport_height).abs() > 0.1 {
self.viewport_height = height;
self.update_visible_range();
}
}
pub fn viewport_height(&self) -> f32 {
self.viewport_height
}
pub fn set_scroll_position(&mut self, position: f32) {
let clamped = position.max(0.0).min(self.max_scroll());
if (clamped - self.scroll_position).abs() > 0.1 {
self.scroll_position = clamped;
self.update_visible_range();
}
}
pub fn scroll_position(&self) -> f32 {
self.scroll_position
}
pub fn max_scroll(&self) -> f32 {
(self.content_height() - self.viewport_height).max(0.0)
}
pub fn scroll_by(&mut self, delta: f32) {
self.set_scroll_position(self.scroll_position + delta);
}
pub fn scroll_to_item(&mut self, index: usize, align: ScrollAlign) {
if index >= self.item_count {
return;
}
let row = index / self.config.columns;
let row_y = self.row_position(row);
let new_scroll = match align {
ScrollAlign::Start => row_y,
ScrollAlign::Center => row_y - (self.viewport_height - self.row_height()) / 2.0,
ScrollAlign::End => row_y - self.viewport_height + self.row_height(),
ScrollAlign::Auto => {
if row_y < self.scroll_position {
row_y
} else if row_y + self.row_height() > self.scroll_position + self.viewport_height {
row_y + self.row_height() - self.viewport_height
} else {
self.scroll_position
}
}
};
self.set_scroll_position(new_scroll);
}
pub fn row_height(&self) -> f32 {
self.config.cell_height + self.config.gap
}
pub fn row_position(&self, row: usize) -> f32 {
row as f32 * self.row_height()
}
pub fn content_height(&self) -> f32 {
let rows = self.row_count();
if rows == 0 {
0.0
} else {
(rows as f32).mul_add(self.config.cell_height, (rows - 1) as f32 * self.config.gap)
}
}
pub fn get_cell_layout(&self, index: usize) -> CellLayout {
let row = index / self.config.columns;
let col = index % self.config.columns;
self.get_cell_layout_by_position(row, col)
}
pub fn get_cell_layout_by_position(&self, row: usize, col: usize) -> CellLayout {
let x = col as f32 * (self.config.cell_width + self.config.gap);
let y = row as f32 * (self.config.cell_height + self.config.gap);
CellLayout::new(x, y, self.config.cell_width, self.config.cell_height)
}
pub fn cell_to_index(&self, cell: &GridCell) -> usize {
cell.row * self.config.columns + cell.col
}
pub fn index_to_cell(&self, index: usize) -> GridCell {
GridCell {
row: index / self.config.columns,
col: index % self.config.columns,
}
}
pub fn visible_range(&self) -> Option<&VisibleGridRange> {
self.visible_range.as_ref()
}
fn update_visible_range(&mut self) {
if self.item_count == 0 || self.viewport_height <= 0.0 {
self.visible_range = None;
return;
}
let row_height = self.row_height();
let start_row = (self.scroll_position / row_height) as usize;
let visible_rows = (self.viewport_height / row_height).ceil() as usize + 1;
let end_row = (start_row + visible_rows).min(self.row_count());
let render_start_row = start_row.saturating_sub(self.config.overscan_rows);
let render_end_row = (end_row + self.config.overscan_rows).min(self.row_count());
let offset = render_start_row as f32 * row_height;
self.visible_range = Some(VisibleGridRange {
start_row,
end_row,
render_start_row,
render_end_row,
columns: self.config.columns,
offset,
});
}
pub fn reset(&mut self) {
self.scroll_position = 0.0;
self.visible_range = None;
self.update_visible_range();
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_virtual_list_default() {
let list = VirtualList::default();
assert_eq!(list.item_count(), 0);
assert_eq!(list.scroll_position(), 0.0);
}
#[test]
fn test_virtual_list_set_item_count() {
let mut list = VirtualList::default();
list.set_item_count(100);
assert_eq!(list.item_count(), 100);
}
#[test]
fn test_virtual_list_viewport() {
let mut list = VirtualList::default();
list.set_viewport_height(500.0);
assert_eq!(list.viewport_height(), 500.0);
}
#[test]
fn test_virtual_list_scroll_position() {
let mut list = VirtualList::default();
list.set_item_count(100);
list.set_viewport_height(500.0);
list.set_scroll_position(100.0);
assert_eq!(list.scroll_position(), 100.0);
}
#[test]
fn test_virtual_list_scroll_clamped() {
let mut list = VirtualList::default();
list.set_item_count(10);
list.set_viewport_height(500.0);
list.set_scroll_position(-100.0);
assert_eq!(list.scroll_position(), 0.0);
}
#[test]
fn test_virtual_list_scroll_by() {
let mut list = VirtualList::default();
list.set_item_count(100);
list.set_viewport_height(500.0);
list.scroll_by(50.0);
assert_eq!(list.scroll_position(), 50.0);
list.scroll_by(25.0);
assert_eq!(list.scroll_position(), 75.0);
}
#[test]
fn test_virtual_list_content_height() {
let config = VirtualListConfig {
estimated_item_height: 40.0,
..Default::default()
};
let mut list = VirtualList::new(config);
list.set_item_count(10);
assert_eq!(list.content_height(), 400.0);
}
#[test]
fn test_virtual_list_max_scroll() {
let config = VirtualListConfig {
estimated_item_height: 50.0,
..Default::default()
};
let mut list = VirtualList::new(config);
list.set_item_count(20);
list.set_viewport_height(400.0);
assert_eq!(list.max_scroll(), 600.0);
}
#[test]
fn test_virtual_list_visible_range() {
let config = VirtualListConfig {
estimated_item_height: 50.0,
overscan_count: 2,
..Default::default()
};
let mut list = VirtualList::new(config);
list.set_item_count(100);
list.set_viewport_height(200.0);
let range = list.visible_range().unwrap();
assert_eq!(range.start, 0);
assert_eq!(range.end, 5);
assert_eq!(range.render_start, 0);
assert_eq!(range.render_end, 7); }
#[test]
fn test_virtual_list_visible_range_scrolled() {
let config = VirtualListConfig {
estimated_item_height: 50.0,
overscan_count: 2,
..Default::default()
};
let mut list = VirtualList::new(config);
list.set_item_count(100);
list.set_viewport_height(200.0);
list.set_scroll_position(250.0);
let range = list.visible_range().unwrap();
assert_eq!(range.start, 5);
assert_eq!(range.end, 10);
assert_eq!(range.render_start, 3); assert_eq!(range.render_end, 12); }
#[test]
fn test_virtual_list_scroll_to_item_start() {
let config = VirtualListConfig {
estimated_item_height: 50.0,
..Default::default()
};
let mut list = VirtualList::new(config);
list.set_item_count(100);
list.set_viewport_height(200.0);
list.scroll_to_item(10, ScrollAlign::Start);
assert_eq!(list.scroll_position(), 500.0);
}
#[test]
fn test_virtual_list_scroll_to_item_center() {
let config = VirtualListConfig {
estimated_item_height: 50.0,
..Default::default()
};
let mut list = VirtualList::new(config);
list.set_item_count(100);
list.set_viewport_height(200.0);
list.scroll_to_item(10, ScrollAlign::Center);
assert_eq!(list.scroll_position(), 425.0);
}
#[test]
fn test_virtual_list_scroll_to_item_end() {
let config = VirtualListConfig {
estimated_item_height: 50.0,
..Default::default()
};
let mut list = VirtualList::new(config);
list.set_item_count(100);
list.set_viewport_height(200.0);
list.scroll_to_item(10, ScrollAlign::End);
assert_eq!(list.scroll_position(), 350.0);
}
#[test]
fn test_virtual_list_scroll_to_item_auto() {
let config = VirtualListConfig {
estimated_item_height: 50.0,
..Default::default()
};
let mut list = VirtualList::new(config);
list.set_item_count(100);
list.set_viewport_height(200.0);
list.scroll_to_item(2, ScrollAlign::Auto);
assert_eq!(list.scroll_position(), 0.0);
list.scroll_to_item(10, ScrollAlign::Auto);
assert!(list.scroll_position() > 0.0);
}
#[test]
fn test_virtual_list_variable_heights() {
let config = VirtualListConfig {
estimated_item_height: 50.0,
variable_heights: true,
..Default::default()
};
let mut list = VirtualList::new(config);
list.set_item_count(10);
list.set_item_height(2, 100.0);
assert_eq!(list.get_item_height(2), 100.0);
assert_eq!(list.get_item_height(3), 50.0); }
#[test]
fn test_virtual_list_item_layout() {
let config = VirtualListConfig {
estimated_item_height: 50.0,
..Default::default()
};
let mut list = VirtualList::new(config);
list.set_item_count(10);
let layout = list.get_item_layout(5);
assert_eq!(layout.y, 250.0);
assert_eq!(layout.height, 50.0);
}
#[test]
fn test_virtual_list_is_near_end() {
let config = VirtualListConfig {
estimated_item_height: 50.0,
load_more_threshold: 100.0,
..Default::default()
};
let mut list = VirtualList::new(config);
list.set_item_count(20); list.set_viewport_height(300.0);
assert!(!list.is_near_end());
list.set_scroll_position(600.0); assert!(list.is_near_end());
}
#[test]
fn test_virtual_list_is_near_start() {
let config = VirtualListConfig {
load_more_threshold: 100.0,
..Default::default()
};
let mut list = VirtualList::new(config);
list.set_item_count(100);
list.set_viewport_height(300.0);
assert!(list.is_near_start());
list.set_scroll_position(200.0);
assert!(!list.is_near_start());
}
#[test]
fn test_virtual_list_reset() {
let mut list = VirtualList::default();
list.set_item_count(100);
list.set_viewport_height(300.0);
list.set_scroll_position(500.0);
list.reset();
assert_eq!(list.scroll_position(), 0.0);
}
#[test]
fn test_visible_range_methods() {
let range = VisibleRange {
start: 5,
end: 10,
render_start: 3,
render_end: 12,
offset: 150.0,
};
assert_eq!(range.visible_range(), 5..10);
assert_eq!(range.render_range(), 3..12);
assert_eq!(range.visible_count(), 5);
assert_eq!(range.render_count(), 9);
assert!(range.is_visible(7));
assert!(!range.is_visible(2));
assert!(range.should_render(5));
assert!(!range.should_render(15));
}
#[test]
fn test_item_layout() {
let layout = ItemLayout::new(100.0, 50.0);
assert_eq!(layout.y, 100.0);
assert_eq!(layout.height, 50.0);
assert_eq!(layout.bottom(), 150.0);
}
#[test]
fn test_item_index() {
let index = ItemIndex(42);
assert_eq!(index.as_usize(), 42);
let from_usize: ItemIndex = 100.into();
assert_eq!(from_usize.0, 100);
}
#[test]
fn test_virtual_grid_default() {
let grid = VirtualGrid::default();
assert_eq!(grid.item_count(), 0);
assert_eq!(grid.scroll_position(), 0.0);
}
#[test]
fn test_virtual_grid_set_item_count() {
let mut grid = VirtualGrid::default();
grid.set_item_count(100);
assert_eq!(grid.item_count(), 100);
}
#[test]
fn test_virtual_grid_row_count() {
let config = VirtualGridConfig {
columns: 3,
..Default::default()
};
let mut grid = VirtualGrid::new(config);
grid.set_item_count(10);
assert_eq!(grid.row_count(), 4); }
#[test]
fn test_virtual_grid_viewport() {
let mut grid = VirtualGrid::default();
grid.set_viewport_height(500.0);
assert_eq!(grid.viewport_height(), 500.0);
}
#[test]
fn test_virtual_grid_scroll_position() {
let mut grid = VirtualGrid::default();
grid.set_item_count(100);
grid.set_viewport_height(500.0);
grid.set_scroll_position(200.0);
assert_eq!(grid.scroll_position(), 200.0);
}
#[test]
fn test_virtual_grid_content_height() {
let config = VirtualGridConfig {
columns: 3,
cell_height: 100.0,
gap: 10.0,
..Default::default()
};
let mut grid = VirtualGrid::new(config);
grid.set_item_count(9);
assert_eq!(grid.content_height(), 320.0);
}
#[test]
fn test_virtual_grid_cell_layout() {
let config = VirtualGridConfig {
columns: 3,
cell_width: 100.0,
cell_height: 80.0,
gap: 10.0,
..Default::default()
};
let grid = VirtualGrid::new(config);
let layout = grid.get_cell_layout(0);
assert_eq!(layout.x, 0.0);
assert_eq!(layout.y, 0.0);
let layout = grid.get_cell_layout(1);
assert_eq!(layout.x, 110.0);
assert_eq!(layout.y, 0.0);
let layout = grid.get_cell_layout(3);
assert_eq!(layout.x, 0.0);
assert_eq!(layout.y, 90.0);
}
#[test]
fn test_virtual_grid_cell_conversion() {
let config = VirtualGridConfig {
columns: 4,
..Default::default()
};
let grid = VirtualGrid::new(config);
let cell = grid.index_to_cell(10);
assert_eq!(cell.row, 2);
assert_eq!(cell.col, 2);
assert_eq!(grid.cell_to_index(&cell), 10);
}
#[test]
fn test_virtual_grid_visible_range() {
let config = VirtualGridConfig {
columns: 3,
cell_height: 100.0,
gap: 10.0,
overscan_rows: 1,
..Default::default()
};
let mut grid = VirtualGrid::new(config);
grid.set_item_count(30); grid.set_viewport_height(250.0);
let range = grid.visible_range().unwrap();
assert_eq!(range.start_row, 0);
assert!(range.end_row >= 2);
}
#[test]
fn test_virtual_grid_scroll_to_item() {
let config = VirtualGridConfig {
columns: 3,
cell_height: 100.0,
gap: 10.0,
..Default::default()
};
let mut grid = VirtualGrid::new(config);
grid.set_item_count(30);
grid.set_viewport_height(250.0);
grid.scroll_to_item(15, ScrollAlign::Start); assert_eq!(grid.scroll_position(), 550.0); }
#[test]
fn test_virtual_grid_reset() {
let mut grid = VirtualGrid::default();
grid.set_item_count(100);
grid.set_viewport_height(300.0);
grid.set_scroll_position(500.0);
grid.reset();
assert_eq!(grid.scroll_position(), 0.0);
}
#[test]
fn test_grid_cell() {
let cell = GridCell::new(5, 2);
assert_eq!(cell.row, 5);
assert_eq!(cell.col, 2);
}
#[test]
fn test_visible_grid_range_cells() {
let range = VisibleGridRange {
start_row: 2,
end_row: 5,
render_start_row: 1,
render_end_row: 6,
columns: 3,
offset: 100.0,
};
let cells = range.cells_to_render(100);
assert_eq!(cells.len(), 15);
let cells = range.cells_to_render(10);
assert_eq!(cells.len(), 7);
}
#[test]
fn test_visible_grid_range_should_render() {
let range = VisibleGridRange {
start_row: 2,
end_row: 5,
render_start_row: 1,
render_end_row: 6,
columns: 3,
offset: 100.0,
};
assert!(range.should_render_cell(3, 1));
assert!(!range.should_render_cell(0, 0));
assert!(!range.should_render_cell(3, 5)); }
#[test]
fn test_cell_layout() {
let layout = CellLayout::new(100.0, 200.0, 50.0, 60.0);
assert_eq!(layout.x, 100.0);
assert_eq!(layout.y, 200.0);
assert_eq!(layout.width, 50.0);
assert_eq!(layout.height, 60.0);
}
#[test]
fn test_scroll_align_variants() {
assert_ne!(ScrollAlign::Start, ScrollAlign::End);
assert_ne!(ScrollAlign::Center, ScrollAlign::Auto);
}
#[test]
fn test_virtual_list_empty() {
let mut list = VirtualList::default();
list.set_viewport_height(300.0);
assert!(list.visible_range().is_none());
}
#[test]
fn test_virtual_grid_empty() {
let mut grid = VirtualGrid::default();
grid.set_viewport_height(300.0);
assert!(grid.visible_range().is_none());
}
#[test]
fn test_virtual_list_config_default() {
let config = VirtualListConfig::default();
assert_eq!(config.estimated_item_height, 50.0);
assert_eq!(config.overscan_count, 3);
assert!(!config.variable_heights);
assert_eq!(config.initial_scroll, 0.0);
assert_eq!(config.load_more_threshold, 100.0);
}
#[test]
fn test_virtual_grid_config_default() {
let config = VirtualGridConfig::default();
assert_eq!(config.columns, 3);
assert_eq!(config.cell_width, 100.0);
assert_eq!(config.cell_height, 100.0);
assert_eq!(config.gap, 8.0);
assert_eq!(config.overscan_rows, 2);
}
}