#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GridMetrics {
pub row_height: f32,
pub header_height: f32,
}
impl Default for GridMetrics {
fn default() -> Self {
Self {
row_height: 22.0,
header_height: 26.0,
}
}
}
pub fn visible_row_range(
scroll_y: f32,
viewport_height: f32,
row_count: usize,
metrics: &GridMetrics,
) -> std::ops::Range<usize> {
if row_count == 0 {
return 0..0;
}
let first = ((scroll_y / metrics.row_height).floor().max(0.0) as usize).min(row_count);
let visible = (viewport_height / metrics.row_height).ceil() as usize;
let end = (first + visible).min(row_count);
first..end
}
pub fn clamp_scroll(
scroll_y: f32,
viewport_height: f32,
row_count: usize,
metrics: &GridMetrics,
) -> f32 {
let content = row_count as f32 * metrics.row_height;
let max = (content - viewport_height).max(0.0);
scroll_y.clamp(0.0, max)
}
pub fn ensure_row_visible(
scroll_y: f32,
viewport_height: f32,
row: usize,
row_count: usize,
metrics: &GridMetrics,
) -> f32 {
let top = row as f32 * metrics.row_height;
let bottom = top + metrics.row_height;
let target = if top < scroll_y {
top
} else if bottom > scroll_y + viewport_height {
bottom - viewport_height
} else {
scroll_y
};
clamp_scroll(target, viewport_height, row_count, metrics)
}
pub fn page_rows(viewport_height: f32, metrics: &GridMetrics) -> usize {
((viewport_height / metrics.row_height).floor() as usize).max(1)
}
pub fn column_at_x(x: f32, widths: &[f32]) -> Option<usize> {
let mut acc = 0.0;
for (i, w) in widths.iter().enumerate() {
acc += w;
if x < acc {
return Some(i);
}
}
None
}
pub fn column_span_x(col: usize, widths: &[f32]) -> (f32, f32) {
let mut acc = 0.0;
for (i, w) in widths.iter().enumerate() {
if i == col {
return (acc, *w);
}
acc += w;
}
(acc, 0.0)
}
pub fn frozen_width(widths: &[f32], frozen: usize) -> f32 {
widths.iter().take(frozen).sum()
}
pub fn frozen_column_x(col: usize, widths: &[f32], scroll_x: f32) -> f32 {
column_span_x(col, widths).0 + scroll_x
}
pub fn scrolled_column_x(col: usize, widths: &[f32], frozen: usize, scroll_x: f32) -> f32 {
let (acc, _) = column_span_x(col, widths);
let (frozen_acc, _) = column_span_x(frozen, widths);
acc - frozen_acc - scroll_x
}
pub fn clamp_scroll_x(scroll_x: f32, content_width: f32, viewport_width: f32) -> f32 {
let max = (content_width - viewport_width).max(0.0);
scroll_x.clamp(0.0, max)
}
pub fn visible_scrolled_range(
scroll_x: f32,
viewport_width: f32,
widths: &[f32],
frozen: usize,
) -> std::ops::Range<usize> {
let pane = widths.len().min(frozen)..widths.len();
let scrolled = &widths[pane.start..];
let first = column_at_x(scroll_x, scrolled).unwrap_or(scrolled.len());
let content_right = scrolled.iter().sum::<f32>();
let probe = (scroll_x + viewport_width).min(content_right) - 0.5;
let end = column_at_x(probe, scrolled).map_or(scrolled.len(), |col| col + 1);
pane.start + first..pane.start + end.max(first)
}
pub fn effective_frozen(stored: usize, columns: usize) -> usize {
stored.min(columns.saturating_sub(1))
}
pub const DEFAULT_COLUMN_WIDTH: f32 = 140.0;
pub const MIN_COLUMN_WIDTH: f32 = 24.0;
pub fn resize_handle_at(x: f32, widths: &[f32], tolerance: f32) -> Option<usize> {
let mut acc = 0.0;
for (i, w) in widths.iter().enumerate() {
acc += w;
if (x - acc).abs() <= tolerance {
return Some(i);
}
}
None
}
pub fn resize_column(width: f32, delta: f32) -> f32 {
(width + delta).max(MIN_COLUMN_WIDTH)
}
pub fn effective_widths(stored: &[f32], columns: usize, default_width: f32) -> Vec<f32> {
let mut widths = stored.to_vec();
widths.truncate(columns);
widths.resize(columns, default_width);
widths
}