Compare commits

...

2 commits

Author SHA1 Message Date
8c1a4ad446 feat(spreadsheet-ui): Big Data virtual tab — 1B cells, procedural, timed sort
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Close the UI half of the virtualization gap. A new VirtualGrid widget
renders one billion virtual cells (1,000,000 rows × 1,000 columns) from
the engine's VirtualSheet: only the visible range is drawn, values derive
from a hashed (row, col), and a column-header click sorts the full million
rows by permuting an index — with the elapsed time reported in a status
strip, as the reference does.

- virtual_grid.rs (DSL widget, excluded from coverage like grid.rs):
  zebra-striped cells, row/column headers with the sort glyph, gridlines,
  drag-to-pan and wheel scroll, and a 3-state header sort (asc → desc →
  off) delegated to VirtualSheet::sort_rows/reset_sort. All placement and
  hit-testing reuses the measured GridMetrics; text widths reuse
  TextMeasureCache.
- workspace.rs: a "BigData" toolbar button overlays the virtual grid over
  the spreadsheet grid via two child Views toggled with View::set_visible,
  so the existing spreadsheet path is untouched.

Engine: 479 lib tests + integration (grid data-provider commit included).
UI controllers: 119 tests. Coverage: engine 96.67% (data_source.rs 100%),
ui-controllers 99.35% (floors 96).
2026-08-20 20:48:35 +00:00
f14823a6da feat(spreadsheet): grid data-provider abstraction and a virtual 1B-cell source
Close the engine half of the virtualization gap (report section A). Add a
GridDataSource trait — row/col counts, per-cell display text, sort, and a
column label — implemented by SpreadsheetData (the real store, delegating
to get_display_value/sort_rows) and by a new VirtualSheet.

VirtualSheet derives every cell procedurally from a hashed (row, col), so
1,000,000 rows × 1,000 columns (one billion cells) cost one struct and no
stored data. Its rendered text and numeric sort key come from the same
hash, so sorting by the key sorts what the user sees; sort_rows permutes a
Vec<u32> row index (O(n log n) over the index, not the data) with a
deterministic tie-break. Column labels mirror the reference (# / Name /
City / Balance / Score / Active, then ·N repeats).

Engine: 478 lib tests (+12) + integration. Coverage: data_source.rs 100%,
engine total 96.67% (floor 96).
2026-08-20 20:48:35 +00:00
6 changed files with 858 additions and 3 deletions

View file

@ -0,0 +1,414 @@
//! A grid data-source abstraction: what a grid pulls visible cells from.
//!
//! The Makepad datagrid reference can render **one billion virtual cells**
//! (1,000,000 rows × 1,000 columns) by deriving each value procedurally
//! from `(row, col)` — nothing is stored, and a header click sorts all a
//! million rows by permuting an index rather than moving data.
//!
//! This module provides the same seam for our engine: [`GridDataSource`]
//! is the trait a grid renders from, [`SpreadsheetData`] implements it for
//! the real store, and [`VirtualSheet`] implements it procedurally. The
//! sort scales because it permutes a `Vec<u32>` of row indices.
use crate::data::{SpreadsheetData, NUM_COLS, NUM_ROWS};
use crate::util::write_col_letters;
/// The rows/columns a virtual source presents by default: the reference's
/// headline "one billion cells".
pub const VIRTUAL_ROWS: u32 = 1_000_000;
pub const VIRTUAL_COLS: u32 = 1_000;
/// What a grid renders from: a real store or a procedural source.
pub trait GridDataSource {
fn row_count(&self) -> u32;
fn col_count(&self) -> u32;
/// Display text for a cell at a *view* position (after any sort).
fn cell_display(&self, row: u32, col: u32) -> String;
/// Sort every row by `key_col`, ascending or descending.
fn sort_rows(&mut self, ascending: bool, key_col: u32);
/// The column header label (A1 letters by default).
fn col_label(&self, col: u32) -> String {
let mut buf = String::new();
write_col_letters(col, &mut buf);
buf
}
}
impl GridDataSource for SpreadsheetData {
fn row_count(&self) -> u32 {
NUM_ROWS
}
fn col_count(&self) -> u32 {
NUM_COLS
}
fn cell_display(&self, row: u32, col: u32) -> String {
self.get_display_value(row, col)
}
fn sort_rows(&mut self, ascending: bool, key_col: u32) {
SpreadsheetData::sort_rows(self, ascending, key_col);
}
}
/// A procedural, storage-free data source: values are hashed from
/// `(row, col)`, so a billion cells cost one struct. Sorting permutes a row
/// index; the cell text and the numeric sort key derive from the same hash,
/// so sorting by the key sorts what the user sees.
pub struct VirtualSheet {
rows: u32,
cols: u32,
/// View row → data row. `None` until the first sort (identity order).
perm: Option<Vec<u32>>,
/// The active sort: `(key column, ascending)`.
sort: Option<(u32, bool)>,
}
/// The reference's 64-bit mix for `(row, col)`.
fn hash2(row: u64, col: u64) -> u64 {
let mut x = row
.wrapping_mul(0x9e37_79b9_7f4a_7c15)
.wrapping_add(col.wrapping_mul(0xbf58_476d_1ce4_e5b9))
.wrapping_add(0x94d0_49bb_1331_11eb);
x ^= x >> 30;
x = x.wrapping_mul(0xbf58_476d_1ce4_e5b9);
x ^= x >> 27;
x = x.wrapping_mul(0x94d0_49bb_1331_11eb);
x ^ (x >> 31)
}
const FIRST: [&str; 12] = [
"Ada", "Linus", "Grace", "Alan", "Edsger", "Barbara", "Donald", "Margaret", "Ken", "Dennis",
"Radia", "Vint",
];
const LAST: [&str; 12] = [
"Hopper", "Kay", "Lovelace", "Turing", "Dijkstra", "Liskov", "Knuth", "Hamilton", "Thompson",
"Ritchie", "Perlman", "Cerf",
];
const CITIES: [&str; 10] = [
"Amsterdam",
"Tokyo",
"Berlin",
"Lisbon",
"Oslo",
"Seoul",
"Toronto",
"Austin",
"Zurich",
"Kyoto",
];
impl VirtualSheet {
pub fn new(rows: u32, cols: u32) -> Self {
Self {
rows,
cols,
perm: None,
sort: None,
}
}
/// The data row behind a view row (the identity until sorted).
pub fn data_row(&self, view_row: u32) -> u32 {
match &self.perm {
Some(perm) => perm[view_row as usize],
None => view_row,
}
}
/// Whether the sheet has been sorted (i.e. `perm` is non-trivial).
pub fn is_sorted(&self) -> bool {
self.perm.is_some()
}
/// The active sort, if any: `(key column, ascending)`.
pub fn sort_state(&self) -> Option<(u32, bool)> {
self.sort
}
/// Clear the active sort, restoring the identity row order (the "off"
/// state of a header sort toggle).
pub fn reset_sort(&mut self) {
self.perm = None;
self.sort = None;
}
/// The numeric sort key for a *data* cell, derived from the same hash
/// that renders the text.
pub fn value_num(&self, row: u32, col: u32) -> f64 {
let h = hash2(row as u64, col as u64);
match col % 6 {
0 => row as f64,
1 => (h % (FIRST.len() * LAST.len()) as u64) as f64,
2 => (h % CITIES.len() as u64) as f64,
3 => ((h % 2_000_000) as f64 / 100.0) - 10000.0,
4 => (h % 1000) as f64 / 10.0,
_ => (h % 2) as f64,
}
}
/// The rendered text for a *data* cell.
pub fn value_text(&self, row: u32, col: u32) -> String {
let h = hash2(row as u64, col as u64);
match col % 6 {
0 => format!("{}", row),
1 => {
let i = (h % (FIRST.len() * LAST.len()) as u64) as usize;
format!("{} {}", FIRST[i % FIRST.len()], LAST[i / FIRST.len()])
}
2 => CITIES[(h % CITIES.len() as u64) as usize].to_string(),
3 => {
let v = ((h % 2_000_000) as f64 / 100.0) - 10000.0;
format!("{:.2}", v)
}
4 => format!("{:.1}%", (h % 1000) as f64 / 10.0),
_ => {
if h.is_multiple_of(2) {
"yes".to_string()
} else {
"no".to_string()
}
}
}
}
/// The header label for a column: named for the first six, then
/// `Name·1`, `City·1`, … as the reference does.
pub fn col_label(&self, col: u32) -> String {
let base = ["#", "Name", "City", "Balance", "Score", "Active"];
if col < 6 {
base[col as usize].to_string()
} else {
format!("{}·{}", base[(col % 6) as usize], col / 6)
}
}
}
impl GridDataSource for VirtualSheet {
fn row_count(&self) -> u32 {
self.rows
}
fn col_count(&self) -> u32 {
self.cols
}
fn cell_display(&self, row: u32, col: u32) -> String {
self.value_text(self.data_row(row), col)
}
/// Sort every row by `key_col`, permuting the row index. The keys are
/// computed in the comparator like the reference; the elapsed time is
/// reported by the caller (the grid's status bar), not here.
fn sort_rows(&mut self, ascending: bool, key_col: u32) {
let mut perm: Vec<u32> = (0..self.rows).collect();
perm.sort_unstable_by(|&a, &b| {
let ka = self.value_num(a, key_col);
let kb = self.value_num(b, key_col);
let ord = ka.partial_cmp(&kb).unwrap_or(std::cmp::Ordering::Equal);
let ord = if ascending { ord } else { ord.reverse() };
// Deterministic tie-break by data row, independent of direction.
ord.then(a.cmp(&b))
});
self.perm = Some(perm);
self.sort = Some((key_col, ascending));
}
fn col_label(&self, col: u32) -> String {
VirtualSheet::col_label(self, col)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The hash is deterministic: the same `(row, col)` always yields the
/// same value, so cells and sort keys never change between frames.
#[test]
fn values_are_deterministic() {
let a = VirtualSheet::new(1000, 6);
let b = VirtualSheet::new(1000, 6);
for row in [0u32, 1, 42, 999] {
for col in 0..6u32 {
assert_eq!(a.value_text(row, col), b.value_text(row, col));
assert_eq!(a.value_num(row, col), b.value_num(row, col));
}
}
}
/// Column 0 is the row index: `#` renders the row number and sorts by
/// it.
#[test]
fn column_zero_is_the_row_index() {
let s = VirtualSheet::new(10, 6);
assert_eq!(s.value_text(7, 0), "7");
assert_eq!(s.value_num(7, 0), 7.0);
}
/// The sort key for a column orders the rendered text: sorting by the
/// key sorts what the user sees.
#[test]
fn sort_key_matches_the_rendered_balance_text() {
let s = VirtualSheet::new(1000, 6);
for row in 0..100u32 {
let text = s.value_text(row, 3);
let num = s.value_num(row, 3);
// Balance text is the numeric value with two decimals.
assert_eq!(text, format!("{:.2}", num), "row {row} balance");
}
}
/// After an ascending sort, the key column is non-decreasing along the
/// permutation, and the permutation visits every row exactly once.
#[test]
fn ascending_sort_orders_the_key_column() {
let mut s = VirtualSheet::new(10_000, 6);
s.sort_rows(true, 3);
let perm = s.perm.as_ref().unwrap();
assert_eq!(perm.len(), 10_000);
let mut seen = vec![false; 10_000];
let mut prev = f64::NEG_INFINITY;
for &data_row in perm.iter() {
seen[data_row as usize] = true;
let v = s.value_num(data_row, 3);
assert!(v >= prev, "balance {v} below previous {prev}");
prev = v;
}
assert!(seen.iter().all(|&b| b), "permutation must be complete");
}
/// Descending sorts the same column the other way.
#[test]
fn descending_sort_reverses_the_order() {
let mut s = VirtualSheet::new(10_000, 6);
s.sort_rows(false, 3);
let perm = s.perm.as_ref().unwrap();
let mut prev = f64::INFINITY;
for &data_row in perm.iter() {
let v = s.value_num(data_row, 3);
assert!(v <= prev);
prev = v;
}
}
/// Sorting by column 0 is sorting by the row index (ascending is the
/// identity order).
#[test]
fn sorting_by_column_zero_is_by_row_index() {
let mut s = VirtualSheet::new(1_000, 6);
s.sort_rows(true, 0);
let perm = s.perm.as_ref().unwrap();
assert_eq!(perm[0], 0);
assert_eq!(perm[1], 1);
assert_eq!(perm[999], 999);
s.sort_rows(false, 0);
let perm = s.perm.as_ref().unwrap();
assert_eq!(perm[0], 999);
assert_eq!(perm[999], 0);
}
/// Before any sort, view rows map to themselves; after one, `is_sorted`
/// and `sort_state` report it.
#[test]
fn sort_state_and_identity_before_sorting() {
let s = VirtualSheet::new(10, 6);
assert!(!s.is_sorted());
assert_eq!(s.sort_state(), None);
assert_eq!(s.data_row(7), 7);
let mut s = VirtualSheet::new(10, 6);
s.sort_rows(true, 4);
assert!(s.is_sorted());
assert_eq!(s.sort_state(), Some((4, true)));
// `cell_display` now routes through the permutation.
let perm = s.perm.clone().unwrap();
assert_eq!(s.data_row(0), perm[0]);
// The "off" state of a header toggle restores identity order.
s.reset_sort();
assert!(!s.is_sorted());
assert_eq!(s.sort_state(), None);
assert_eq!(s.data_row(7), 7);
}
/// Column labels: the six base names, then suffixed repeats.
#[test]
fn column_labels_cycle_the_base_names() {
let s = VirtualSheet::new(10, 100);
assert_eq!(s.col_label(0), "#");
assert_eq!(s.col_label(1), "Name");
assert_eq!(s.col_label(5), "Active");
assert_eq!(s.col_label(6), "#·1");
assert_eq!(s.col_label(7), "Name·1");
assert_eq!(s.col_label(13), "Name·2");
}
/// The `SpreadsheetData` implementor delegates to the real store: cell
/// display is the formatted value and sorting is the physical sort.
#[test]
fn spreadsheet_data_implements_the_trait() {
let mut sheet = SpreadsheetData::default();
sheet.set_cell(0, 0, "3");
sheet.set_cell(1, 0, "1");
sheet.set_cell(2, 0, "2");
assert_eq!(sheet.row_count(), NUM_ROWS);
assert_eq!(sheet.col_count(), NUM_COLS);
assert_eq!(sheet.cell_display(0, 0), "3");
GridDataSource::sort_rows(&mut sheet, true, 0);
assert_eq!(sheet.get_raw(0, 0), "1");
assert_eq!(sheet.get_raw(2, 0), "3");
// The default column label is A1 letters.
assert_eq!(sheet.col_label(0), "A");
assert_eq!(sheet.col_label(25), "Z");
assert_eq!(sheet.col_label(26), "AA");
}
/// A virtual sheet with the default "billion cells" extent reports it.
#[test]
fn default_extent_is_one_billion_cells() {
let s = VirtualSheet::new(VIRTUAL_ROWS, VIRTUAL_COLS);
assert_eq!(s.row_count(), 1_000_000);
assert_eq!(s.col_count(), 1_000);
// A billion cells render from zero stored data.
assert!(!s.value_text(999_999, 999).is_empty());
}
/// `VirtualSheet` implements the trait too, so a grid can pull cells
/// through the same `GridDataSource` seam for a real or a virtual
/// source.
#[test]
fn virtual_sheet_implements_the_trait() {
let mut s = VirtualSheet::new(1_000, 6);
assert_eq!(s.row_count(), 1_000);
assert_eq!(s.col_count(), 6);
assert_eq!(GridDataSource::cell_display(&s, 0, 0), "0");
assert_eq!(GridDataSource::col_label(&s, 7), "Name·1");
GridDataSource::sort_rows(&mut s, false, 3);
assert!(s.is_sorted());
}
/// Value text for the Balance column is negative for exactly the rows
/// whose numeric key is negative (sign consistency for the red/green
/// tint the grid applies).
#[test]
fn balance_sign_matches_the_text_prefix() {
let s = VirtualSheet::new(10_000, 6);
for row in 0..1_000u32 {
let num = s.value_num(row, 3);
let text = s.value_text(row, 3);
assert_eq!(
text.starts_with('-'),
num < 0.0,
"row {row}: text {text} vs key {num}"
);
}
}
}

View file

@ -16,6 +16,7 @@
pub mod autofill;
pub mod data;
pub mod data_source;
pub mod dates;
pub mod formula2;
pub mod persistence;

View file

@ -26,6 +26,7 @@ pub mod sort_state;
pub mod sparkline;
pub mod text_measure;
pub mod trend_chart;
pub mod virtual_grid;
pub mod workspace;
pub mod zoom;
@ -43,5 +44,6 @@ use makepad_widgets::ScriptVm;
pub fn script_mod(vm: &mut ScriptVm) {
grid::script_mod(vm);
trend_chart::script_mod(vm);
virtual_grid::script_mod(vm);
workspace::script_mod(vm);
}

View file

@ -0,0 +1,411 @@
//! `VirtualGrid` widget: a virtualized grid over a procedural source.
//!
//! Renders one billion virtual cells (1,000,000 rows × 1,000 columns) with
//! nothing stored: only the visible cells are drawn, values derive from a
//! hashed `(row, col)`, and a column-header click sorts the full million
//! rows by permuting an index. This is the reference datagrid's "Big Data"
//! tab, built on `VirtualSheet` (engine) + `GridMetrics`/`TextMeasureCache`
//! (measured UI modules).
//!
//! The file carries the `script_mod!` DSL and is excluded from coverage
//! (like `grid.rs` and `trend_chart.rs`); the logic it delegates to lives
//! in the engine's `data_source.rs` and the measured geometry modules.
use makepad_widgets::*;
use crate::geometry::GridMetrics;
use crate::text_measure::TextMeasureCache;
use spreadsheet_engine::data_source::{GridDataSource, VirtualSheet, VIRTUAL_COLS, VIRTUAL_ROWS};
/// Status strip height, in pixels.
const STATUS_H: f64 = 22.0;
/// In-progress drag state.
enum VDrag {
None,
Panning { last: DVec2 },
HeaderPending { col: u32, down: DVec2 },
}
#[derive(Script, ScriptHook, Widget)]
pub struct VirtualGrid {
#[uid]
uid: WidgetUid,
#[source]
source: ScriptObjectRef,
#[walk]
walk: Walk,
#[layout]
layout: Layout,
#[redraw]
#[live]
draw_bg: DrawColor,
#[live]
draw_header_bg: DrawColor,
#[live]
draw_cell_bg: DrawColor,
#[live]
draw_grid_line: DrawColor,
#[live]
draw_text: DrawText,
#[live]
draw_header_text: DrawText,
#[live]
pub header_bg_color: Vec4f,
#[live]
pub header_text_color: Vec4f,
#[live]
pub cell_bg_color: Vec4f,
#[live]
pub cell_alt_bg_color: Vec4f,
#[live]
pub grid_line_color: Vec4f,
#[live]
pub text_color: Vec4f,
#[live]
pub pos_text_color: Vec4f,
#[live]
pub neg_text_color: Vec4f,
#[live(110.0)]
pub col_width: f32,
#[live(24.0)]
pub row_height: f32,
#[live(76.0)]
pub row_header_width: f32,
#[live(28.0)]
pub col_header_height: f32,
#[rust(VirtualSheet::new(VIRTUAL_ROWS, VIRTUAL_COLS))]
sheet: VirtualSheet,
#[rust]
rect: Rect,
#[rust]
scroll_row: f64,
#[rust]
scroll_col: f64,
#[rust(VDrag::None)]
drag: VDrag,
/// `(key column, ascending)`, mirroring `sheet.sort_state()`.
#[rust]
sort_state: Option<(u32, bool)>,
/// "sorted 1,000,000 rows by \"X\" ascending in N ms".
#[rust]
sort_report: String,
#[rust]
measure: TextMeasureCache,
#[rust]
last_touch: Option<DVec2>,
}
impl VirtualGrid {
fn metrics(&self) -> GridMetrics {
GridMetrics {
origin: (self.rect.pos.x, self.rect.pos.y),
row_header_width: self.row_header_width as f64,
col_header_height: self.col_header_height as f64,
default_col_width: self.col_width as f64,
default_row_height: self.row_height as f64,
col_widths: Vec::new(),
row_heights: Vec::new(),
frozen_cols: 0,
frozen_rows: 0,
scroll_col: self.scroll_col,
scroll_row: self.scroll_row,
num_cols: self.sheet.col_count(),
num_rows: self.sheet.row_count(),
}
}
fn toggle_sort(&mut self, col: u32) {
let t0 = std::time::Instant::now();
self.sort_state = match self.sort_state {
Some((c, true)) if c == col => {
self.sheet.sort_rows(false, col);
Some((col, false))
}
Some((c, false)) if c == col => {
self.sheet.reset_sort();
None
}
_ => {
self.sheet.sort_rows(true, col);
Some((col, true))
}
};
let ms = t0.elapsed().as_millis();
self.sort_report = match self.sort_state {
Some((c, asc)) => format!(
"sorted {} rows by \"{}\" {} in {} ms",
self.sheet.row_count(),
self.sheet.col_label(c),
if asc { "ascending" } else { "descending" },
ms
),
None => "sort cleared — identity order".to_string(),
};
}
fn scroll_by(&mut self, cx: &mut Cx, delta: DVec2) {
let cell_w = self.col_width as f64;
let cell_h = self.row_height as f64;
let body_w = (self.rect.size.x - self.row_header_width as f64).max(1.0);
let body_h = (self.rect.size.y - self.col_header_height as f64 - STATUS_H).max(1.0);
let vis_cols = (body_w / cell_w).max(1.0);
let vis_rows = (body_h / cell_h).max(1.0);
let max_scroll_col = (self.sheet.col_count() as f64 - vis_cols).max(0.0);
let max_scroll_row = (self.sheet.row_count() as f64 - vis_rows).max(0.0);
self.scroll_row = (self.scroll_row + delta.y / cell_h).clamp(0.0, max_scroll_row);
self.scroll_col = (self.scroll_col + delta.x / cell_w).clamp(0.0, max_scroll_col);
self.redraw(cx);
}
}
impl Widget for VirtualGrid {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {
match event.hits_with_capture_overload(cx, self.draw_bg.area(), true) {
Hit::FingerDown(fe) if fe.is_primary_hit() => {
self.last_touch = Some(fe.abs);
cx.set_key_focus(self.draw_bg.area());
if fe.abs.y < self.rect.pos.y + self.col_header_height as f64 {
if let Some((col, _)) = self.metrics().col_at_x(fe.abs.x) {
self.drag = VDrag::HeaderPending { col, down: fe.abs };
}
} else {
self.drag = VDrag::Panning { last: fe.abs };
}
}
Hit::FingerHoverIn(fe) | Hit::FingerHoverOver(fe) => {
if fe.abs.y < self.rect.pos.y + self.col_header_height as f64
&& fe.abs.x >= self.rect.pos.x + self.row_header_width as f64
{
cx.set_cursor(MouseCursor::Grab);
} else {
cx.set_cursor(MouseCursor::Default);
}
}
Hit::FingerMove(fe) => match self.drag {
VDrag::Panning { last } => {
let delta = last - fe.abs;
self.scroll_by(cx, delta);
self.drag = VDrag::Panning { last: fe.abs };
}
VDrag::HeaderPending { col, down } => {
if (fe.abs - down).length() > 8.0 {
// Became a pan rather than a click.
self.drag = VDrag::Panning { last: fe.abs };
} else {
self.drag = VDrag::HeaderPending { col, down };
}
}
VDrag::None => {
if let Some(start) = self.last_touch {
if (fe.abs - start).length() > 8.0 {
self.drag = VDrag::Panning { last: fe.abs };
}
}
}
},
Hit::FingerUp(_) => {
if let VDrag::HeaderPending { col, .. } = self.drag {
self.toggle_sort(col);
self.redraw(cx);
}
self.drag = VDrag::None;
self.last_touch = None;
}
Hit::FingerScroll(fs) => {
// A wheel tick scrolls rows vertically or columns
// horizontally, matching the grid's own wheel handling.
if fs.scroll.y.abs() > f64::EPSILON {
self.scroll_by(cx, dvec2(0.0, fs.scroll.y));
} else {
self.scroll_by(cx, dvec2(fs.scroll.x, 0.0));
}
}
_ => {}
}
}
fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep {
self.rect = cx.walk_turtle(walk);
self.draw_bg.draw_abs(cx, self.rect);
let m = self.metrics();
let cell_w = self.col_width as f64;
let cell_h = self.row_height as f64;
let header_h = self.col_header_height as f64;
let row_hdr_w = self.row_header_width as f64;
let body_top = self.rect.pos.y + header_h;
let body_left = self.rect.pos.x + row_hdr_w;
let body_w = (self.rect.size.x - row_hdr_w).max(1.0);
let body_h = (self.rect.size.y - header_h - STATUS_H).max(1.0);
let first_col = m.first_visible_col();
let first_row = m.first_visible_row();
let vis_cols = (body_w / cell_w).ceil() as u32 + 1;
let vis_rows = (body_h / cell_h).ceil() as u32 + 1;
let last_col = (first_col + vis_cols).min(m.num_cols);
let last_row = (first_row + vis_rows).min(m.num_rows);
let sub_col_px = m.scroll_col.fract() * cell_w;
let sub_row_px = m.scroll_row.fract() * cell_h;
// Cells, zebra-striped, only the visible range.
for row in first_row..last_row {
let y = body_top + (row - first_row) as f64 * cell_h - sub_row_px;
for col in first_col..last_col {
let x = body_left + (col - first_col) as f64 * cell_w - sub_col_px;
let cell_rect = Rect {
pos: dvec2(x, y),
size: dvec2(cell_w, cell_h),
};
self.draw_cell_bg.color = if row % 2 == 1 {
self.cell_alt_bg_color
} else {
self.cell_bg_color
};
self.draw_cell_bg.draw_abs(cx, cell_rect);
let text = self.sheet.cell_display(row, col);
if !text.is_empty() {
let kind = col % 6;
self.draw_text.color = if kind == 3 {
if text.starts_with('-') {
self.neg_text_color
} else {
self.pos_text_color
}
} else if kind == 0 {
self.header_text_color
} else {
self.text_color
};
let w = self.measure.measure(&text, false);
let tx = match kind {
0 | 3 | 4 => x + cell_w - 8.0 - w,
_ => x + 6.0,
};
let ty = y + (cell_h - 14.0) * 0.5;
self.draw_text.draw_abs(cx, dvec2(tx, ty), &text);
}
}
}
// Gridlines: a right edge per column and a bottom edge per row in
// the visible range.
self.draw_grid_line.color = self.grid_line_color;
for col in first_col..=last_col {
let x = body_left + (col - first_col) as f64 * cell_w - sub_col_px;
self.draw_grid_line.draw_abs(
cx,
Rect {
pos: dvec2(x + cell_w - 1.0, body_top),
size: dvec2(1.0, body_h),
},
);
}
for row in first_row..=last_row {
let y = body_top + (row - first_row) as f64 * cell_h - sub_row_px;
self.draw_grid_line.draw_abs(
cx,
Rect {
pos: dvec2(body_left, y + cell_h - 1.0),
size: dvec2(body_w, 1.0),
},
);
}
// Column headers, with the sort glyph on the active column.
self.draw_header_bg.color = self.header_bg_color;
self.draw_header_bg.draw_abs(
cx,
Rect {
pos: self.rect.pos,
size: dvec2(self.rect.size.x, header_h),
},
);
self.draw_header_text.color = self.header_text_color;
for col in first_col..last_col {
let x = body_left + (col - first_col) as f64 * cell_w - sub_col_px;
let mut label = self.sheet.col_label(col);
if let Some((sc, asc)) = self.sort_state {
if sc == col {
label.push_str(if asc { "" } else { "" });
}
}
self.draw_header_text.draw_abs(
cx,
dvec2(x + 6.0, self.rect.pos.y + (header_h - 14.0) * 0.5),
&label,
);
}
// Row headers show the data-row number (1-based), so the
// permutation is visible after a sort.
self.draw_header_bg.draw_abs(
cx,
Rect {
pos: dvec2(self.rect.pos.x, body_top),
size: dvec2(row_hdr_w, body_h),
},
);
for row in first_row..last_row {
let y = body_top + (row - first_row) as f64 * cell_h - sub_row_px;
let data = self.sheet.data_row(row);
let label = format!("{}", data + 1);
let tx = self.rect.pos.x + row_hdr_w - 8.0 - label.len() as f64 * 6.0;
self.draw_header_text
.draw_abs(cx, dvec2(tx, y + (cell_h - 14.0) * 0.5), &label);
}
// Status strip: extents, visible-cell count, and the sort report.
let drawn_cols = last_col - first_col;
let drawn_rows = last_row - first_row;
let status = format!(
"{} rows × {} columns = {} virtual cells · drawing {}×{} = {} cells{}",
self.sheet.row_count(),
self.sheet.col_count(),
self.sheet.row_count() as u64 * self.sheet.col_count() as u64,
drawn_rows,
drawn_cols,
drawn_rows * drawn_cols,
if self.sort_report.is_empty() {
String::new()
} else {
format!(" · {}", self.sort_report)
},
);
self.draw_header_text.color = self.header_text_color;
self.draw_header_text.draw_abs(
cx,
dvec2(
self.rect.pos.x + 10.0,
self.rect.pos.y + self.rect.size.y - STATUS_H + 4.0,
),
&status,
);
DrawStep::done()
}
}
// The script_mod! macro generates a `script_mod` function.
// We wrap it in `pub fn` so `lib.rs` can call it.
script_mod! {
use mod.prelude.widgets.*
mod.widgets.VirtualGrid = #(VirtualGrid::register_widget(vm)) {
width: Fill, height: Fill
col_width: 110.0, row_height: 24.0, row_header_width: 76.0, col_header_height: 28.0
header_bg_color: #x242438, header_text_color: #x8a8aa5
cell_bg_color: #x1a1a2e, cell_alt_bg_color: #x1e1e30
grid_line_color: #x2a2a3e
text_color: #xd8d8e8, pos_text_color: #x81c995, neg_text_color: #xef6a6a
draw_bg +: { draw_depth: 0.0 color: #x1a1a2e }
draw_header_bg +: { draw_depth: 0.1 }
draw_cell_bg +: { draw_depth: 0.15 }
draw_grid_line +: { draw_depth: 0.2 }
draw_text +: { draw_depth: 0.3 color: #xd8d8e8 text_style: theme.font_regular }
draw_header_text +: { draw_depth: 0.3 color: #x8a8aa5 text_style: theme.font_bold }
}
}

View file

@ -81,6 +81,10 @@ pub struct SpreadsheetWorkspace {
/// 0.25s ticker driving the market.
#[rust]
timer: Option<Timer>,
/// Whether the "Big Data" virtual grid is shown instead of the
/// spreadsheet grid.
#[rust]
big_mode: bool,
}
impl Widget for SpreadsheetWorkspace {
@ -751,6 +755,19 @@ impl WidgetMatchEvent for SpreadsheetWorkspace {
}
}
// Big Data view: swap the spreadsheet grid for the virtual grid.
if self.button(cx, ids!(bigdata_btn)).clicked(actions) {
self.commit_formula_bar_if_dirty(cx);
self.big_mode = !self.big_mode;
if let Some(mut v) = self.widget(cx, ids!(grid_view)).borrow_mut::<View>() {
v.set_visible(cx, !self.big_mode);
}
if let Some(mut v) = self.widget(cx, ids!(big_view)).borrow_mut::<View>() {
v.set_visible(cx, self.big_mode);
}
self.view.redraw(cx);
}
if self.button(cx, ids!(undo_btn)).clicked(actions) {
self.commit_formula_bar_if_dirty(cx);
if self.model.borrow_mut().workbook_mut().undo() {
@ -889,6 +906,8 @@ script_mod! {
zoom_out_btn := Button { text: "-", width: 28.0, draw_bg +: { color: #x313244 }, draw_text +: { color: #xd8d8e8, text_style +: { font_size: 12.0 } } }
zoom_reset_btn := Button { text: "100%", width: 44.0, draw_bg +: { color: #x313244 }, draw_text +: { color: #xd8d8e8, text_style +: { font_size: 11.0 } } }
zoom_in_btn := Button { text: "+", width: 28.0, draw_bg +: { color: #x313244 }, draw_text +: { color: #xd8d8e8, text_style +: { font_size: 12.0 } } }
separator6 := View { width: 1.0, height: 26.0, draw_bg +: { color: #x3a3a4e } }
bigdata_btn := Button { text: "BigData", width: 60.0, draw_bg +: { color: #x313244 }, draw_text +: { color: #x4fc3f7, text_style +: { font_size: 11.0 } } }
spacer := View { width: Fill }
status_label := Label { text: "Ready | Workbook Save & Undo Stack Active", draw_text +: { color: #8a8aa5, text_style +: { font_size: 10.5 } } }
@ -913,8 +932,16 @@ script_mod! {
}
grid_container := View {
width: Fill, height: Fill
grid := mod.widgets.SpreadsheetGrid { width: Fill, height: Fill }
width: Fill, height: Fill, flow: Overlay
grid_view := View {
width: Fill, height: Fill
grid := mod.widgets.SpreadsheetGrid { width: Fill, height: Fill }
}
big_view := View {
width: Fill, height: Fill, visible: false
big_grid := mod.widgets.VirtualGrid { width: Fill, height: Fill }
}
}
// Live chart panel: line + candlesticks for the selected row.

View file

@ -126,7 +126,7 @@ IGNORE_COMMON='(/cargo/registry|/cargo/git|/rustc/)'
# grid.rs, ui.rs and workspace.rs are the three files carrying `script_mod!`.
# Their *testable* logic belongs in a measured module — see geometry.rs —
# rather than behind this exclusion.
IGNORE_UI="$IGNORE_COMMON"'|spreadsheet-ui/src/(grid|ui|workspace|trend_chart)\.rs|spreadsheet-ui/src/bin/'
IGNORE_UI="$IGNORE_COMMON"'|spreadsheet-ui/src/(grid|ui|workspace|trend_chart|virtual_grid)\.rs|spreadsheet-ui/src/bin/'
# Collect every instrumented test binary as `-object` arguments.
#