use std::cell::{Cell, RefCell};
use std::collections::BTreeSet;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use crate::compat::Rc;
use crate::core::{Color, HorizontalAlignment, Point, Rect};
use crate::event::{Event, WheelScroll};
use crate::impl_widget_property_hooks;
use crate::property_names_of;
use crate::render::RenderContext;
use crate::signal::{ConnectionHandle, ConnectionScope, Signal1};
use crate::widget::capability::access::{column_filters_to_string, sort_specs_to_string};
use crate::widget::capability::coercion::{
expect_bool, expect_column_filters, expect_sort_specs, expect_string_list,
expect_u32_saturating, expect_usize,
};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::metrics::{effective_font, ControlMetrics};
use crate::widget::nav_widgets::pagination::Pagination;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use super::data_source::{DataRole, IncrementalTableDataSource};
const GRID_INSET: u32 = 2;
const GRID_HEADER_HEIGHT: u32 = 22;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DataGridSelectionBehavior {
#[default]
Cells,
Rows,
Columns,
}
fn data_grid_selection_mode_to_str(
mode: crate::widget::view_widgets::list_view::SelectionMode,
) -> &'static str {
use crate::widget::view_widgets::list_view::SelectionMode;
match mode {
SelectionMode::Single => "single",
SelectionMode::Multi => "multi",
SelectionMode::Extended => "extended",
SelectionMode::None => "none",
}
}
fn column_width_mode_to_str(mode: ColumnWidthMode) -> &'static str {
match mode {
ColumnWidthMode::Fixed => "fixed",
ColumnWidthMode::Stretch => "stretch",
ColumnWidthMode::StretchLast => "stretch_last",
ColumnWidthMode::Interactive => "interactive",
}
}
fn expect_column_width_mode(
value: CapabilityValue,
) -> Result<ColumnWidthMode, CapabilityAccessError> {
match crate::widget::capability::coercion::expect_string(value)?.to_ascii_lowercase().as_str() {
"fixed" => Ok(ColumnWidthMode::Fixed),
"stretch" => Ok(ColumnWidthMode::Stretch),
"stretch_last" | "stretchlast" => Ok(ColumnWidthMode::StretchLast),
"interactive" => Ok(ColumnWidthMode::Interactive),
_ => Err(CapabilityAccessError::TypeMismatch),
}
}
fn expect_data_grid_selection_mode(
value: CapabilityValue,
) -> Result<crate::widget::view_widgets::list_view::SelectionMode, CapabilityAccessError> {
use crate::widget::view_widgets::list_view::SelectionMode;
match value {
CapabilityValue::String(token) => match token.as_str() {
"single" => Ok(SelectionMode::Single),
"multi" => Ok(SelectionMode::Multi),
"extended" => Ok(SelectionMode::Extended),
"none" => Ok(SelectionMode::None),
_ => Err(CapabilityAccessError::TypeMismatch),
},
_ => Err(CapabilityAccessError::TypeMismatch),
}
}
fn selection_behavior_to_str(behavior: DataGridSelectionBehavior) -> &'static str {
match behavior {
DataGridSelectionBehavior::Cells => "cells",
DataGridSelectionBehavior::Rows => "rows",
DataGridSelectionBehavior::Columns => "columns",
}
}
fn expect_selection_behavior(
value: CapabilityValue,
) -> Result<DataGridSelectionBehavior, CapabilityAccessError> {
match value {
CapabilityValue::String(token) => match token.as_str() {
"cells" => Ok(DataGridSelectionBehavior::Cells),
"rows" => Ok(DataGridSelectionBehavior::Rows),
"columns" => Ok(DataGridSelectionBehavior::Columns),
_ => Err(CapabilityAccessError::TypeMismatch),
},
_ => Err(CapabilityAccessError::TypeMismatch),
}
}
fn normalise_sort_key(text: &str) -> (String, Option<f64>) {
let trimmed = text.trim();
(trimmed.to_lowercase(), trimmed.parse::<f64>().ok())
}
fn compare_sort_keys(
left: &(String, Option<f64>),
right: &(String, Option<f64>),
) -> core::cmp::Ordering {
if let (Some(left), Some(right)) = (left.1, right.1) {
if let Some(order) = left.partial_cmp(&right) {
return order;
}
}
left.0.cmp(&right.0)
}
use super::filter_expr::{FilterCondition, FilterExpr};
type ProjectedRows = Vec<Vec<Option<String>>>;
type FullWindowCache =
Option<(Rc<ProjectedRows>, (usize, usize, usize, usize), Rc<Vec<Vec<Option<String>>>>)>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SortSpec {
pub column: usize,
pub descending: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ColumnFilter {
pub column: usize,
pub query: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct GridWindowCache {
row_start: usize,
row_len: usize,
column_start: usize,
column_len: usize,
revision: u64,
cells: Rc<Vec<Vec<Option<String>>>>,
projected: Option<Rc<Vec<Vec<Option<String>>>>>,
}
#[derive(Clone, Debug)]
struct ColumnPrefixCache {
revision: u64,
column_count: usize,
cells_width: u32,
mode: ColumnWidthMode,
default_width: u32,
prefix: Vec<i32>,
}
#[derive(Clone, Debug, Default)]
struct HeaderTextCache {
labels_revision: u64,
generation: u64,
column_count: usize,
entries: std::collections::HashMap<(usize, u32), String>,
}
struct ProjectedWindowView {
rows: Rc<ProjectedRows>,
row_start: usize,
row_len: usize,
col_start: usize,
col_len: usize,
}
impl ProjectedWindowView {
fn is_empty(&self) -> bool {
self.row_len == 0 || self.col_len == 0
}
fn len(&self) -> usize {
self.row_len
}
fn row(&self, index: usize) -> Option<&[Option<String>]> {
if index >= self.row_len {
return None;
}
let row = self.rows.get(self.row_start.checked_add(index)?)?;
let start = self.col_start.min(row.len());
let end = self.col_start.saturating_add(self.col_len).min(row.len());
row.get(start..end)
}
}
pub struct DataGrid {
base: BaseWidget,
data_source: Option<Arc<dyn IncrementalTableDataSource>>,
data_source_connection_scope: ConnectionScope,
scroll_row: usize,
scroll_column: usize,
row_height: u32,
column_width: u32,
overscan_rows: usize,
overscan_columns: usize,
frozen_columns: usize,
sort_specs: Vec<SortSpec>,
filters: Vec<ColumnFilter>,
filter_expr: FilterExpr,
window_cache: Option<GridWindowCache>,
column_prefix_cache: RefCell<Option<ColumnPrefixCache>>,
column_geometry_revision: u64,
column_prefix_rebuilds: Cell<u64>,
header_text_cache: RefCell<HeaderTextCache>,
header_text_revision: u64,
data_generation: Arc<AtomicU64>,
data_is_observable: bool,
selection: Option<(usize, usize)>,
selection_mode: crate::widget::view_widgets::list_view::SelectionMode,
selection_behavior: DataGridSelectionBehavior,
selected_cells: BTreeSet<(usize, usize)>,
selection_anchor: Option<(usize, usize)>,
current_cell: Option<(usize, usize)>,
all_selected: bool,
sort_roles: std::collections::HashMap<usize, DataRole>,
sort_indicator_visible: bool,
column_widths: std::collections::HashMap<usize, u32>,
resizing_column: Option<usize>,
resize_start_x: i32,
resize_start_width: u32,
header_labels: Vec<String>,
header_columns: Vec<ColumnSpec>,
header_filter_visible: bool,
pagination: Option<PaginationBinding>,
pagination_applied_page: usize,
selection_changed: Signal1<()>,
current_changed: Signal1<(usize, usize)>,
cell_clicked: Signal1<(usize, usize)>,
cell_double_clicked: Signal1<(usize, usize)>,
column_resized: Signal1<(usize, u32)>,
hovered_cell: Option<(usize, usize)>,
pub visible_window_changed: Signal1<(usize, usize, usize, usize)>,
full_model_projection: bool,
full_projection_cache: Option<(u64, Rc<ProjectedRows>)>,
full_window_cache: FullWindowCache,
column_width_mode: ColumnWidthMode,
alternating_rows: bool,
edit_triggers: EditTriggers,
show_scrollbars: bool,
pub header_clicked: Signal1<usize>,
pub sort_changed: Signal1<(usize, bool)>,
pub edit_requested: Signal1<(usize, usize)>,
pub context_menu_requested: Signal1<Point>,
pub filter_changed: Signal1<FilterExpr>,
pub page_applied: Signal1<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColumnWidthMode {
#[default]
Fixed,
Stretch,
StretchLast,
Interactive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct EditTriggers(u8);
impl EditTriggers {
pub const NONE: Self = Self(0);
pub const DOUBLE_CLICKED: Self = Self(1 << 0);
pub const EDIT_KEY: Self = Self(1 << 1);
pub const ALL: Self = Self(0b11);
pub fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
pub fn or(self, other: Self) -> Self {
Self(self.0 | other.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColumnSpec {
pub title: String,
pub width: u32,
pub sortable: bool,
pub filterable: bool,
pub resizable: bool,
}
impl ColumnSpec {
pub fn new(title: impl Into<String>, width: u32) -> Self {
Self {
title: title.into(),
width: width.max(1),
sortable: false,
filterable: false,
resizable: true,
}
}
pub fn with_sortable(mut self, sortable: bool) -> Self {
self.sortable = sortable;
self
}
pub fn with_filterable(mut self, filterable: bool) -> Self {
self.filterable = filterable;
self
}
pub fn with_resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
pub fn to_token(&self) -> String {
format!(
"{}|{}|{}|{}|{}",
self.title,
self.width,
u8::from(self.sortable),
u8::from(self.filterable),
u8::from(self.resizable)
)
}
pub fn from_token(token: &str) -> Option<Self> {
let mut parts = token.splitn(5, '|');
let title = parts.next()?.to_string();
let width = parts.next()?.trim().parse::<u32>().ok()?.max(1);
let flag = |text: Option<&str>| matches!(text.map(str::trim), Some("1" | "true"));
Some(Self {
title,
width,
sortable: flag(parts.next()),
filterable: flag(parts.next()),
resizable: parts.next().is_none_or(|text| !matches!(text.trim(), "0" | "false")),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortOrder {
Ascending,
Descending,
}
impl SortOrder {
pub fn as_str(self) -> &'static str {
match self {
Self::Ascending => "ascending",
Self::Descending => "descending",
}
}
pub fn from_token(token: &str) -> Option<Self> {
match token {
"ascending" | "asc" => Some(Self::Ascending),
"descending" | "desc" => Some(Self::Descending),
_ => None,
}
}
pub fn is_descending(self) -> bool {
matches!(self, Self::Descending)
}
pub fn next(self) -> Self {
match self {
Self::Ascending => Self::Descending,
Self::Descending => Self::Ascending,
}
}
}
const HEADER_STRIP_HEIGHT: u32 = 28;
const HEADER_RESIZE_GRAB: i32 = 4;
const HEADER_ICON_SIDE: u32 = 10;
pub struct GridViewHeader {
base: BaseWidget,
columns: Vec<ColumnSpec>,
sorts: Vec<Option<SortOrder>>,
sort_indicator_visible: bool,
resize_mode: ColumnWidthMode,
filter_icon_visible: bool,
hovered: Option<usize>,
resizing: Option<(usize, i32, u32)>,
pub section_clicked: Signal1<usize>,
pub section_resized: Signal1<(usize, u32)>,
pub filter_clicked: Signal1<usize>,
}
impl GridViewHeader {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::GridViewHeader, geometry, "GridViewHeader"),
columns: Vec::new(),
sorts: Vec::new(),
sort_indicator_visible: true,
resize_mode: ColumnWidthMode::Fixed,
filter_icon_visible: true,
hovered: None,
resizing: None,
section_clicked: Signal1::new(),
section_resized: Signal1::new(),
filter_clicked: Signal1::new(),
}
}
pub fn columns(&self) -> &[ColumnSpec] {
&self.columns
}
pub fn set_columns(&mut self, columns: Vec<ColumnSpec>) {
let mut sorts = core::mem::take(&mut self.sorts);
sorts.resize(columns.len(), None);
self.sorts = sorts;
self.columns = columns;
self.hovered = None;
self.resizing = None;
self.base.request_layout();
self.base.request_redraw();
}
pub fn sort_indicator_visible(&self) -> bool {
self.sort_indicator_visible
}
pub fn set_sort_indicator_visible(&mut self, visible: bool) {
if self.sort_indicator_visible == visible {
return;
}
self.sort_indicator_visible = visible;
self.base.request_redraw();
}
pub fn filter_icon_visible(&self) -> bool {
self.filter_icon_visible
}
pub fn set_filter_icon_visible(&mut self, visible: bool) {
if self.filter_icon_visible == visible {
return;
}
self.filter_icon_visible = visible;
self.base.request_redraw();
}
pub fn resize_mode(&self) -> ColumnWidthMode {
self.resize_mode
}
pub fn set_resize_mode(&mut self, mode: ColumnWidthMode) {
if self.resize_mode == mode {
return;
}
self.resize_mode = mode;
self.base.request_redraw();
}
pub fn section_width(&self, column: usize) -> u32 {
let count = self.columns.len().max(1) as u32;
let fixed = |column: usize| self.columns.get(column).map_or(1, |spec| spec.width.max(1));
match self.resize_mode {
ColumnWidthMode::Fixed | ColumnWidthMode::Interactive => fixed(column),
ColumnWidthMode::Stretch => (self.geometry().width / count).max(1),
ColumnWidthMode::StretchLast => {
if column + 1 != count as usize {
return fixed(column);
}
let used: u32 = (0..self.columns.len().saturating_sub(1)).map(fixed).sum();
self.geometry().width.saturating_sub(used).max(1)
}
}
}
pub fn section_rect(&self, column: usize) -> Option<Rect> {
if column >= self.columns.len() {
return None;
}
let rect = self.geometry();
let x: i32 = (0..column).map(|index| self.section_width(index) as i32).sum();
let width = self.section_width(column);
let trimmed = width.min((rect.width as i32 - x).max(0) as u32);
(trimmed > 0).then(|| Rect::new(rect.x + x, rect.y, trimmed, rect.height))
}
pub fn filter_rect(&self, column: usize) -> Option<Rect> {
if !self.filter_icon_visible {
return None;
}
if !self.columns.get(column).is_some_and(|spec| spec.filterable) {
return None;
}
let section = self.section_rect(column)?;
if section.width < HEADER_ICON_SIDE + 4 {
return None;
}
Some(Rect::new(
section.x + section.width as i32 - HEADER_ICON_SIDE as i32 - 3,
section.y + (section.height as i32 - HEADER_ICON_SIDE as i32) / 2,
HEADER_ICON_SIDE,
HEADER_ICON_SIDE,
))
}
pub fn sort_indicator(&self, column: usize) -> Option<SortOrder> {
self.sorts.get(column).copied().flatten()
}
pub fn set_sort_indicator(&mut self, column: usize, order: Option<SortOrder>) -> bool {
if column >= self.columns.len() {
return false;
}
if self.sorts[column] == order {
return true;
}
self.sorts[column] = order;
self.base.request_redraw();
true
}
pub fn toggle_sort(&mut self, column: usize) -> Option<SortOrder> {
let spec = self.columns.get(column)?;
if !spec.sortable {
return None;
}
let next = match self.sorts.get(column).copied().flatten() {
None => Some(SortOrder::Ascending),
Some(SortOrder::Ascending) => Some(SortOrder::Descending),
Some(SortOrder::Descending) => None,
};
let _ = self.set_sort_indicator(column, next);
next
}
fn section_at(&self, pos: Point) -> Option<usize> {
(0..self.columns.len())
.find(|column| self.section_rect(*column).is_some_and(|rect| rect.contains_point(pos)))
}
fn resize_handle_at(&self, pos: Point) -> Option<usize> {
let column = self.section_at(pos)?;
let spec = self.columns.get(column)?;
if !spec.resizable || !matches!(self.resize_mode, ColumnWidthMode::Interactive) {
return None;
}
let section = self.section_rect(column)?;
let right = section.x + section.width as i32;
((pos.x - right).abs() <= HEADER_RESIZE_GRAB).then_some(column)
}
pub fn set_section_width(&mut self, column: usize, width: u32) -> bool {
let Some(spec) = self.columns.get_mut(column) else {
return false;
};
let width = width.max(8);
if spec.width == width {
return true;
}
spec.width = width;
self.resize_mode = ColumnWidthMode::Interactive;
self.base.request_layout();
self.base.request_redraw();
true
}
}
impl Widget for GridViewHeader {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(320, HEADER_STRIP_HEIGHT)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"section_clicked" => {
Some(EventSignalRef::mapped("section_clicked", &self.section_clicked, |v| {
CapabilityValue::UInt(*v as u64)
}))
}
"section_resized" => {
Some(EventSignalRef::mapped("section_resized", &self.section_resized, |v| {
CapabilityValue::Tuple(crate::compat::Vec::from([
CapabilityValue::UInt(v.0 as u64),
CapabilityValue::UInt(v.1 as u64),
]))
}))
}
"filter_clicked" => {
Some(EventSignalRef::mapped("filter_clicked", &self.filter_clicked, |v| {
CapabilityValue::UInt(*v as u64)
}))
}
_ => None,
}
}
}
impl WidgetProperties for GridViewHeader {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"columns" => Ok(CapabilityValue::Tuple(
self.columns.iter().map(|spec| CapabilityValue::String(spec.to_token())).collect(),
)),
"column_count" => Ok(CapabilityValue::UInt(self.columns.len() as u64)),
"sort_indicator_visible" => Ok(CapabilityValue::Bool(self.sort_indicator_visible)),
"resize_mode" => {
Ok(CapabilityValue::String(column_width_mode_to_str(self.resize_mode).to_string()))
}
"filter_icon_visible" => Ok(CapabilityValue::Bool(self.filter_icon_visible)),
"sort_columns" => Ok(CapabilityValue::String(
self.sorts
.iter()
.enumerate()
.filter_map(|(column, order)| {
order.map(|order| format!("{column}:{}", order.as_str()))
})
.collect::<Vec<_>>()
.join(","),
)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"columns" => {
let items = expect_string_list(value)?;
let mut columns = Vec::with_capacity(items.len());
for item in items {
columns.push(
ColumnSpec::from_token(&item).ok_or(CapabilityAccessError::TypeMismatch)?,
);
}
self.set_columns(columns);
Ok(())
}
"column_count" | "sort_columns" => Err(CapabilityAccessError::ReadOnlyProperty),
"sort_indicator_visible" => {
self.set_sort_indicator_visible(expect_bool(value)?);
Ok(())
}
"resize_mode" => {
self.set_resize_mode(expect_column_width_mode(value)?);
Ok(())
}
"filter_icon_visible" => {
self.set_filter_icon_visible(expect_bool(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"columns",
"column_count",
"sort_indicator_visible",
"resize_mode",
"filter_icon_visible",
"sort_columns",
BASE_PROPERTY_NAMES
]
}
}
impl crate::event::EventHandler for GridViewHeader {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
match event {
Event::MousePress { pos, button: 1, .. } => {
if let Some(column) = self.resize_handle_at(*pos) {
let width = self.section_width(column);
self.resizing = Some((column, pos.x, width));
return;
}
if let Some(column) = (0..self.columns.len()).rev().find(|column| {
self.filter_rect(*column).is_some_and(|rect| rect.contains_point(*pos))
}) {
self.filter_clicked.emit(column);
return;
}
if let Some(column) = self.section_at(*pos) {
let _ = self.toggle_sort(column);
self.section_clicked.emit(column);
}
}
Event::MouseMove { pos } => {
if let Some((column, press_x, start_width)) = self.resizing {
let width = (start_width as i32 + (pos.x - press_x)).max(8) as u32;
if let Some(spec) = self.columns.get_mut(column) {
if spec.width != width {
spec.width = width;
self.base.request_redraw();
}
}
return;
}
let hovered = self.section_at(*pos);
if hovered != self.hovered {
self.hovered = hovered;
self.base.request_redraw();
}
}
Event::MouseRelease { button: 1, .. } => {
if let Some((column, _, start_width)) = self.resizing.take() {
let width = self.section_width(column);
if width != start_width {
self.section_resized.emit((column, width));
}
self.base.request_redraw();
}
}
_ => {}
}
}
}
impl Draw for GridViewHeader {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let style = self.base.style();
let theme = crate::style::resolved_theme_style_shared("data_grid");
let surface = style
.background_color
.or_else(|| theme.as_ref().and_then(|t| t.background_color))
.unwrap_or(Color::WHITE);
let ink = style
.text_color
.or_else(|| theme.as_ref().and_then(|t| t.text_color))
.unwrap_or(Color::BLACK);
let border = style
.border_color
.or_else(|| theme.as_ref().and_then(|t| t.border_color))
.unwrap_or_else(|| surface.blend(&ink, 0.20));
let font = effective_font(style);
context.face_with_gradient(
rect,
surface.blend(&ink, 0.06),
self.style().background_gradient.as_ref(),
self.style().surface.unwrap_or_default(),
self.style().border_radius.unwrap_or(0),
Color::BLACK,
);
crate::widget::draw::stroke_surface_border(
context,
rect,
self.style().border_radius.unwrap_or(0),
border,
1,
);
for column in 0..self.columns.len() {
let Some(section) = self.section_rect(column) else {
continue;
};
if self.hovered == Some(column) {
context.fill_rect(section, surface.blend(&ink, 0.03));
}
let spec = &self.columns[column];
let filter_slot = if self.filter_rect(column).is_some() { 12i32 } else { 0 };
let sort_slot = if self.sort_indicator_visible && self.sort_indicator(column).is_some()
{
12
} else {
0
};
let title_width =
section.width.saturating_sub(8 + filter_slot.max(0) as u32 + sort_slot).max(8);
let label = crate::widget::text_utils::elide_end(
&spec.title,
(title_width as usize / 7).max(1),
);
context.draw_text(
Point::new(section.x + 4, section.y + (section.height as i32 - 12) / 2),
&label,
font,
ink,
HorizontalAlignment::Left,
);
if self.sort_indicator_visible {
if let Some(order) = self.sort_indicator(column) {
let icon = if order.is_descending() {
crate::widget::IconName::ArrowDown
} else {
crate::widget::IconName::ArrowUp
};
let icon_rect = Rect::new(
section.x + section.width as i32
- filter_slot
- HEADER_ICON_SIDE as i32
- 3,
section.y + (section.height as i32 - HEADER_ICON_SIDE as i32) / 2,
HEADER_ICON_SIDE,
HEADER_ICON_SIDE,
);
crate::widget::draw_icon_at(context, icon_rect, ink, icon);
}
}
if let Some(filter_rect) = self.filter_rect(column) {
crate::widget::draw_icon_at(
context,
filter_rect,
ink.blend(&surface, 0.25),
crate::widget::IconName::Filter,
);
}
context.draw_line(
Point::new(section.x + section.width as i32 - 1, section.y),
Point::new(section.x + section.width as i32 - 1, section.y + section.height as i32),
surface.blend(&ink, 0.12),
);
}
}
}
struct PaginationBinding {
page: Arc<AtomicU64>,
page_size: usize,
_connection: ConnectionHandle,
}
impl DataGrid {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Table, geometry, "DataGrid"),
data_source: None,
data_source_connection_scope: ConnectionScope::new(),
scroll_row: 0,
scroll_column: 0,
row_height: 20,
column_width: 120,
overscan_rows: 2,
overscan_columns: 1,
frozen_columns: 0,
sort_specs: Vec::new(),
filters: Vec::new(),
filter_expr: FilterExpr::MatchAll,
window_cache: None,
column_prefix_cache: RefCell::new(None),
column_geometry_revision: 0,
column_prefix_rebuilds: Cell::new(0),
header_text_cache: RefCell::new(HeaderTextCache::default()),
header_text_revision: 0,
data_generation: Arc::new(AtomicU64::new(0)),
data_is_observable: false,
selection: None,
selection_mode: crate::widget::view_widgets::list_view::SelectionMode::Single,
selection_behavior: DataGridSelectionBehavior::Cells,
selected_cells: BTreeSet::new(),
selection_anchor: None,
current_cell: None,
all_selected: false,
sort_roles: std::collections::HashMap::new(),
sort_indicator_visible: true,
column_widths: std::collections::HashMap::new(),
resizing_column: None,
resize_start_x: 0,
resize_start_width: 0,
header_labels: Vec::new(),
header_columns: Vec::new(),
header_filter_visible: false,
pagination: None,
pagination_applied_page: 0,
selection_changed: Signal1::new(),
current_changed: Signal1::new(),
cell_clicked: Signal1::new(),
cell_double_clicked: Signal1::new(),
column_resized: Signal1::new(),
hovered_cell: None,
visible_window_changed: Signal1::new(),
full_model_projection: true,
full_projection_cache: None,
full_window_cache: None,
column_width_mode: ColumnWidthMode::Fixed,
alternating_rows: false,
edit_triggers: EditTriggers::NONE,
show_scrollbars: false,
header_clicked: Signal1::new(),
sort_changed: Signal1::new(),
edit_requested: Signal1::new(),
context_menu_requested: Signal1::new(),
filter_changed: Signal1::new(),
page_applied: Signal1::new(),
}
}
pub fn set_header_labels(&mut self, labels: &[&str]) {
self.header_labels = labels.iter().map(|label| (*label).to_string()).collect();
self.header_text_revision = self.header_text_revision.wrapping_add(1);
self.base.request_redraw();
}
pub fn header_columns(&self) -> &[ColumnSpec] {
&self.header_columns
}
pub fn set_header(&mut self, header: &GridViewHeader) {
self.header_columns = header.columns().to_vec();
self.header_labels = header.columns().iter().map(|spec| spec.title.clone()).collect();
self.header_filter_visible = header.filter_icon_visible();
self.sort_indicator_visible = header.sort_indicator_visible();
self.column_width_mode = match header.resize_mode() {
ColumnWidthMode::Fixed => ColumnWidthMode::Interactive,
mode => mode,
};
self.column_widths.clear();
self.sort_specs.clear();
for (column, spec) in self.header_columns.iter().enumerate() {
self.column_widths.insert(column, spec.width.max(1));
if let Some(order) = header.sort_indicator(column) {
self.sort_specs.push(SortSpec { column, descending: order.is_descending() });
}
}
self.clear_cache();
self.full_projection_cache = None;
self.column_geometry_revision = self.column_geometry_revision.wrapping_add(1);
self.header_text_revision = self.header_text_revision.wrapping_add(1);
self.base.request_layout();
self.base.request_redraw();
}
pub fn set_pagination(&mut self, pagination: &mut Pagination) {
pagination.set_total(self.row_count());
let page_size = pagination.page_size().max(1);
let page = pagination.page();
let shared = Arc::new(AtomicU64::new(page as u64));
let slot = Arc::clone(&shared);
let connection = pagination.page_changed.connect(move |page| {
slot.store(*page as u64, Ordering::Relaxed);
});
self.pagination =
Some(PaginationBinding { page: shared, page_size, _connection: connection });
self.scroll_to_page(page);
self.pagination_applied_page = page;
}
pub fn poll_pagination(&mut self) -> Option<usize> {
let page = self.pagination.as_ref()?.page.load(Ordering::Relaxed) as usize;
if page == self.pagination_applied_page {
return None;
}
self.apply_page(page);
Some(page)
}
pub fn apply_page(&mut self, page: usize) -> bool {
let moved = self.scroll_to_page(page);
self.pagination_applied_page = page;
self.page_applied.emit(page);
moved
}
fn scroll_to_page(&mut self, page: usize) -> bool {
let page_size = self
.pagination
.as_ref()
.map_or_else(|| self.visible_row_capacity().max(1), |binding| binding.page_size.max(1));
let first_row = page.saturating_mul(page_size);
let clamped = first_row.min(self.row_count().saturating_sub(1));
if self.scroll_row == clamped {
return false;
}
self.set_scroll_row(clamped);
true
}
pub fn sync_pagination(&mut self, pagination: &mut Pagination) -> bool {
let Some(binding) = self.pagination.as_ref() else {
return false;
};
let page_size = binding.page_size.max(1);
let page = self.scroll_row / page_size;
pagination.set_total(self.row_count());
let before = pagination.page();
pagination.set_page(page);
binding.page.store(page as u64, Ordering::Relaxed);
self.pagination_applied_page = page;
before != page
}
pub fn pagination_state(&self) -> Option<(usize, usize)> {
self.pagination.as_ref().map(|binding| (self.pagination_applied_page, binding.page_size))
}
pub fn summary_text(&self) -> String {
let rows = self.row_count();
let mut text = format!("{rows} rows");
let selected = self.selected_indexes().len();
if selected > 0 {
text.push_str(&format!(" \u{00b7} {selected} selected"));
}
if !self.filters.is_empty() {
text.push_str(" \u{00b7} filtered");
}
text
}
pub fn set_data_source(&mut self, data_source: Arc<dyn IncrementalTableDataSource>) {
self.data_source_connection_scope = ConnectionScope::new();
let revision_nonzero = data_source.revision() > 0;
let has_signal = if let Some(changed) = data_source.data_changed_signal() {
let redraw = self.base.redraw_requested_signal().clone();
let layout = self.base.layout_requested_signal().clone();
let generation = Arc::clone(&self.data_generation);
changed.connect_scoped(&self.data_source_connection_scope, move || {
generation.fetch_add(1, Ordering::Relaxed);
redraw.emit();
layout.emit();
});
true
} else {
false
};
self.data_generation.fetch_add(1, Ordering::Relaxed);
self.data_is_observable = revision_nonzero || has_signal;
self.data_source = Some(data_source);
self.scroll_row = 0;
self.scroll_column = 0;
self.clear_cache();
self.full_projection_cache = None;
self.clear_selection_units();
self.current_cell = None;
self.normalize_projection_state();
self.emit_visible_window_changed();
self.base.request_layout();
self.base.request_redraw();
}
pub fn clear_data_source(&mut self) {
self.data_source_connection_scope = ConnectionScope::new();
self.data_source = None;
self.data_is_observable = false;
self.data_generation.fetch_add(1, Ordering::Relaxed);
self.scroll_row = 0;
self.scroll_column = 0;
self.frozen_columns = 0;
self.clear_cache();
self.full_projection_cache = None;
self.clear_selection_units();
self.current_cell = None;
self.emit_visible_window_changed();
self.base.request_layout();
self.base.request_redraw();
}
pub fn has_data_source(&self) -> bool {
self.data_source.is_some()
}
pub fn row_count(&self) -> usize {
self.data_source.as_ref().map(|source| source.row_count()).unwrap_or(0)
}
pub fn column_count(&self) -> usize {
self.data_source.as_ref().map(|source| source.column_count()).unwrap_or(0)
}
pub fn scroll_row(&self) -> usize {
self.scroll_row
}
pub fn selection(&self) -> Option<(usize, usize)> {
self.selection
}
pub fn selection_mode(&self) -> crate::widget::view_widgets::list_view::SelectionMode {
self.selection_mode
}
pub fn set_selection_mode(
&mut self,
mode: crate::widget::view_widgets::list_view::SelectionMode,
) {
if self.selection_mode == mode {
return;
}
self.selection_mode = mode;
if mode == crate::widget::view_widgets::list_view::SelectionMode::None {
self.clear_selection();
return;
}
if mode == crate::widget::view_widgets::list_view::SelectionMode::Single
&& (self.selected_cells.len() > 1 || self.all_selected)
{
self.all_selected = false;
let keep = self.selection.or_else(|| self.selected_cells.iter().next().copied());
self.selected_cells.clear();
if let Some((row, column)) = keep {
self.push_selection_unit(row, column);
self.selection = Some((row, column));
}
self.selection_changed.emit(());
}
self.base.request_redraw();
}
pub fn selection_behavior(&self) -> DataGridSelectionBehavior {
self.selection_behavior
}
pub fn set_selection_behavior(&mut self, behavior: DataGridSelectionBehavior) {
if self.selection_behavior == behavior {
return;
}
self.selection_behavior = behavior;
self.clear_selection_units();
self.base.request_redraw();
}
pub fn current_cell(&self) -> Option<(usize, usize)> {
self.current_cell
}
pub fn set_current_cell(&mut self, row: usize, column: usize) -> bool {
if row >= self.row_count() || column >= self.column_count() {
return false;
}
self.current_cell = Some((row, column));
self.current_changed.emit((row, column));
if self.selection_mode == crate::widget::view_widgets::list_view::SelectionMode::Single {
self.replace_selection(row, column);
}
self.scroll_to_cell(row, column);
self.base.request_redraw();
true
}
pub fn move_current_cell(&mut self, row: usize, column: usize, extend: bool) -> bool {
if row >= self.row_count() || column >= self.column_count() {
return false;
}
if self.selection_mode == crate::widget::view_widgets::list_view::SelectionMode::None {
self.current_cell = Some((row, column));
self.current_changed.emit((row, column));
self.scroll_to_cell(row, column);
self.base.request_redraw();
return true;
}
let anchor =
self.selection_anchor.or(self.selection).or(self.current_cell).unwrap_or((row, column));
self.current_cell = Some((row, column));
if extend {
self.all_selected = false;
self.push_selection_range(anchor, (row, column));
self.selection = Some((row, column));
self.selection_changed.emit(());
} else {
self.replace_selection(row, column);
}
self.current_changed.emit((row, column));
self.scroll_to_cell(row, column);
self.base.request_redraw();
true
}
pub fn selection_changed_signal(&self) -> &Signal1<()> {
&self.selection_changed
}
pub fn current_changed_signal(&self) -> &Signal1<(usize, usize)> {
&self.current_changed
}
pub fn cell_clicked_signal(&self) -> &Signal1<(usize, usize)> {
&self.cell_clicked
}
pub fn cell_double_clicked_signal(&self) -> &Signal1<(usize, usize)> {
&self.cell_double_clicked
}
pub fn column_resized_signal(&self) -> &Signal1<(usize, u32)> {
&self.column_resized
}
pub fn selected_cells(&self) -> Vec<(usize, usize)> {
if self.all_selected {
let columns = self.column_count();
return (0..self.row_count())
.flat_map(|row| (0..columns).map(move |column| (row, column)))
.collect();
}
match self.selection_behavior {
DataGridSelectionBehavior::Cells => self.selected_cells.iter().copied().collect(),
DataGridSelectionBehavior::Rows => {
let columns = self.column_count();
self.selected_cells
.iter()
.flat_map(|(row, _)| (0..columns).map(move |column| (*row, column)))
.collect()
}
DataGridSelectionBehavior::Columns => {
let rows = self.row_count();
self.selected_cells
.iter()
.flat_map(|(_, column)| (0..rows).map(move |row| (row, *column)))
.collect()
}
}
}
pub fn selected_indexes(&self) -> Vec<usize> {
if self.all_selected {
return match self.selection_behavior {
DataGridSelectionBehavior::Columns => (0..self.column_count()).collect(),
_ => (0..self.row_count()).collect(),
};
}
match self.selection_behavior {
DataGridSelectionBehavior::Columns => {
let mut columns: Vec<usize> =
self.selected_cells.iter().map(|(_, column)| *column).collect();
columns.dedup();
columns
}
_ => {
let mut rows: Vec<usize> =
self.selected_cells.iter().map(|(row, _)| *row).collect();
rows.dedup();
rows
}
}
}
pub fn select_all(&mut self) {
if self.row_count() == 0 || self.column_count() == 0 {
return;
}
self.all_selected = true;
self.selected_cells.clear();
self.selection = Some((0, 0));
self.current_cell.get_or_insert((0, 0));
self.selection_changed.emit(());
self.base.request_redraw();
}
pub fn clear_selection(&mut self) {
if self.selection.is_none() && self.selected_cells.is_empty() && !self.all_selected {
return;
}
self.clear_selection_units();
self.selection_changed.emit(());
self.base.request_redraw();
}
fn clear_selection_units(&mut self) {
self.selection = None;
self.selected_cells.clear();
self.all_selected = false;
self.selection_anchor = None;
}
fn selection_contains(&self, row: usize, column: usize) -> bool {
if self.all_selected {
return true;
}
match self.selection_behavior {
DataGridSelectionBehavior::Cells => self.selected_cells.contains(&(row, column)),
DataGridSelectionBehavior::Rows => {
self.selected_cells.iter().any(|(selected, _)| *selected == row)
}
DataGridSelectionBehavior::Columns => {
self.selected_cells.iter().any(|(_, selected)| *selected == column)
}
}
}
fn push_selection_unit(&mut self, row: usize, column: usize) {
let unit = match self.selection_behavior {
DataGridSelectionBehavior::Cells => (row, column),
DataGridSelectionBehavior::Rows => (row, 0),
DataGridSelectionBehavior::Columns => (0, column),
};
self.selected_cells.insert(unit);
}
fn push_selection_range(&mut self, from: (usize, usize), to: (usize, usize)) {
match self.selection_behavior {
DataGridSelectionBehavior::Cells => {
for row in from.0.min(to.0)..=from.0.max(to.0) {
for column in from.1.min(to.1)..=from.1.max(to.1) {
self.selected_cells.insert((row, column));
}
}
}
DataGridSelectionBehavior::Rows => {
for row in from.0.min(to.0)..=from.0.max(to.0) {
self.selected_cells.insert((row, 0));
}
}
DataGridSelectionBehavior::Columns => {
for column in from.1.min(to.1)..=from.1.max(to.1) {
self.selected_cells.insert((0, column));
}
}
}
}
fn replace_selection(&mut self, row: usize, column: usize) {
self.selected_cells.clear();
self.all_selected = false;
self.selection = Some((row, column));
self.selection_anchor = Some((row, column));
self.push_selection_unit(row, column);
self.selection_changed.emit(());
}
fn apply_selection_click(&mut self, row: usize, column: usize, modifiers: u32) {
use crate::shortcut::Modifiers;
if self.selection_mode == crate::widget::view_widgets::list_view::SelectionMode::None {
return;
}
let modifiers = Modifiers::from_event_bits(modifiers);
let extended =
self.selection_mode == crate::widget::view_widgets::list_view::SelectionMode::Extended;
let shift = extended && modifiers.contains(Modifiers::SHIFT);
let toggle = extended
&& (modifiers.contains(Modifiers::CTRL) || modifiers.contains(Modifiers::META));
if shift {
let anchor = self.selection_anchor.unwrap_or((row, column));
if !self.all_selected {
self.push_selection_range(anchor, (row, column));
}
self.selection = Some((row, column));
self.selection_changed.emit(());
} else if toggle {
self.all_selected = false;
let unit = match self.selection_behavior {
DataGridSelectionBehavior::Cells => (row, column),
DataGridSelectionBehavior::Rows => (row, 0),
DataGridSelectionBehavior::Columns => (0, column),
};
if !self.selected_cells.remove(&unit) {
self.selected_cells.insert(unit);
}
self.selection = Some((row, column));
self.selection_anchor = Some((row, column));
self.selection_changed.emit(());
} else {
self.replace_selection(row, column);
}
}
fn scroll_to_cell(&mut self, row: usize, column: usize) {
let rows = self.visible_row_capacity().max(1);
if row < self.scroll_row {
self.set_scroll_row(row);
} else if row >= self.scroll_row.saturating_add(rows) {
self.set_scroll_row(row.saturating_add(1).saturating_sub(rows));
}
let columns = self.visible_column_capacity().max(1);
if column < self.scroll_column {
self.set_scroll_column(column);
} else if column >= self.scroll_column.saturating_add(columns) {
self.set_scroll_column(column.saturating_add(1).saturating_sub(columns));
}
}
pub fn scroll_column(&self) -> usize {
self.scroll_column
}
pub fn set_scroll_row(&mut self, row: usize) {
self.normalize_projection_state();
let max_row = self.row_count().saturating_sub(1);
let next = row.min(max_row);
if next == self.scroll_row {
return;
}
self.scroll_row = next;
self.clear_cache();
self.emit_visible_window_changed();
self.base.request_redraw();
}
pub fn set_scroll_column(&mut self, column: usize) {
self.normalize_projection_state();
let max_column = self.column_count().saturating_sub(1);
let next = column.min(max_column);
if next == self.scroll_column {
return;
}
self.scroll_column = next;
self.clear_cache();
self.emit_visible_window_changed();
self.base.request_redraw();
}
pub fn set_row_height(&mut self, row_height: u32) {
let next = row_height.max(1);
if self.row_height == next {
return;
}
self.row_height = next;
self.clear_cache();
self.emit_visible_window_changed();
self.base.request_layout();
self.base.request_redraw();
}
pub fn row_height(&self) -> u32 {
self.row_height
}
pub fn set_default_column_width(&mut self, column_width: u32) {
let next = column_width.max(1);
if self.column_width == next {
return;
}
self.column_width = next;
self.column_geometry_revision = self.column_geometry_revision.wrapping_add(1);
self.clear_cache();
self.emit_visible_window_changed();
self.base.request_layout();
self.base.request_redraw();
}
pub fn default_column_width(&self) -> u32 {
self.column_width
}
pub fn column_width(&self, column: usize) -> u32 {
self.column_width_at(column)
}
pub fn set_column_width(&mut self, column: usize, width: u32) -> bool {
self.set_column_width_at(column, width)
}
pub fn set_frozen_columns(&mut self, frozen_columns: usize) {
let next = frozen_columns.min(self.column_count());
if self.frozen_columns == next {
return;
}
self.frozen_columns = next;
self.base.request_layout();
self.base.request_redraw();
}
pub fn frozen_columns(&self) -> usize {
self.frozen_columns
}
pub fn full_model_projection(&self) -> bool {
self.full_model_projection
}
pub fn set_full_model_projection(&mut self, enabled: bool) {
if self.full_model_projection == enabled {
return;
}
self.full_model_projection = enabled;
self.full_projection_cache = None;
self.clear_cache();
self.base.request_redraw();
}
pub fn column_width_mode(&self) -> ColumnWidthMode {
self.column_width_mode
}
pub fn section_resize_mode(&self) -> ColumnWidthMode {
self.column_width_mode
}
pub fn set_section_resize_mode(&mut self, mode: ColumnWidthMode) {
self.set_column_width_mode(mode);
}
pub fn set_column_width_mode(&mut self, mode: ColumnWidthMode) {
if self.column_width_mode == mode {
return;
}
self.column_width_mode = mode;
self.column_geometry_revision = self.column_geometry_revision.wrapping_add(1);
self.clear_cache();
self.base.request_redraw();
}
pub fn alternating_rows(&self) -> bool {
self.alternating_rows
}
pub fn set_alternating_rows(&mut self, alternating: bool) {
if self.alternating_rows == alternating {
return;
}
self.alternating_rows = alternating;
self.base.request_redraw();
}
pub fn edit_triggers(&self) -> EditTriggers {
self.edit_triggers
}
pub fn set_edit_triggers(&mut self, triggers: EditTriggers) {
if self.edit_triggers == triggers {
return;
}
self.edit_triggers = triggers;
}
pub fn show_scrollbars(&self) -> bool {
self.show_scrollbars
}
pub fn set_show_scrollbars(&mut self, show: bool) {
if self.show_scrollbars == show {
return;
}
self.show_scrollbars = show;
self.base.request_redraw();
}
fn effective_column_width(&self) -> u32 {
match self.column_width_mode {
ColumnWidthMode::Fixed | ColumnWidthMode::Interactive => self.column_width,
ColumnWidthMode::Stretch => {
let count = self.column_count().max(1) as u32;
(self.cells_box().width / count).max(1)
}
ColumnWidthMode::StretchLast => self.column_width,
}
}
pub fn set_sort_specs(&mut self, sort_specs: Vec<SortSpec>) {
self.sort_specs = sort_specs;
self.full_projection_cache = None;
self.clear_cache();
self.base.request_redraw();
}
pub fn sort_specs(&self) -> &[SortSpec] {
&self.sort_specs
}
pub fn sort_role(&self, column: usize) -> Option<DataRole> {
self.sort_roles.get(&column).copied()
}
pub fn set_sort_role(&mut self, column: usize, role: DataRole) {
if self.sort_roles.insert(column, role) == Some(role) {
return;
}
self.full_projection_cache = None;
self.clear_cache();
self.base.request_redraw();
}
pub fn clear_sort_role(&mut self, column: usize) {
if self.sort_roles.remove(&column).is_some() {
self.full_projection_cache = None;
self.clear_cache();
self.base.request_redraw();
}
}
pub fn sort_indicator_visible(&self) -> bool {
self.sort_indicator_visible
}
pub fn set_sort_indicator_visible(&mut self, visible: bool) {
if self.sort_indicator_visible == visible {
return;
}
self.sort_indicator_visible = visible;
self.base.request_redraw();
}
pub fn column_width_at(&self, column: usize) -> u32 {
match self.column_width_mode {
ColumnWidthMode::Fixed => self.column_width,
ColumnWidthMode::Interactive => {
self.column_widths.get(&column).copied().unwrap_or(self.column_width)
}
ColumnWidthMode::Stretch => {
(self.cells_box().width / self.column_count().max(1) as u32).max(1)
}
ColumnWidthMode::StretchLast => {
let count = self.column_count().max(1);
if column + 1 != count {
return self.column_widths.get(&column).copied().unwrap_or(self.column_width);
}
let used: u32 = (0..count - 1)
.map(|index| {
self.column_widths.get(&index).copied().unwrap_or(self.column_width)
})
.sum();
self.cells_box().width.saturating_sub(used).max(1)
}
}
}
pub fn set_column_width_at(&mut self, column: usize, width: u32) -> bool {
if !self.store_column_width(column, width) {
return false;
}
self.column_resized.emit((column, self.column_width_at(column)));
true
}
fn store_column_width(&mut self, column: usize, width: u32) -> bool {
if column >= self.column_count() {
return false;
}
let width = width.max(1);
self.column_width_mode = ColumnWidthMode::Interactive;
self.column_geometry_revision = self.column_geometry_revision.wrapping_add(1);
if self.column_widths.insert(column, width) == Some(width) {
return true;
}
self.clear_cache();
self.emit_visible_window_changed();
self.base.request_layout();
self.base.request_redraw();
true
}
pub fn resize_column_to_contents(&mut self, column: usize) -> bool {
let Some(source) = self.data_source.clone() else {
return false;
};
if column >= source.column_count() {
return false;
}
let font = effective_font(self.style());
let mut widest = 0u32;
let mut text = String::new();
for row in 0..source.row_count() {
text.clear();
if source.fill_cell(row, column, &mut text) {
widest = widest.max(crate::widget::metrics::estimate_text_width(&text, font, 1.0));
}
}
self.set_column_width_at(column, widest.saturating_add(16).max(24))
}
fn resize_handle_at(&self, point: Point) -> Option<usize> {
let band = self.header_band();
if !band.contains_point(point) {
return None;
}
let cells = self.cells_box();
for column in 0..self.column_count() {
let x = cells.x + self.column_view_x(column);
let right = x + self.column_width_at(column) as i32;
if right <= band.x {
continue;
}
if x >= band.x + band.width as i32 {
break;
}
if (point.x - right).abs() <= 4 {
return Some(column);
}
}
None
}
fn column_view_x(&self, column: usize) -> i32 {
let frozen = self.frozen_columns.min(self.column_count());
let frozen_width = self.column_prefix_width(frozen);
if column < frozen {
self.column_prefix_width(column)
} else {
let start = self.scroll_column.max(frozen);
frozen_width + (self.column_prefix_width(column) - self.column_prefix_width(start))
}
}
fn column_prefix_width(&self, upto: usize) -> i32 {
let count = self.column_count();
let cells_width = self.cells_box().width;
let mut cache = self.column_prefix_cache.borrow_mut();
let stale = match cache.as_ref() {
Some(entry) => {
entry.revision != self.column_geometry_revision
|| entry.column_count != count
|| entry.cells_width != cells_width
|| entry.mode != self.column_width_mode
|| entry.default_width != self.column_width
}
None => true,
};
if stale {
*cache = Some(self.build_column_prefixes(count, cells_width));
self.column_prefix_rebuilds.set(self.column_prefix_rebuilds.get().saturating_add(1));
}
let entry = cache.as_ref().expect("the prefix table was just built");
if upto <= entry.column_count {
return entry.prefix.get(upto).copied().unwrap_or(0);
}
let base = entry.prefix.get(entry.column_count).copied().unwrap_or(0);
base + (entry.column_count..upto)
.map(|column| self.column_width_at(column) as i32)
.sum::<i32>()
}
fn build_column_prefixes(&self, count: usize, cells_width: u32) -> ColumnPrefixCache {
let mut prefix = Vec::with_capacity(count.saturating_add(1));
let mut sum = 0i32;
prefix.push(0);
for column in 0..count {
sum = sum.saturating_add(self.column_width_at(column) as i32);
prefix.push(sum);
}
ColumnPrefixCache {
revision: self.column_geometry_revision,
column_count: count,
cells_width,
mode: self.column_width_mode,
default_width: self.column_width,
prefix,
}
}
fn column_at_dx(&self, dx: i32) -> Option<usize> {
if dx < 0 {
return None;
}
let frozen = self.frozen_columns.min(self.column_count());
let frozen_width = self.column_prefix_width(frozen);
let (mut column, mut x) = if dx < frozen_width {
(0, 0)
} else {
(self.scroll_column.max(frozen), frozen_width)
};
while column < self.column_count() {
let width = self.column_width_at(column) as i32;
if dx < x + width {
return Some(column);
}
x += width;
column += 1;
}
None
}
pub fn set_filters(&mut self, filters: Vec<ColumnFilter>) {
let expr = FilterExpr::from_conditions(
filters
.iter()
.map(|filter| FilterCondition::contains(filter.column, filter.query.clone()))
.collect(),
);
self.set_filter_expr(expr);
self.filters = filters;
}
pub fn filters(&self) -> Vec<ColumnFilter> {
self.filter_expr
.conditions()
.iter()
.map(|condition| ColumnFilter {
column: condition.column,
query: condition.operand.clone(),
})
.collect()
}
pub fn set_filter_expr(&mut self, filter_expr: FilterExpr) {
self.filter_expr = filter_expr.clone();
self.full_projection_cache = None;
self.clear_cache();
self.filter_changed.emit(filter_expr);
self.base.request_redraw();
}
pub fn filter_expr(&self) -> &FilterExpr {
&self.filter_expr
}
pub fn visible_window(&self) -> (usize, usize, usize, usize) {
let row_count = self.row_count();
let column_count = self.column_count();
if row_count == 0 || column_count == 0 {
return (0, 0, 0, 0);
}
let row_start = self.scroll_row.saturating_sub(self.overscan_rows);
let col_start = self.scroll_column.saturating_sub(self.overscan_columns);
let visible_rows = self.visible_row_capacity();
let visible_cols = self.visible_column_capacity();
let row_fetch = visible_rows.saturating_add(self.overscan_rows.saturating_mul(2));
let col_fetch = visible_cols.saturating_add(self.overscan_columns.saturating_mul(2));
let row_end = row_start.saturating_add(row_fetch).min(row_count);
let col_end = col_start.saturating_add(col_fetch).min(column_count);
let col_start = if self.frozen_columns > 0 { 0 } else { col_start };
let col_len = col_end.saturating_sub(col_start);
(row_start, row_end.saturating_sub(row_start), col_start, col_len)
}
fn window_origin(&self) -> (usize, usize) {
let (row_start, _, col_start, _) = self.visible_window();
(row_start, col_start)
}
pub fn fetch_visible_cells(&mut self) -> Vec<Vec<Option<String>>> {
(*self.fetch_visible_cells_shared()).clone()
}
pub fn fetch_visible_cells_shared(&mut self) -> Rc<Vec<Vec<Option<String>>>> {
if self.full_model_projection {
return self.fetch_full_projected_window();
}
self.fetch_window_shared()
}
fn fetch_visible_window_view(&mut self) -> ProjectedWindowView {
self.normalize_projection_state();
if self.full_model_projection {
let projected = self.full_projection_rows();
let (row_start, row_len, col_start, col_len) = self.visible_window();
return ProjectedWindowView { rows: projected, row_start, row_len, col_start, col_len };
}
let cells = self.fetch_window_shared();
let row_len = cells.len();
let col_len = cells.first().map_or(0, Vec::len);
ProjectedWindowView { rows: cells, row_start: 0, row_len, col_start: 0, col_len }
}
fn fetch_window_shared(&mut self) -> Rc<Vec<Vec<Option<String>>>> {
self.normalize_projection_state();
let Some(source) = self.data_source.as_ref() else {
return Rc::new(Vec::new());
};
let (row_start, row_len, col_start, col_len) = self.visible_window();
if row_len == 0 || col_len == 0 {
return Rc::new(Vec::new());
}
let revision = source.revision();
let cache_revision =
if revision > 0 { revision } else { self.data_generation.load(Ordering::Relaxed) };
if self.data_is_observable {
let mut cached_projection = None;
let mut cached_cells = None;
if let Some(cache) = self.window_cache.as_ref() {
if cache.row_start == row_start
&& cache.row_len == row_len
&& cache.column_start == col_start
&& cache.column_len == col_len
&& cache.revision == cache_revision
{
if self.filter_expr.is_noop() && self.sort_specs.is_empty() {
return Rc::clone(&cache.cells);
}
cached_projection = cache.projected.clone();
cached_cells = Some(Rc::clone(&cache.cells));
}
}
if let Some(projected) = cached_projection {
return projected;
}
if let Some(cells) = cached_cells {
let projected = Rc::new(self.apply_filter_sort((*cells).clone(), row_start));
if let Some(cache) = self.window_cache.as_mut() {
cache.projected = Some(Rc::clone(&projected));
}
return projected;
}
}
let cells = Rc::new(source.fetch_window(row_start, row_len, col_start, col_len));
if self.data_is_observable {
self.window_cache = Some(GridWindowCache {
row_start,
row_len,
column_start: col_start,
column_len: col_len,
revision: cache_revision,
cells: Rc::clone(&cells),
projected: None,
});
} else {
self.window_cache = None;
}
if self.filter_expr.is_noop() && self.sort_specs.is_empty() {
cells
} else {
let projected = Rc::new(self.apply_filter_sort((*cells).clone(), row_start));
if let Some(cache) = self.window_cache.as_mut() {
cache.projected = Some(Rc::clone(&projected));
}
projected
}
}
pub fn refresh_data(&mut self) {
self.data_generation.fetch_add(1, Ordering::Relaxed);
self.full_projection_cache = None;
self.full_window_cache = None;
self.clear_cache();
self.emit_visible_window_changed();
self.base.request_redraw();
}
fn fetch_full_projected_window(&mut self) -> Rc<Vec<Vec<Option<String>>>> {
let projected = self.full_projection_rows();
if projected.is_empty() {
return Rc::new(Vec::new());
}
let window = self.visible_window();
if let Some((cached_projection, cached_window, cells)) = self.full_window_cache.as_ref() {
if Rc::ptr_eq(cached_projection, &projected) && *cached_window == window {
return Rc::clone(cells);
}
}
let window_cells = Rc::new(Self::slice_window(&projected, window));
self.full_window_cache = Some((Rc::clone(&projected), window, Rc::clone(&window_cells)));
window_cells
}
fn full_projection_rows(&mut self) -> Rc<ProjectedRows> {
let Some(source) = self.data_source.as_ref() else {
return Rc::new(Vec::new());
};
let revision = source.revision();
let cache_key =
if revision > 0 { revision } else { self.data_generation.load(Ordering::Relaxed) };
if let Some((key, projected)) = self.full_projection_cache.as_ref() {
if *key == cache_key {
return Rc::clone(projected);
}
}
let rows = source.row_count();
let columns = source.column_count();
let cells = source.fetch_window(0, rows, 0, columns);
let projected = Rc::new(self.apply_filter_sort(cells, 0));
self.full_projection_cache = Some((cache_key, Rc::clone(&projected)));
projected
}
fn slice_window(
rows: &[Vec<Option<String>>],
(row_start, row_len, col_start, col_len): (usize, usize, usize, usize),
) -> Vec<Vec<Option<String>>> {
rows.iter()
.skip(row_start)
.take(row_len)
.map(|row| row.iter().skip(col_start).take(col_len).cloned().collect::<Vec<_>>())
.collect()
}
fn apply_filter_sort(
&self,
cells: Vec<Vec<Option<String>>>,
row_origin: usize,
) -> Vec<Vec<Option<String>>> {
let row_span = cells.len();
let mut rows: Vec<(usize, Vec<Option<String>>)> = cells
.into_iter()
.enumerate()
.map(|(offset, row)| (row_origin.saturating_add(offset), row))
.collect();
if !self.filter_expr.is_noop() {
rows.retain(|(_, row)| self.filter_expr.accepts(row));
}
if !self.sort_specs.is_empty() {
let source = self.data_source.as_deref();
let mut key_sets: Vec<Vec<Option<(String, Option<f64>)>>> =
Vec::with_capacity(self.sort_specs.len());
for spec in &self.sort_specs {
let role = self.sort_roles.get(&spec.column).copied();
let mut keys: Vec<Option<(String, Option<f64>)>> = vec![None; row_span];
for (source_row, row) in &rows {
let role_value = match (role, source) {
(Some(role), Some(source)) => {
source.data_role(*source_row, spec.column, role)
}
_ => None,
};
let display = row
.get(spec.column)
.and_then(|cell| cell.as_ref())
.cloned()
.unwrap_or_default();
if let Some(slot) = keys.get_mut(source_row.saturating_sub(row_origin)) {
*slot = Some(normalise_sort_key(&role_value.unwrap_or(display)));
}
}
key_sets.push(keys);
}
let empty = (String::new(), None);
rows.sort_by(|(left_row, _), (right_row, _)| {
let left = *left_row - row_origin;
let right = *right_row - row_origin;
for (index, spec) in self.sort_specs.iter().enumerate() {
let left_key = key_sets[index].get(left).and_then(|key| key.as_ref());
let right_key = key_sets[index].get(right).and_then(|key| key.as_ref());
let ordering =
compare_sort_keys(left_key.unwrap_or(&empty), right_key.unwrap_or(&empty));
if ordering != core::cmp::Ordering::Equal {
return if spec.descending { ordering.reverse() } else { ordering };
}
}
left_row.cmp(right_row)
});
}
rows.into_iter().map(|(_, row)| row).collect()
}
fn visible_row_capacity(&self) -> usize {
let height = self.base.geometry().height;
if height == 0 {
return 0;
}
height.div_ceil(self.row_height.max(1)) as usize
}
fn cells_box(&self) -> Rect {
let content = ControlMetrics::band_inset(self.base.geometry(), GRID_INSET);
let height = content.height.saturating_sub(GRID_HEADER_HEIGHT);
Rect::new(content.x, content.y + GRID_HEADER_HEIGHT as i32, content.width, height)
}
fn cell_rect(&self, row: usize, column: usize) -> Rect {
let cells = self.cells_box();
let row = row.saturating_sub(self.scroll_row);
Rect::new(
cells.x + self.column_view_x(column),
cells.y + row as i32 * self.row_height as i32,
self.column_width_at(column),
self.row_height,
)
}
fn paint_cell(
&self,
context: &mut RenderContext,
cell_rect: Rect,
value: Option<&String>,
cell_border: Color,
ink: Color,
) {
context.draw_rect(cell_rect, cell_border);
let Some(text) = value else {
return;
};
if text.is_empty() {
return;
}
let font = effective_font(self.style());
context.draw_text_fitted_cow(
context.text_line(cell_rect, font),
text,
font,
ink,
HorizontalAlignment::Left,
);
}
pub fn cell_at(&self, point: Point) -> Option<(usize, usize)> {
let cells = self.cells_box();
if !cells.contains_point(point) {
return None;
}
let dx = point.x - cells.x;
let dy = point.y - cells.y;
if dx < 0 || dy < 0 {
return None;
}
let column = self.column_at_dx(dx)?;
let row = self.scroll_row + dy as usize / self.row_height.max(1) as usize;
if column >= self.column_count() || row >= self.row_count() {
return None;
}
self.cell_rect(row, column).contains_point(point).then_some((row, column))
}
fn visible_column_capacity(&self) -> usize {
let width = self.base.geometry().width;
if width == 0 {
return 0;
}
width.div_ceil(self.effective_column_width().max(1)) as usize
}
fn header_band(&self) -> Rect {
let cells = self.cells_box();
Rect::new(cells.x, cells.y - GRID_HEADER_HEIGHT as i32, cells.width, GRID_HEADER_HEIGHT)
}
fn header_column_at(&self, point: Point) -> Option<usize> {
let band = self.header_band();
if !band.contains_point(point) {
return None;
}
let dx = point.x - band.x;
if dx < 0 {
return None;
}
self.column_at_dx(dx).filter(|column| *column < self.column_count())
}
fn normalize_projection_state(&mut self) {
let row_count = self.row_count();
let col_count = self.column_count();
if row_count == 0 {
self.scroll_row = 0;
} else if self.scroll_row >= row_count {
self.scroll_row = row_count.saturating_sub(1);
}
if col_count == 0 {
self.scroll_column = 0;
self.frozen_columns = 0;
} else {
if self.scroll_column >= col_count {
self.scroll_column = col_count.saturating_sub(1);
}
self.frozen_columns = self.frozen_columns.min(col_count);
}
}
fn clear_cache(&mut self) {
self.window_cache = None;
}
fn emit_visible_window_changed(&self) {
self.visible_window_changed.emit(self.visible_window());
}
}
impl Widget for DataGrid {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(
crate::widget::metrics::dimensions::DATA_GRID_DEFAULT_WIDTH,
crate::widget::metrics::dimensions::DATA_GRID_DEFAULT_HEIGHT,
)
}
fn as_draw_mut(&mut self) -> Option<&mut dyn crate::widget::Draw> {
Some(self)
}
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"visible_window_changed" => Some(EventSignalRef::mapped(
"visible_window_changed",
&self.visible_window_changed,
|v| {
CapabilityValue::Tuple(alloc::vec![
CapabilityValue::UInt(v.0 as u64),
CapabilityValue::UInt(v.1 as u64),
CapabilityValue::UInt(v.2 as u64),
CapabilityValue::UInt(v.3 as u64),
])
},
)),
"context_menu_requested" => Some(EventSignalRef::mapped(
"context_menu_requested",
&self.context_menu_requested,
|v| crate::widget::capability::CapabilityValue::String(format!("{},{}", v.x, v.y)),
)),
"edit_requested" => {
Some(EventSignalRef::mapped("edit_requested", &self.edit_requested, |v| {
crate::widget::capability::CapabilityValue::Tuple(crate::compat::Vec::from([
crate::widget::capability::CapabilityValue::Int(v.0 as i64),
crate::widget::capability::CapabilityValue::Int(v.1 as i64),
]))
}))
}
"filter_changed" => {
Some(EventSignalRef::mapped("filter_changed", &self.filter_changed, |v| {
crate::widget::capability::CapabilityValue::String(format!("{v:?}"))
}))
}
"header_clicked" => {
Some(EventSignalRef::mapped("header_clicked", &self.header_clicked, |v| {
crate::widget::capability::CapabilityValue::UInt(*v as u64)
}))
}
"page_applied" => {
Some(EventSignalRef::mapped("page_applied", &self.page_applied, |v| {
CapabilityValue::UInt(*v as u64)
}))
}
"sort_changed" => {
Some(EventSignalRef::mapped("sort_changed", &self.sort_changed, |v| {
crate::widget::capability::CapabilityValue::Tuple(crate::compat::Vec::from([
crate::widget::capability::CapabilityValue::UInt(v.0 as u64),
crate::widget::capability::CapabilityValue::Bool(v.1),
]))
}))
}
_ => None,
}
}
}
impl WidgetProperties for DataGrid {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"has_data_source" => Ok(CapabilityValue::Bool(self.has_data_source())),
"row_count" => Ok(CapabilityValue::UInt(self.row_count() as u64)),
"column_count" => Ok(CapabilityValue::UInt(self.column_count() as u64)),
"scroll_row" => Ok(CapabilityValue::UInt(self.scroll_row() as u64)),
"scroll_column" => Ok(CapabilityValue::UInt(self.scroll_column() as u64)),
"row_height" => Ok(CapabilityValue::UInt(self.row_height() as u64)),
"column_width" => Ok(CapabilityValue::UInt(self.default_column_width() as u64)),
"frozen_columns" => Ok(CapabilityValue::UInt(self.frozen_columns() as u64)),
"sort_spec_count" => Ok(CapabilityValue::UInt(self.sort_specs().len() as u64)),
"filter_count" => {
Ok(CapabilityValue::UInt(self.filter_expr().condition_count() as u64))
}
"sort_specs" => Ok(CapabilityValue::String(sort_specs_to_string(self.sort_specs()))),
"filters" => Ok(CapabilityValue::String(column_filters_to_string(&self.filters()))),
"selection_mode" => Ok(CapabilityValue::String(
data_grid_selection_mode_to_str(self.selection_mode()).to_string(),
)),
"selection_behavior" => Ok(CapabilityValue::String(
selection_behavior_to_str(self.selection_behavior()).to_string(),
)),
"sort_indicator_visible" => Ok(CapabilityValue::Bool(self.sort_indicator_visible())),
"section_resize_mode" => Ok(CapabilityValue::String(
column_width_mode_to_str(self.section_resize_mode()).to_string(),
)),
"current_cell" => match self.current_cell() {
Some((row, column)) => Ok(CapabilityValue::String(format!("{row},{column}"))),
None => Ok(CapabilityValue::Null),
},
"summary_text" => Ok(CapabilityValue::String(self.summary_text())),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"summary_text" => Err(CapabilityAccessError::ReadOnlyProperty),
"scroll_row" => {
self.set_scroll_row(expect_usize(value)?);
Ok(())
}
"scroll_column" => {
self.set_scroll_column(expect_usize(value)?);
Ok(())
}
"row_height" => {
self.set_row_height(expect_u32_saturating(value)?);
Ok(())
}
"column_width" => {
self.set_default_column_width(expect_u32_saturating(value)?);
Ok(())
}
"frozen_columns" => {
self.set_frozen_columns(expect_usize(value)?);
Ok(())
}
"sort_specs" => {
self.set_sort_specs(expect_sort_specs(value)?);
Ok(())
}
"filters" => {
self.set_filters(expect_column_filters(value)?);
Ok(())
}
"selection_mode" => {
self.set_selection_mode(expect_data_grid_selection_mode(value)?);
Ok(())
}
"selection_behavior" => {
self.set_selection_behavior(expect_selection_behavior(value)?);
Ok(())
}
"sort_indicator_visible" => {
self.set_sort_indicator_visible(crate::widget::capability::coercion::expect_bool(
value,
)?);
Ok(())
}
"section_resize_mode" => {
self.set_section_resize_mode(expect_column_width_mode(value)?);
Ok(())
}
"current_cell" => Err(CapabilityAccessError::ReadOnlyProperty),
"has_data_source" | "row_count" | "column_count" | "sort_spec_count"
| "filter_count" => Err(CapabilityAccessError::ReadOnlyProperty),
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"has_data_source",
"row_count",
"column_count",
"scroll_row",
"scroll_column",
"row_height",
"column_width",
"frozen_columns",
"sort_spec_count",
"filter_count",
"sort_specs",
"filters",
"selection_mode",
"selection_behavior",
"sort_indicator_visible",
"section_resize_mode",
"current_cell",
"summary_text",
BASE_PROPERTY_NAMES
]
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"clear_data_source" => {
self.clear_data_source();
Ok(())
}
"set_data_source" | "set_scroll_row" | "set_scroll_column" | "set_row_height"
| "set_column_width" | "set_frozen_columns" => Err(CapabilityAccessError::OutOfRange),
_ => Err(CapabilityAccessError::UnknownCommand),
}
}
}
impl Draw for DataGrid {
fn draw(&mut self, context: &mut RenderContext) {
self.poll_pagination();
let rect = self.base.geometry();
let (background_color, text_color, border_color) = {
let style = self.base.style();
(style.background_color, style.text_color, style.border_color)
};
let theme = crate::style::resolved_theme_style_shared("data_grid");
let mut surface = background_color
.or_else(|| theme.as_ref().and_then(|t| t.background_color))
.unwrap_or(Color::WHITE);
let ink = text_color
.or_else(|| theme.as_ref().and_then(|t| t.text_color))
.unwrap_or(Color::BLACK);
let palette = crate::style::palette_shared();
let window_fill = palette.as_ref().map(|colors| colors.background).unwrap_or(Color::WHITE);
if surface == window_fill {
surface = window_fill.blend(&ink, 0.08);
}
let border = border_color
.or_else(|| theme.as_ref().and_then(|t| t.border_color))
.filter(|resolved| *resolved != surface)
.unwrap_or_else(|| surface.blend(&ink, 0.20));
let cell_border = palette
.as_ref()
.map(|colors| colors.outline_variant)
.filter(|resolved| *resolved != surface)
.unwrap_or_else(|| surface.blend(&ink, 0.10));
let accent = palette.as_ref().map(|colors| colors.primary).unwrap_or(Color::PRIMARY);
let selected_bg = surface.blend(&accent, 0.22);
context.face_with_gradient(
rect,
surface,
self.style().background_gradient.as_ref(),
self.style().surface.unwrap_or_default(),
self.style().border_radius.unwrap_or(0),
Color::BLACK,
);
crate::widget::draw::stroke_surface_border(
context,
rect,
self.style().border_radius.unwrap_or(0),
border,
1,
);
let window_view = self.fetch_visible_window_view();
if window_view.is_empty() {
return;
}
let cells = self.cells_box();
let header_font = effective_font(self.style());
let header_y = cells.y - GRID_HEADER_HEIGHT as i32;
if let Some(source) = self.data_source.as_ref() {
let cells_right = cells.x + cells.width as i32;
let generation = self.data_generation.load(Ordering::Relaxed);
let column_count = self.column_count();
let mut text_cache = self.header_text_cache.borrow_mut();
if text_cache.labels_revision != self.header_text_revision
|| text_cache.generation != generation
|| text_cache.column_count != column_count
{
text_cache.entries.clear();
text_cache.labels_revision = self.header_text_revision;
text_cache.generation = generation;
text_cache.column_count = column_count;
}
for column in self.scroll_column..column_count {
let x = cells.x + self.column_view_x(column);
if x >= cells_right {
break;
}
let width = self.column_width_at(column).max(1);
let band = Rect::new(x, header_y, width, GRID_HEADER_HEIGHT);
context.fill_rect(band, surface.blend(&ink, 0.06));
let name = text_cache.entries.entry((column, width)).or_insert_with(|| {
let raw = self
.header_labels
.get(column)
.filter(|label| !label.is_empty())
.cloned()
.or_else(|| source.column_name(column));
raw.map_or_else(String::new, |name| {
crate::widget::text_utils::elide_end(&name, 24).into_owned()
})
});
if !name.is_empty() {
let filter_slot = if self.header_filter_visible
&& self.header_columns.get(column).is_some_and(|spec| spec.filterable)
{
12
} else {
0
};
let sort_slot = if self.sort_indicator_visible
&& self.sort_specs.iter().any(|spec| spec.column == column)
{
12
} else {
0
};
let arrow = filter_slot + sort_slot;
let text_band = Rect::new(
band.x + 2,
band.y,
band.width.saturating_sub(4 + arrow),
band.height,
);
context.draw_text_fitted_cow(
context.text_line(text_band, header_font),
name.as_str(),
header_font,
ink,
HorizontalAlignment::Left,
);
}
if self.sort_indicator_visible {
if let Some(spec) = self.sort_specs.iter().find(|spec| spec.column == column) {
let icon_side = band.height.min(10);
let filter_slot = if self.header_filter_visible
&& self.header_columns.get(column).is_some_and(|spec| spec.filterable)
{
12
} else {
0
};
let icon_rect = Rect::new(
band.x + band.width as i32 - filter_slot - icon_side as i32 - 3,
band.y + (band.height as i32 - icon_side as i32) / 2,
icon_side,
icon_side,
);
let icon_name = if spec.descending {
crate::widget::IconName::ArrowDown
} else {
crate::widget::IconName::ArrowUp
};
crate::widget::draw_icon_at(context, icon_rect, ink, icon_name);
}
}
if self.header_filter_visible
&& self.header_columns.get(column).is_some_and(|spec| spec.filterable)
{
let icon_side = band.height.min(10);
let icon_rect = Rect::new(
band.x + band.width as i32 - icon_side as i32 - 3,
band.y + (band.height as i32 - icon_side as i32) / 2,
icon_side,
icon_side,
);
crate::widget::draw_icon_at(
context,
icon_rect,
ink.blend(&surface, 0.3),
crate::widget::IconName::Filter,
);
}
}
}
let cells = self.cells_box();
let cells_right = cells.x + cells.width as i32;
let cells_bottom = cells.y + cells.height as i32;
let (row_origin, col_origin) = self.window_origin();
if self.alternating_rows {
for row_idx in 0..window_view.len() {
let global_row = row_origin + row_idx;
if global_row % 2 == 1 {
let y = self.cell_rect(global_row, 0).y;
context.fill_rect(
Rect::new(cells.x, y, cells.width, self.row_height),
surface.blend(&accent, 0.05),
);
}
}
}
for row_idx in 0..window_view.len() {
if self.cell_rect(row_origin + row_idx, 0).y >= cells_bottom {
break;
}
let Some(row) = window_view.row(row_idx) else { continue };
for (col_idx, cell) in row.iter().enumerate() {
let global_column = col_origin + col_idx;
if global_column < self.frozen_columns {
continue;
}
let cell_rect = self.cell_rect(row_origin + row_idx, global_column);
if cell_rect.x >= cells_right {
break;
}
if self.selection_contains(row_origin + row_idx, global_column) {
context.fill_rect(cell_rect, selected_bg);
}
self.paint_cell(context, cell_rect, cell.as_ref(), cell_border, ink);
}
}
if self.frozen_columns > 0 {
for row_idx in 0..window_view.len() {
let Some(row) = window_view.row(row_idx) else { continue };
let global_row = row_origin + row_idx;
if self.cell_rect(global_row, 0).y >= cells_bottom {
break;
}
for column in 0..self.frozen_columns {
let local = column.saturating_sub(col_origin);
let cell_rect = self.cell_rect(global_row, column);
if cell_rect.x >= cells_right {
break;
}
context.fill_rect(cell_rect, surface);
if self.selection_contains(global_row, column) {
context.fill_rect(cell_rect, selected_bg);
}
let value = row.get(local).and_then(|cell| cell.as_ref());
self.paint_cell(context, cell_rect, value, cell_border, ink);
}
}
}
if let Some((row, column)) = self.hovered_cell {
if !self.selection_contains(row, column) {
let box_rect = self.cell_rect(row, column);
context.draw_rect_stroke(box_rect, accent.with_alpha(120), 1);
}
}
if let Some((row, column)) = self.current_cell.or(self.selection) {
let box_rect = self.cell_rect(row, column);
context.draw_rect_stroke(box_rect, accent, 2);
}
if self.frozen_columns > 0 {
let split_x = cells.x + self.column_prefix_width(self.frozen_columns);
if split_x > cells.x {
context.draw_line(
Point::new(split_x, cells.y),
Point::new(split_x, cells.y + cells.height as i32),
accent,
);
}
}
if self.show_scrollbars {
let total_rows = self.row_count().max(1);
let visible_rows = self.visible_row_capacity().max(1).min(total_rows);
let track = cells.height.max(1);
let thumb = ((track as u64 * visible_rows as u64) / total_rows as u64)
.max(24)
.min(track as u64) as u32;
let travel = track.saturating_sub(thumb);
let max_scroll = total_rows.saturating_sub(visible_rows);
let offset = if max_scroll == 0 {
0
} else {
(travel as u64 * self.scroll_row.min(max_scroll) as u64 / max_scroll as u64) as u32
};
let band = Rect::new(rect.x + rect.width as i32 - 6, cells.y + offset as i32, 6, thumb);
context.fill_rect(band, accent.with_alpha(120));
}
}
}
impl crate::event::EventHandler for DataGrid {
fn handle_event(&mut self, event: &Event) {
self.poll_pagination();
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
if let Event::Wheel { delta, modifiers } = event {
let scroll = WheelScroll::from_event(*delta, *modifiers, 1);
let lines = scroll.lines_y.unsigned_abs() as usize;
if scroll.lines_y < 0 {
let next = self.scroll_row.saturating_add(lines);
self.set_scroll_row(next);
} else if scroll.lines_y > 0 {
let up = self.scroll_row.saturating_sub(lines);
self.set_scroll_row(up);
}
if scroll.columns_x != 0 {
let next = self.scroll_column.saturating_add_signed(scroll.columns_x as isize);
self.set_scroll_column(next);
}
let mods = crate::shortcut::Modifiers::from_event_bits(*modifiers);
if mods.contains(crate::shortcut::Modifiers::SHIFT) {
let next = self.scroll_column.saturating_add_signed(-(scroll.lines_y as isize));
self.set_scroll_column(next);
}
}
if let Event::MousePress { pos, button, modifiers } = event {
if *button == 3 {
self.context_menu_requested.emit(*pos);
return;
}
if *button != 1 {
return;
}
if let Some(column) = self.resize_handle_at(*pos) {
self.resizing_column = Some(column);
self.resize_start_x = pos.x;
self.resize_start_width = self.column_width_at(column);
return;
}
if let Some(column) = self.header_column_at(*pos) {
self.header_clicked.emit(column);
let existing = self.sort_specs.iter().position(|spec| spec.column == column);
let descending = match existing {
Some(index) => {
let descending = !self.sort_specs[index].descending;
self.sort_specs[index].descending = descending;
Some(descending)
}
None => {
self.sort_specs.push(SortSpec { column, descending: false });
Some(false)
}
};
self.full_projection_cache = None;
self.clear_cache();
if let Some(descending) = descending {
self.sort_changed.emit((column, descending));
}
self.base.request_redraw();
return;
}
let hit = self.cell_at(*pos);
self.current_cell = hit;
match hit {
Some((row, column)) => {
self.cell_clicked.emit((row, column));
self.apply_selection_click(row, column, *modifiers);
self.current_changed.emit((row, column));
}
None => {
if self.selection.is_some() || !self.selected_cells.is_empty() {
self.clear_selection();
}
}
}
self.base.request_redraw();
}
match event {
Event::MouseDoubleClick { pos, button: 1 } => {
if let Some((row, column)) = self.cell_at(*pos) {
self.cell_double_clicked.emit((row, column));
self.apply_selection_click(row, column, 0);
self.current_cell = Some((row, column));
self.current_changed.emit((row, column));
if self.edit_triggers.contains(EditTriggers::DOUBLE_CLICKED) {
self.edit_requested.emit((row, column));
}
self.base.request_redraw();
}
}
Event::ContextMenu { pos } => self.context_menu_requested.emit(*pos),
Event::KeyPress { key, modifiers } if self.base.focus_reason().is_some() => {
let page = self.visible_row_capacity().max(1);
let last_row = self.row_count().saturating_sub(1);
let last_column = self.column_count().saturating_sub(1);
if self.row_count() == 0 || self.column_count() == 0 {
return;
}
let (mut row, mut column) = self
.current_cell
.or(self.selection)
.unwrap_or((self.scroll_row, self.scroll_column));
let modifiers = crate::shortcut::Modifiers::from_event_bits(*modifiers);
let shift = modifiers.contains(crate::shortcut::Modifiers::SHIFT)
&& self.selection_mode
== crate::widget::view_widgets::list_view::SelectionMode::Extended;
match *key {
crate::event::key_codes::UP => row = row.saturating_sub(1),
crate::event::key_codes::DOWN => row = row.saturating_add(1).min(last_row),
crate::event::key_codes::LEFT => column = column.saturating_sub(1),
crate::event::key_codes::RIGHT => {
column = column.saturating_add(1).min(last_column)
}
crate::event::key_codes::PAGE_UP => row = row.saturating_sub(page),
crate::event::key_codes::PAGE_DOWN => {
row = row.saturating_add(page).min(last_row)
}
crate::event::key_codes::HOME => column = 0,
crate::event::key_codes::END => column = last_column,
crate::event::key_codes::ENTER
if self.edit_triggers.contains(EditTriggers::EDIT_KEY) =>
{
if let Some(cell) = self.current_cell.or(self.selection) {
self.edit_requested.emit(cell);
}
return;
}
_ => return,
}
self.move_current_cell(row, column, shift);
}
Event::MouseMove { pos } => {
if let Some(column) = self.resizing_column {
let delta = pos.x - self.resize_start_x;
let width = (self.resize_start_width as i32 + delta).max(1) as u32;
if self.store_column_width(column, width) {
self.base.request_redraw();
}
return;
}
let hovered = self.cell_at(*pos);
if hovered != self.hovered_cell {
self.hovered_cell = hovered;
self.base.request_redraw();
}
}
Event::MouseRelease { button: 1, .. } => {
if let Some(column) = self.resizing_column.take() {
self.column_resized.emit((column, self.column_width_at(column)));
self.base.request_redraw();
}
}
Event::MouseLeave { .. } => {
let had_hover = self.hovered_cell.take().is_some();
if had_hover {
self.base.request_redraw();
}
}
_ => { }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::types::EventHandler;
use crate::signal::GenericSignal;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Mutex;
struct StaticSource {
rows: usize,
cols: usize,
data: Vec<Vec<String>>,
}
impl IncrementalTableDataSource for StaticSource {
fn row_count(&self) -> usize {
self.rows
}
fn column_count(&self) -> usize {
self.cols
}
fn data(&self, row: usize, column: usize) -> Option<String> {
self.data.get(row).and_then(|line| line.get(column)).cloned()
}
}
fn static_grid(rows: usize, cols: usize) -> DataGrid {
let mut grid = DataGrid::new(Rect::new(0, 0, 600, 300));
grid.set_data_source(Arc::new(StaticSource {
rows,
cols,
data: (0..rows).map(|r| (0..cols).map(|c| format!("{r}:{c}")).collect()).collect(),
}));
grid
}
#[test]
fn header_sections_split_by_width_and_report_clicks() {
let mut header = GridViewHeader::new(Rect::new(0, 0, 300, 28));
header.set_columns(vec![
ColumnSpec::new("Name", 100).with_sortable(true),
ColumnSpec::new("Size", 100).with_filterable(true),
ColumnSpec::new("Owner", 100),
]);
let clicked = Arc::new(Mutex::new(Vec::<usize>::new()));
let sink = clicked.clone();
header.section_clicked.connect(move |index| {
if let Ok(mut guard) = sink.lock() {
guard.push(*index);
}
});
assert_eq!(header.section_rect(1), Some(Rect::new(100, 0, 100, 28)));
let middle = header.section_rect(1).expect("section 1");
header.handle_event(&Event::mouse_press(middle.x + 10, middle.y + 10, 1));
assert_eq!(*clicked.lock().expect("clicked lock"), vec![1]);
}
#[test]
fn header_sort_indicator_cycles_and_reports_the_order() {
let mut header = GridViewHeader::new(Rect::new(0, 0, 200, 28));
header.set_columns(vec![
ColumnSpec::new("Name", 100).with_sortable(true),
ColumnSpec::new("Fixed", 100),
]);
assert!(header.set_sort_indicator(0, Some(SortOrder::Ascending)));
assert_eq!(header.sort_indicator(0), Some(SortOrder::Ascending));
assert_eq!(header.toggle_sort(0), Some(SortOrder::Descending));
assert_eq!(header.toggle_sort(0), None);
assert_eq!(header.sort_indicator(0), None);
assert_eq!(header.toggle_sort(1), None, "an unsortable section never sorts");
assert!(!header.set_sort_indicator(5, None), "there is no section 5");
}
#[test]
fn header_resize_emits_the_new_width() {
let mut header = GridViewHeader::new(Rect::new(0, 0, 300, 28));
header.set_columns(vec![ColumnSpec::new("Name", 100)]);
header.set_resize_mode(ColumnWidthMode::Interactive);
let resized = Arc::new(Mutex::new(Vec::<(usize, u32)>::new()));
let sink = resized.clone();
header.section_resized.connect(move |pair| {
if let Ok(mut guard) = sink.lock() {
guard.push(*pair);
}
});
header.handle_event(&Event::mouse_press(99, 10, 1));
header.handle_event(&Event::mouse_move(139, 10));
header.handle_event(&Event::mouse_release(139, 10, 1));
assert_eq!(header.section_width(0), 140);
assert_eq!(*resized.lock().expect("resized lock"), vec![(0, 140)]);
}
#[test]
fn set_header_adopts_titles_widths_and_arrows() {
let mut grid = static_grid(3, 3);
let mut header = GridViewHeader::new(Rect::new(0, 0, 400, 28));
header.set_columns(vec![
ColumnSpec::new("Alpha", 90).with_sortable(true),
ColumnSpec::new("Beta", 110).with_filterable(true),
ColumnSpec::new("Gamma", 120),
]);
header.set_sort_indicator(1, Some(SortOrder::Descending));
grid.set_header(&header);
assert_eq!(grid.header_columns().len(), 3);
assert_eq!(grid.header_columns()[0].title, "Alpha");
assert_eq!(grid.column_width_at(1), 110);
assert_eq!(grid.sort_specs().len(), 1);
assert_eq!(grid.sort_specs()[0], SortSpec { column: 1, descending: true });
assert_eq!(grid.get("summary_text"), Ok(CapabilityValue::String("3 rows".to_string())));
}
#[test]
fn a_page_click_on_the_bound_bar_reaches_the_grid() {
let mut grid = static_grid(100, 2);
let mut pagination =
crate::widget::nav_widgets::pagination::Pagination::new(Rect::new(0, 0, 300, 32));
pagination.set_page_size(10);
grid.set_pagination(&mut pagination);
assert_eq!(pagination.total(), 100, "the bar learns the row count");
assert_eq!(grid.pagination_state(), Some((0, 10)));
let applied = Arc::new(Mutex::new(Vec::<usize>::new()));
let sink = applied.clone();
grid.page_applied.connect(move |page| {
if let Ok(mut guard) = sink.lock() {
guard.push(*page);
}
});
assert!(pagination.next_page());
assert_eq!(grid.poll_pagination(), Some(1));
assert_eq!(grid.scroll_row(), 10);
assert_eq!(*applied.lock().expect("applied lock"), vec![1]);
assert_eq!(grid.poll_pagination(), None, "an unchanged page is not re-applied");
grid.set_scroll_row(30);
assert!(grid.sync_pagination(&mut pagination));
assert_eq!(pagination.page(), 3);
}
#[test]
fn summary_text_reports_rows_selection_and_filter() {
let mut grid = static_grid(10, 2);
assert_eq!(grid.summary_text(), "10 rows");
grid.set_current_cell(0, 0);
assert!(grid.summary_text().contains("selected"));
grid.set_filters(vec![ColumnFilter { column: 0, query: "1".to_string() }]);
assert!(grid.summary_text().contains("filtered"));
}
#[test]
fn stretch_last_gives_the_remaining_room_to_the_last_column() {
let mut grid = static_grid(3, 3);
grid.set_column_width_mode(ColumnWidthMode::StretchLast);
let width = grid.column_width_at(2);
assert!(width > 0);
assert_eq!(grid.column_width_at(0), grid.default_column_width());
assert_eq!(grid.column_width_at(1), grid.default_column_width());
assert_eq!(
width as i32,
grid.cells_box().width as i32 - 2 * grid.default_column_width() as i32
);
}
#[test]
fn a_cells_box_is_a_function_of_its_own_indices() {
let grid = DataGrid::new(Rect::new(0, 0, 400, 300));
let cells = grid.cells_box();
assert_eq!(grid.cell_rect(0, 0), Rect::new(cells.x, cells.y, 120, 20));
assert_eq!(grid.cell_rect(0, 2).x, cells.x + 240);
assert_eq!(grid.cell_rect(3, 0).y, cells.y + 60);
assert_eq!(grid.cell_rect(3, 0).width, 120);
assert_eq!(grid.cell_rect(3, 0).height, 20);
assert_eq!(grid.cell_rect(1, 1), grid.cell_rect(1, 1));
assert!(grid.cells_box().y > 0, "cells must clear the column-title strip");
assert!(grid.cells_box().height < 300, "the title strip must be removed");
}
#[test]
fn the_cell_a_press_resolves_to_is_the_cell_that_was_painted() {
let mut grid = DataGrid::new(Rect::new(0, 0, 900, 300));
grid.set_data_source(Arc::new(StaticSource {
rows: 4,
cols: 4,
data: (0..4).map(|r| (0..4).map(|c| format!("{}:{}", r, c)).collect()).collect(),
}));
let target = grid.cell_rect(2, 3);
let centre =
Point::new(target.x + target.width as i32 / 2, target.y + target.height as i32 / 2);
assert_eq!(grid.cell_at(centre), Some((2, 3)));
assert_eq!(grid.selection(), None, "merely probing must not select");
grid.handle_event(&Event::MousePress { pos: centre, button: 1, modifiers: 0 });
assert_eq!(grid.selection(), Some((2, 3)));
assert_eq!(grid.cell_at(Point::new(centre.x, 1)), None);
assert_eq!(grid.cell_at(Point::new(-5, -5)), None);
let below = Point::new(centre.x, target.y + 20 * 100);
grid.handle_event(&Event::MousePress { pos: below, button: 1, modifiers: 0 });
assert_eq!(grid.selection(), None, "a press on empty space must clear the selection");
}
#[test]
#[cfg(all(device_profile, feature = "desktop"))]
fn a_selected_cell_is_painted_as_selected() {
let _guard = crate::style::theme_test_guard();
crate::widget::census::install_preset_appearances();
crate::theme::global_theme_manager().set_appearance(crate::theme::AppearanceMode::Light);
let mut grid = DataGrid::new(Rect::new(0, 0, 400, 300));
grid.set_data_source(Arc::new(StaticSource {
rows: 4,
cols: 4,
data: (0..4).map(|r| (0..4).map(|c| format!("{r}:{c}")).collect()).collect(),
}));
let target = grid.cell_rect(1, 1);
let centre =
Point::new(target.x + target.width as i32 / 2, target.y + target.height as i32 / 2);
let before = width_two_strokes(&mut grid);
assert_eq!(before, 0, "an untouched grid draws no selection marker");
grid.handle_event(&Event::MousePress { pos: centre, button: 1, modifiers: 0 });
assert_eq!(grid.selection(), Some((1, 1)));
let after = width_two_strokes(&mut grid);
assert_eq!(
after,
before + 1,
"selecting a cell must paint exactly one selection marker (had {before}, now {after})"
);
}
#[cfg(all(device_profile, feature = "desktop"))]
fn width_two_strokes(grid: &mut DataGrid) -> usize {
let rect = grid.geometry();
let svg = crate::widget::svg::render_widget_to_svg(grid, rect);
svg.match_indices("stroke-width=\"2\"").count()
}
#[test]
fn scrolling_moves_the_point_to_cell_mapping_with_the_cells() {
let mut grid = DataGrid::new(Rect::new(0, 0, 400, 300));
grid.set_data_source(Arc::new(StaticSource {
rows: 4,
cols: 8,
data: (0..4).map(|r| (0..8).map(|c| format!("{}:{}", r, c)).collect()).collect(),
}));
let first = grid.cell_rect(0, 0);
let probe = Point::new(first.x + 5, first.y + 5);
assert_eq!(grid.cell_at(probe), Some((0, 0)));
grid.set_scroll_column(2);
assert_eq!(grid.cell_at(probe), Some((0, 2)));
assert_eq!(grid.cell_rect(0, 2).x, first.x);
assert!(grid.cell_rect(0, 0).x < first.x, "scrolled-away columns stay off-screen");
assert_eq!(grid.cell_at(Point::new(first.x + 120 + 5, probe.y)), Some((0, 3)));
assert_eq!(
grid.cell_at(Point::new(first.x - 1, probe.y)),
None,
"the frame margin is not a cell"
);
}
#[test]
fn frozen_columns_ignore_horizontal_scroll() {
let mut grid = DataGrid::new(Rect::new(0, 0, 900, 300));
grid.set_data_source(Arc::new(StaticSource {
rows: 4,
cols: 8,
data: (0..4).map(|r| (0..8).map(|c| format!("{r}:{c}")).collect()).collect(),
}));
grid.set_frozen_columns(2);
let cells = grid.cells_box();
let first = grid.cell_rect(0, 0);
let second = grid.cell_rect(0, 1);
assert_eq!(first.x, cells.x);
assert_eq!(second.x, cells.x + grid.column_width as i32);
grid.set_scroll_column(5);
assert_eq!(grid.cell_rect(0, 0), first, "the frozen column must not travel the scroll");
assert_eq!(grid.cell_rect(0, 1), second, "nor the next frozen column");
assert_eq!(grid.cell_rect(2, 0).y, cells.y + 2 * grid.row_height as i32);
let frozen_centre =
Point::new(second.x + second.width as i32 / 2, second.y + second.height as i32 / 2);
assert_eq!(grid.cell_at(frozen_centre), Some((0, 1)));
let scrolling = grid.cell_rect(2, 7);
assert_eq!(
scrolling.x,
cells.x + 2 * grid.column_width as i32 + 2 * grid.column_width as i32,
"the frozen prefix occupies its own band before the scrolled columns"
);
let scrolling_centre = Point::new(
scrolling.x + scrolling.width as i32 / 2,
scrolling.y + scrolling.height as i32 / 2,
);
assert_eq!(grid.cell_at(scrolling_centre), Some((2, 7)));
assert_eq!(grid.visible_window().2, 0);
}
#[test]
fn a_click_in_the_frozen_band_selects_the_frozen_column() {
let mut grid = DataGrid::new(Rect::new(0, 0, 400, 300));
grid.set_data_source(Arc::new(StaticSource {
rows: 4,
cols: 8,
data: (0..4).map(|r| (0..8).map(|c| format!("{r}:{c}")).collect()).collect(),
}));
grid.set_frozen_columns(2);
grid.set_scroll_column(5);
let target = grid.cell_rect(3, 1);
let centre =
Point::new(target.x + target.width as i32 / 2, target.y + target.height as i32 / 2);
grid.handle_event(&Event::MousePress { pos: centre, button: 1, modifiers: 0 });
assert_eq!(grid.selection(), Some((3, 1)));
}
#[test]
#[cfg(all(device_profile, feature = "desktop"))]
fn a_frozen_column_is_painted_at_the_leading_edge_after_scrolling() {
let mut grid = DataGrid::new(Rect::new(0, 0, 400, 300));
let mut data: Vec<Vec<String>> = vec![vec![String::new(); 8]; 4];
data[1][0] = "P".to_string();
grid.set_data_source(Arc::new(StaticSource { rows: 4, cols: 8, data }));
grid.set_frozen_columns(2);
grid.set_scroll_column(5);
let rect = grid.geometry();
let svg = crate::widget::svg::render_widget_to_svg(&mut grid, rect);
let boxes = crate::widget::svg::text_ink_boxes(&svg);
assert_eq!(
boxes.len(),
1,
"only the frozen cell is non-empty; a missing frozen window paints nothing"
);
let frozen = grid.cell_rect(1, 0);
let (bx, by, _, _) = boxes[0];
assert!(
bx >= frozen.x && bx < frozen.x + frozen.width as i32,
"the frozen value must land in the leading column ({frozen:?}), got x={bx}"
);
assert!(
by >= frozen.y && by < frozen.y + frozen.height as i32,
"and in its own row ({frozen:?}), got y={by}"
);
}
#[test]
fn a_vertical_scroll_keeps_the_first_visible_row_at_the_scroll_position() {
let mut grid = DataGrid::new(Rect::new(0, 0, 400, 300));
grid.set_data_source(Arc::new(StaticSource {
rows: 30,
cols: 6,
data: (0..30).map(|r| (0..6).map(|c| format!("{r}:{c}")).collect()).collect(),
}));
grid.set_scroll_row(8);
assert_eq!(grid.visible_window().0, 6, "the fetch origin leads the scroll by the overscan");
let first = grid.cell_rect(8, 0);
let centre =
Point::new(first.x + first.width as i32 / 2, first.y + first.height as i32 / 2);
assert_eq!(grid.cell_at(centre), Some((8, 0)));
grid.handle_event(&Event::MousePress { pos: centre, button: 1, modifiers: 0 });
assert_eq!(grid.selection(), Some((8, 0)));
}
#[test]
#[cfg(all(device_profile, feature = "desktop"))]
fn the_visible_rows_are_drawn_blocked_at_the_scroll_position() {
let mut grid = DataGrid::new(Rect::new(0, 0, 400, 300));
let rows = 30usize;
let mut data: Vec<Vec<String>> = vec![vec![String::new(); 6]; rows];
data[8][0] = "W".to_string();
grid.set_data_source(Arc::new(StaticSource { rows, cols: 6, data }));
grid.set_scroll_row(8);
let first = grid.cell_rect(8, 0);
let rect = grid.geometry();
let svg = crate::widget::svg::render_widget_to_svg(&mut grid, rect);
let boxes = crate::widget::svg::text_ink_boxes(&svg);
assert_eq!(
boxes.len(),
1,
"only the scrolled-to cell must be painted as text; a missing row-scroll offset paints \
the blank lead row instead"
);
let (bx, by, _, _) = boxes[0];
assert!(
bx >= first.x && bx < first.x + first.width as i32,
"the value must land in the scrolled-to row's first column ({first:?}), got x={bx}"
);
assert!(
by >= first.y && by < first.y + first.height as i32,
"the value must land in the scrolled-to row's box ({first:?}), got y={by}"
);
}
#[test]
fn visible_window_includes_overscan_and_clamps() {
let mut grid = DataGrid::new(Rect::new(0, 0, 240, 60));
grid.set_data_source(Arc::new(StaticSource {
rows: 10,
cols: 6,
data: (0..10).map(|r| (0..6).map(|c| format!("{}:{}", r, c)).collect()).collect(),
}));
assert_eq!(grid.visible_window(), (0, 7, 0, 4));
grid.set_scroll_row(8);
grid.set_scroll_column(5);
let window = grid.visible_window();
assert_eq!(window.0, 6);
assert_eq!(window.2, 4);
assert!(window.1 <= 4);
assert!(window.3 <= 2);
}
#[test]
fn filter_and_sort_apply_to_window_rows() {
let mut grid = DataGrid::new(Rect::new(0, 0, 240, 80));
grid.set_data_source(Arc::new(StaticSource {
rows: 4,
cols: 2,
data: vec![
vec!["u1".to_string(), "alice".to_string()],
vec!["u2".to_string(), "bob".to_string()],
vec!["u3".to_string(), "bruce".to_string()],
vec!["u4".to_string(), "zoe".to_string()],
],
}));
grid.set_filters(vec![ColumnFilter { column: 1, query: "b".to_string() }]);
grid.set_sort_specs(vec![SortSpec { column: 1, descending: true }]);
let rows = grid.fetch_visible_cells();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0][1], Some("bruce".to_string()));
assert_eq!(rows[1][1], Some("bob".to_string()));
}
#[test]
fn frozen_columns_clamp_to_column_count() {
let mut grid = DataGrid::new(Rect::new(0, 0, 200, 60));
grid.set_data_source(Arc::new(StaticSource {
rows: 2,
cols: 3,
data: vec![
vec!["a".to_string(), "b".to_string(), "c".to_string()],
vec!["d".to_string(), "e".to_string(), "f".to_string()],
],
}));
grid.set_frozen_columns(99);
assert_eq!(grid.frozen_columns(), 3);
grid.clear_data_source();
assert_eq!(grid.frozen_columns(), 0);
}
#[test]
fn new_creates_default_state() {
let mut grid = DataGrid::new(Rect::new(0, 0, 800, 600));
assert!(!grid.has_data_source());
assert_eq!(grid.scroll_row(), 0);
assert_eq!(grid.scroll_column(), 0);
assert_eq!(grid.row_height(), 20);
assert_eq!(grid.default_column_width(), 120);
assert_eq!(grid.frozen_columns(), 0);
assert!(grid.sort_specs().is_empty());
assert!(grid.filters().is_empty());
assert_eq!(grid.row_count(), 0);
assert_eq!(grid.column_count(), 0);
assert!(grid.fetch_visible_cells().is_empty());
}
#[test]
fn has_data_source_before_and_after() {
let mut grid = DataGrid::new(Rect::new(0, 0, 800, 600));
assert!(!grid.has_data_source());
grid.set_data_source(Arc::new(StaticSource {
rows: 3,
cols: 3,
data: vec![
vec!["a".to_string(), "b".to_string(), "c".to_string()],
vec!["d".to_string(), "e".to_string(), "f".to_string()],
vec!["g".to_string(), "h".to_string(), "i".to_string()],
],
}));
assert!(grid.has_data_source());
grid.clear_data_source();
assert!(!grid.has_data_source());
}
#[test]
fn row_and_column_count_queries() {
let mut grid = DataGrid::new(Rect::new(0, 0, 800, 600));
assert_eq!(grid.row_count(), 0);
assert_eq!(grid.column_count(), 0);
grid.set_data_source(Arc::new(StaticSource { rows: 5, cols: 4, data: vec![] }));
assert_eq!(grid.row_count(), 5);
assert_eq!(grid.column_count(), 4);
grid.clear_data_source();
assert_eq!(grid.row_count(), 0);
assert_eq!(grid.column_count(), 0);
}
#[test]
fn scroll_position_clamping() {
let mut grid = DataGrid::new(Rect::new(0, 0, 800, 600));
grid.set_data_source(Arc::new(StaticSource {
rows: 10,
cols: 6,
data: (0..10).map(|r| (0..6).map(|c| format!("{}:{}", r, c)).collect()).collect(),
}));
grid.set_scroll_row(5);
assert_eq!(grid.scroll_row(), 5);
grid.set_scroll_row(999);
assert_eq!(grid.scroll_row(), 9);
grid.set_scroll_column(3);
assert_eq!(grid.scroll_column(), 3);
grid.set_scroll_column(999);
assert_eq!(grid.scroll_column(), 5);
}
#[test]
fn clear_data_source_resets_state() {
let mut grid = DataGrid::new(Rect::new(0, 0, 800, 600));
grid.set_data_source(Arc::new(StaticSource { rows: 5, cols: 3, data: vec![] }));
grid.set_scroll_row(2);
grid.set_scroll_column(1);
grid.set_frozen_columns(2);
grid.set_sort_specs(vec![SortSpec { column: 0, descending: false }]);
grid.clear_data_source();
assert!(!grid.has_data_source());
assert_eq!(grid.scroll_row(), 0);
assert_eq!(grid.scroll_column(), 0);
assert_eq!(grid.frozen_columns(), 0);
assert!(grid.fetch_visible_cells().is_empty());
}
#[test]
fn sort_specs_set_get_invalidate_cache() {
let mut grid = DataGrid::new(Rect::new(0, 0, 800, 600));
grid.set_data_source(Arc::new(StaticSource {
rows: 3,
cols: 2,
data: vec![
vec!["c".to_string(), "x".to_string()],
vec!["a".to_string(), "y".to_string()],
vec!["b".to_string(), "z".to_string()],
],
}));
assert!(grid.sort_specs().is_empty());
let specs = vec![SortSpec { column: 0, descending: false }];
grid.set_sort_specs(specs.clone());
assert_eq!(grid.sort_specs().len(), 1);
assert_eq!(grid.sort_specs()[0].column, 0);
let rows = grid.fetch_visible_cells();
assert_eq!(rows.len(), 3);
assert_eq!(rows[0][0], Some("a".to_string()));
assert_eq!(rows[1][0], Some("b".to_string()));
assert_eq!(rows[2][0], Some("c".to_string()));
grid.set_sort_specs(vec![SortSpec { column: 0, descending: true }]);
let rows2 = grid.fetch_visible_cells();
assert_eq!(rows2[0][0], Some("c".to_string()));
assert_eq!(rows2[2][0], Some("a".to_string()));
grid.set_sort_specs(Vec::new());
assert!(grid.sort_specs().is_empty());
}
#[test]
fn filters_set_get_invalidate_cache() {
let mut grid = DataGrid::new(Rect::new(0, 0, 800, 600));
grid.set_data_source(Arc::new(StaticSource {
rows: 4,
cols: 2,
data: vec![
vec!["apple".to_string(), "red".to_string()],
vec!["banana".to_string(), "yellow".to_string()],
vec!["cherry".to_string(), "red".to_string()],
vec!["date".to_string(), "brown".to_string()],
],
}));
assert!(grid.filters().is_empty());
let filters = vec![ColumnFilter { column: 1, query: "red".to_string() }];
grid.set_filters(filters.clone());
assert_eq!(grid.filters().len(), 1);
let rows = grid.fetch_visible_cells();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0][0], Some("apple".to_string()));
assert_eq!(rows[1][0], Some("cherry".to_string()));
grid.set_filters(vec![ColumnFilter { column: 0, query: "b".to_string() }]);
let rows2 = grid.fetch_visible_cells();
assert_eq!(rows2.len(), 1);
assert_eq!(rows2[0][0], Some("banana".to_string()));
grid.set_filters(Vec::new());
assert!(grid.filters().is_empty());
}
#[test]
fn frozen_columns_set_get() {
let mut grid = DataGrid::new(Rect::new(0, 0, 800, 600));
grid.set_data_source(Arc::new(StaticSource { rows: 2, cols: 5, data: vec![] }));
assert_eq!(grid.frozen_columns(), 0);
grid.set_frozen_columns(3);
assert_eq!(grid.frozen_columns(), 3);
grid.set_frozen_columns(999);
assert_eq!(grid.frozen_columns(), 5);
}
#[test]
fn visible_window_changed_signal_emission() {
let mut grid = DataGrid::new(Rect::new(0, 0, 240, 60));
grid.set_data_source(Arc::new(StaticSource {
rows: 10,
cols: 6,
data: (0..10).map(|r| (0..6).map(|c| format!("{}:{}", r, c)).collect()).collect(),
}));
let emitted = Arc::new(Mutex::new(false));
let sink = emitted.clone();
grid.visible_window_changed.connect(move |_win| {
*sink.lock().unwrap() = true;
});
grid.set_scroll_row(3);
assert!(*emitted.lock().unwrap());
}
#[test]
fn empty_source_returns_empty_window() {
let mut grid = DataGrid::new(Rect::new(0, 0, 800, 600));
grid.set_data_source(Arc::new(StaticSource { rows: 0, cols: 0, data: vec![] }));
assert_eq!(grid.visible_window(), (0, 0, 0, 0));
assert!(grid.fetch_visible_cells().is_empty());
}
struct RevisionSource {
rows: usize,
cols: usize,
rev: AtomicU64,
changed: GenericSignal,
call_count: AtomicUsize,
}
impl RevisionSource {
fn new(rows: usize, cols: usize) -> Self {
Self {
rows,
cols,
rev: AtomicU64::new(1),
changed: GenericSignal::new(),
call_count: AtomicUsize::new(0),
}
}
fn bump(&self) {
self.rev.fetch_add(1, Ordering::Relaxed);
self.changed.emit();
}
fn with_revision(rows: usize, cols: usize, revision: u64) -> Self {
let source = Self::new(rows, cols);
source.rev.store(revision, Ordering::Relaxed);
source
}
fn touch(&self) {
self.changed.emit();
}
}
impl IncrementalTableDataSource for RevisionSource {
fn row_count(&self) -> usize {
self.rows
}
fn column_count(&self) -> usize {
self.cols
}
fn data(&self, row: usize, column: usize) -> Option<String> {
self.call_count.fetch_add(1, Ordering::Relaxed);
Some(format!("{}:{}", row, column))
}
fn revision(&self) -> u64 {
self.rev.load(Ordering::Relaxed)
}
fn data_changed_signal(&self) -> Option<&GenericSignal> {
Some(&self.changed)
}
}
#[test]
fn cache_invalidation_on_revision_change() {
let mut grid = DataGrid::new(Rect::new(0, 0, 240, 60));
let source = Arc::new(RevisionSource::new(10, 4));
grid.set_data_source(source.clone());
let _a = grid.fetch_visible_cells();
let calls_after_a = source.call_count.load(Ordering::Relaxed);
assert!(calls_after_a > 0);
let _b = grid.fetch_visible_cells();
assert_eq!(source.call_count.load(Ordering::Relaxed), calls_after_a);
source.bump();
let _c = grid.fetch_visible_cells();
assert!(source.call_count.load(Ordering::Relaxed) > calls_after_a);
}
#[test]
fn a_revision_less_source_is_cached_and_invalidated_by_its_signal() {
let mut grid = DataGrid::new(Rect::new(0, 0, 240, 60));
let source = Arc::new(RevisionSource::with_revision(10, 4, 0));
grid.set_data_source(source.clone());
let first = grid.fetch_visible_cells_shared();
let calls_after_first = source.call_count.load(Ordering::Relaxed);
assert!(calls_after_first > 0);
let second = grid.fetch_visible_cells_shared();
assert!(Rc::ptr_eq(&first, &second), "the cached window is shared, not re-fetched");
assert_eq!(
source.call_count.load(Ordering::Relaxed),
calls_after_first,
"the signal makes this source observable"
);
source.touch();
let _ = grid.fetch_visible_cells_shared();
assert!(
source.call_count.load(Ordering::Relaxed) > calls_after_first,
"a reported change must invalidate the window"
);
}
#[test]
fn row_height_and_column_width_minimum_clamp() {
let mut grid = DataGrid::new(Rect::new(0, 0, 800, 600));
grid.set_data_source(Arc::new(StaticSource { rows: 3, cols: 3, data: vec![] }));
grid.set_row_height(0);
assert_eq!(grid.row_height(), 1);
grid.set_row_height(25);
assert_eq!(grid.row_height(), 25);
grid.set_default_column_width(0);
assert_eq!(grid.default_column_width(), 1);
grid.set_default_column_width(100);
assert_eq!(grid.default_column_width(), 100);
}
#[test]
fn extreme_dimensions_do_not_overflow_visible_capacity() {
let mut grid = DataGrid::new(Rect::new(0, 0, u32::MAX, u32::MAX));
grid.set_row_height(u32::MAX);
grid.set_default_column_width(u32::MAX);
assert_eq!(grid.visible_row_capacity(), 1);
assert_eq!(grid.visible_column_capacity(), 1);
}
#[test]
fn wheel_scrolls_one_row_per_notch() {
let mut grid = DataGrid::new(Rect::new(0, 0, 400, 300));
grid.set_data_source(Arc::new(StaticSource { rows: 10, cols: 2, data: vec![] }));
grid.handle_event(&Event::wheel(0, -120, 0));
assert_eq!(grid.scroll_row(), 1, "one legacy notch is one row");
grid.handle_event(&Event::wheel(0, -1, 0));
assert_eq!(grid.scroll_row(), 2, "one documented notch is one row");
grid.handle_event(&Event::wheel(0, 120, 0));
assert_eq!(grid.scroll_row(), 1, "the opposite notch scrolls back one row");
}
#[test]
fn full_model_projection_header_sort_and_keyboard_are_wired() {
let data = vec![vec!["b".to_string()], vec!["c".to_string()], vec!["a".to_string()]];
let mut grid = DataGrid::new(Rect::new(0, 0, 300, 120));
grid.set_data_source(Arc::new(StaticSource { rows: 3, cols: 1, data }));
grid.set_full_model_projection(true);
grid.set_sort_specs(vec![SortSpec { column: 0, descending: false }]);
let rows = grid.fetch_visible_cells();
assert_eq!(rows.first().and_then(|row| row.first().cloned()), Some(Some("a".to_string())));
grid.set_column_width_mode(ColumnWidthMode::Stretch);
assert_eq!(grid.column_width_mode(), ColumnWidthMode::Stretch);
grid.set_alternating_rows(true);
assert!(grid.alternating_rows());
grid.set_show_scrollbars(true);
assert!(grid.show_scrollbars());
let header = grid.header_band();
let clicked = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(usize::MAX));
let sink = std::sync::Arc::clone(&clicked);
grid.header_clicked.connect(move |column| {
sink.store(*column, std::sync::atomic::Ordering::SeqCst);
});
grid.handle_event(&Event::MousePress {
pos: Point::new(header.x + 1, header.y + 1),
button: 1,
modifiers: 0,
});
assert_eq!(
clicked.load(std::sync::atomic::Ordering::SeqCst),
0,
"a header click names its column"
);
grid.handle_event(&Event::FocusGained { reason: crate::event::FocusReason::Tab });
grid.handle_event(&Event::KeyPress { key: crate::event::key_codes::DOWN, modifiers: 0 });
assert_eq!(grid.current_cell(), Some((1, 0)), "Down moves the current cell one row");
grid.handle_event(&Event::KeyPress { key: crate::event::key_codes::DOWN, modifiers: 0 });
assert_eq!(grid.current_cell(), Some((2, 0)), "Down reaches the last row");
let _ = grid.fetch_visible_cells();
}
#[test]
fn column_prefix_width_is_cached_until_a_width_changes() {
let mut grid = static_grid(50, 5);
grid.set_column_width_mode(ColumnWidthMode::Interactive);
assert!(grid.set_column_width(1, 80));
let expected = 120 + 80 + 120 + 120;
assert_eq!(grid.column_prefix_width(4), expected);
let builds = grid.column_prefix_rebuilds.get();
for _ in 0..100 {
assert_eq!(grid.column_prefix_width(4), expected);
assert_eq!(grid.column_prefix_width(2), 200);
}
assert_eq!(grid.column_prefix_rebuilds.get(), builds, "the table was reused");
assert!(grid.set_column_width(1, 50));
assert_eq!(grid.column_prefix_width(4), 120 + 50 + 120 + 120);
assert_eq!(grid.column_prefix_rebuilds.get(), builds + 1);
}
#[test]
fn draw_view_shares_the_projection_across_scroll_positions() {
let mut grid = static_grid(200, 4);
let first = grid.fetch_visible_window_view();
assert!(!first.is_empty());
let same_projection = Rc::clone(&first.rows);
grid.set_scroll_row(10);
let second = grid.fetch_visible_window_view();
assert!(
Rc::ptr_eq(&same_projection, &second.rows),
"a scroll reuses the same projected matrix instead of slicing a copy"
);
assert_ne!(first.row_start, second.row_start, "the window did move");
assert_eq!(second.row(2).and_then(|row| row[0].clone()), Some("10:0".to_string()));
}
#[test]
fn non_observable_projection_is_cached_until_refresh_data() {
struct CountingSource {
rows: usize,
columns: usize,
fetches: Arc<AtomicU64>,
}
impl IncrementalTableDataSource for CountingSource {
fn row_count(&self) -> usize {
self.rows
}
fn column_count(&self) -> usize {
self.columns
}
fn data(&self, row: usize, column: usize) -> Option<String> {
Some(format!("{row}:{column}"))
}
fn fetch_window(
&self,
row_start: usize,
row_len: usize,
column_start: usize,
column_len: usize,
) -> Vec<Vec<Option<String>>> {
self.fetches.fetch_add(1, Ordering::Relaxed);
(row_start..row_start.saturating_add(row_len).min(self.rows))
.map(|row| {
(column_start..column_start.saturating_add(column_len).min(self.columns))
.map(|column| Some(format!("{row}:{column}")))
.collect()
})
.collect()
}
}
let fetches = Arc::new(AtomicU64::new(0));
let mut grid = DataGrid::new(Rect::new(0, 0, 400, 160));
grid.set_data_source(Arc::new(CountingSource {
rows: 500,
columns: 3,
fetches: Arc::clone(&fetches),
}));
assert!(!grid.data_is_observable, "the fixture has no invalidation contract");
let _ = grid.fetch_visible_cells();
let after_first = fetches.load(Ordering::Relaxed);
assert!(after_first > 0);
grid.set_scroll_row(5);
let _ = grid.fetch_visible_cells();
grid.set_scroll_row(9);
let _ = grid.fetch_visible_cells();
assert_eq!(
fetches.load(Ordering::Relaxed),
after_first,
"scrolling must hit the cached projection"
);
grid.refresh_data();
let _ = grid.fetch_visible_cells();
assert!(
fetches.load(Ordering::Relaxed) > after_first,
"refresh_data is how a non-observable source reports change"
);
}
#[test]
fn header_labels_are_elided_once_and_cached() {
let mut grid = static_grid(4, 3);
grid.set_header_labels(&["Alpha", "Beta", "Gamma"]);
let _ = crate::widget::svg::render_to_svg(&mut grid);
let cached = grid.header_text_cache.borrow();
assert_eq!(cached.entries.get(&(0, 120)).map(String::as_str), Some("Alpha"));
assert_eq!(cached.entries.get(&(2, 120)).map(String::as_str), Some("Gamma"));
drop(cached);
grid.set_header_labels(&["Renamed", "Beta", "Gamma"]);
let _ = crate::widget::svg::render_to_svg(&mut grid);
let cached = grid.header_text_cache.borrow();
assert_eq!(cached.entries.get(&(0, 120)).map(String::as_str), Some("Renamed"));
}
}