Compare commits

..

No commits in common. "8c1a4ad44601706bcfcdb4a070b48abf0cdbc99a" and "f27ace8b7fe084285d13eec9969032ddb03b7cee" have entirely different histories.

6 changed files with 3 additions and 858 deletions

View file

@ -1,414 +0,0 @@
//! 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,7 +16,6 @@
pub mod autofill;
pub mod data;
pub mod data_source;
pub mod dates;
pub mod formula2;
pub mod persistence;

View file

@ -26,7 +26,6 @@ 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;
@ -44,6 +43,5 @@ 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

@ -1,411 +0,0 @@
//! `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,10 +81,6 @@ 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 {
@ -755,19 +751,6 @@ 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() {
@ -906,8 +889,6 @@ 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 } } }
@ -932,16 +913,8 @@ script_mod! {
}
grid_container := View {
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 }
}
width: Fill, height: Fill
grid := mod.widgets.SpreadsheetGrid { 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|virtual_grid)\.rs|spreadsheet-ui/src/bin/'
IGNORE_UI="$IGNORE_COMMON"'|spreadsheet-ui/src/(grid|ui|workspace|trend_chart)\.rs|spreadsheet-ui/src/bin/'
# Collect every instrumented test binary as `-object` arguments.
#