mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-09 03:49:54 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f57b54ab9 |
Generated
+46
-56
@@ -118,9 +118,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32"
|
||||
checksum = "fc0fc36d7440d015b45468ba08a21e80e056ebbd014c611c6298bb7503e04bd1"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
"cpubits",
|
||||
@@ -195,15 +195,6 @@ version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.7.0"
|
||||
@@ -963,7 +954,7 @@ dependencies = [
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
"skrifa",
|
||||
"skrifa 0.44.0",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
@@ -992,7 +983,7 @@ dependencies = [
|
||||
"verifysign",
|
||||
"winreg",
|
||||
"winsafe",
|
||||
"zip 9.1.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1150,17 +1141,6 @@ version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dify"
|
||||
version = "0.8.0"
|
||||
@@ -1504,7 +1484,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"profiling",
|
||||
"self_cell",
|
||||
"skrifa",
|
||||
"skrifa 0.44.0",
|
||||
"smallvec",
|
||||
"unicode-bidi",
|
||||
"unicode-general-category",
|
||||
@@ -1710,9 +1690,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "font-types"
|
||||
version = "0.12.6"
|
||||
version = "0.12.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c92d9081c7ab3bd83ef1eecdfad090f3d966e3ce74106779664114cfe1a6274"
|
||||
checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
@@ -1923,7 +1903,7 @@ dependencies = [
|
||||
"hashbrown 0.17.1",
|
||||
"log",
|
||||
"peniko",
|
||||
"skrifa",
|
||||
"skrifa 0.44.0",
|
||||
"smallvec",
|
||||
"vello_common",
|
||||
]
|
||||
@@ -2077,9 +2057,9 @@ dependencies = [
|
||||
"gridcraft-engine",
|
||||
"gridcraft-mcp",
|
||||
"log",
|
||||
"quick-xml 0.41.0",
|
||||
"quick-xml 0.42.0",
|
||||
"serde_json",
|
||||
"zip 3.0.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2113,7 +2093,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
"web-time",
|
||||
"zip 3.0.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2194,7 +2174,7 @@ dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"skrifa",
|
||||
"skrifa 0.48.0",
|
||||
"sys-locale",
|
||||
"toml",
|
||||
"web-sys",
|
||||
@@ -2231,11 +2211,11 @@ dependencies = [
|
||||
"gridcraft-functions",
|
||||
"gridcraft-math",
|
||||
"gridcraft-model",
|
||||
"quick-xml 0.41.0",
|
||||
"quick-xml 0.42.0",
|
||||
"sha1 0.11.0",
|
||||
"sha2 0.11.1",
|
||||
"thiserror 2.0.21",
|
||||
"zip 3.0.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2286,7 +2266,7 @@ checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"bytemuck",
|
||||
"read-fonts",
|
||||
"read-fonts 0.41.0",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
@@ -2956,9 +2936,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
version = "0.4.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
@@ -4092,9 +4072,9 @@ checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
|
||||
|
||||
[[package]]
|
||||
name = "ratex-font"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a7b73531dabe934a3e725ed5c90d18dacf43778b7a208381390943a9a8c24db"
|
||||
checksum = "e4373503a1068c9f7d07862583bbd897ac4d2bdf89d42ac8c65870c33135bf6f"
|
||||
dependencies = [
|
||||
"phf 0.11.3",
|
||||
"ratex-types",
|
||||
@@ -4143,9 +4123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ratex-types"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cf0be3d050525f0f7d37af55a443d7648c222ccc617639950e3de1f8e6f9900"
|
||||
checksum = "ae81208192f9898cbd2424aa42fd4472bd79c44a52092673b101cdca72c89b7c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -4199,6 +4179,19 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.45.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4edc8db0d972de88e53a085b1f01fb3fbdae7bb0cb7a0ce2b2f8285a412fe3ef"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"foldhash 0.2.0",
|
||||
"font-types",
|
||||
"once_cell",
|
||||
"yoke",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
@@ -4938,7 +4931,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"read-fonts",
|
||||
"read-fonts 0.41.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "skrifa"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5213b4825bac3587046330052d61bfac7bd3bd0d203c758d7c2ef726fcea034"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"read-fonts 0.45.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6907,20 +6910,6 @@ dependencies = [
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap",
|
||||
"memchr",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.1.0"
|
||||
@@ -6935,6 +6924,7 @@ dependencies = [
|
||||
"nix",
|
||||
"typed-path",
|
||||
"windows-sys 0.61.2",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+5
-5
@@ -40,8 +40,8 @@ chrono = { version = "0.4.45", default-features = false, features = ["std", "clo
|
||||
web-time = "1.1.0"
|
||||
rayon = "1"
|
||||
regex = { version = "1", default-features = false, features = ["std", "unicode", "perf"] }
|
||||
quick-xml = "0.41"
|
||||
zip = { version = "3", default-features = false, features = ["deflate"] }
|
||||
quick-xml = "0.42"
|
||||
zip = { version = "9", default-features = false, features = ["deflate"] }
|
||||
sha2 = { version = "0.11", default-features = false }
|
||||
# Decrypting password-protected workbooks ([MS-OFFCRYPTO]): AES and SHA-1, pure Rust (wasm-safe).
|
||||
aes = { version = "0.9", default-features = false }
|
||||
@@ -52,8 +52,8 @@ image = { version = "0.25", default-features = false, features = ["png", "jpeg"]
|
||||
# young (0.1.x), and the gate recorded in docs/decisions.md vetted this release.
|
||||
ratex-parser = "=0.1.14"
|
||||
ratex-layout = "=0.1.14"
|
||||
ratex-types = "=0.1.14"
|
||||
ratex-font = "=0.1.14"
|
||||
ratex-types = "=0.1.15"
|
||||
ratex-font = "=0.1.15"
|
||||
|
||||
# ui
|
||||
egui = "0.37"
|
||||
@@ -66,7 +66,7 @@ arboard = { version = "3.6", default-features = false }
|
||||
sys-locale = "0.3"
|
||||
# Font parser egui's epaint uses; the UI validates system font files with it before handing them
|
||||
# to egui, which panics on a file it can't parse. Same version as epaint's, so no second copy.
|
||||
skrifa = { version = "0.44", default-features = false, features = ["std"] }
|
||||
skrifa = { version = "0.48", default-features = false, features = ["std"] }
|
||||
# Real font bytes for tests (Ubuntu Light, compiled into epaint).
|
||||
epaint_default_fonts = "0.37"
|
||||
# One native window per workbook: the window list, Window menu titles and the immediate-viewport
|
||||
|
||||
@@ -566,9 +566,7 @@ impl LineIndex {
|
||||
let mut prev = 0u32;
|
||||
for (&line, info) in custom {
|
||||
pos += (line - prev) as f64 * default;
|
||||
// A negative size (only a malformed file has one) counts as zero, so positions only
|
||||
// grow and the searches below stay valid.
|
||||
let size = if info.hidden { 0.0 } else { info.size.map_or(default, |s| s as f64).max(0.0) };
|
||||
let size = if info.hidden { 0.0 } else { info.size.map_or(default, |s| s as f64) };
|
||||
lines.push((line, pos, size));
|
||||
pos += size;
|
||||
prev = line + 1;
|
||||
@@ -586,44 +584,6 @@ impl LineIndex {
|
||||
},
|
||||
}
|
||||
}
|
||||
/// The first line at or after `line` (and before the axis end) with a size above zero, or
|
||||
/// `None` when every line from there on is hidden or zero-sized. O(log n): a run of hidden
|
||||
/// lines all start where the next shown line starts, so one search steps over the whole run.
|
||||
pub fn next_visible(&self, line: u32) -> Option<u32> {
|
||||
if line >= self.max {
|
||||
return None;
|
||||
}
|
||||
let i = self.lines.partition_point(|e| e.0 < line);
|
||||
let custom = self.lines.get(i).is_some_and(|e| e.0 == line);
|
||||
if !custom && self.default > 0.0 {
|
||||
return Some(line);
|
||||
}
|
||||
// Every line from `line` up to the answer is zero-sized, so they all start at `p`; the
|
||||
// last custom line starting at `p` either has a size (the answer) or is the end of the run.
|
||||
let p = self.start(line);
|
||||
let k = self.lines.partition_point(|e| e.1 <= p);
|
||||
let &(last, _, size) = self.lines.get(k.checked_sub(1)?)?;
|
||||
if last < line {
|
||||
return None;
|
||||
}
|
||||
if size > 0.0 {
|
||||
return Some(last).filter(|l| *l < self.max);
|
||||
}
|
||||
// The line after the run has no custom line (a custom one would start at `p` too and be
|
||||
// part of the run): it is shown when the default size is.
|
||||
let next = last.checked_add(1).filter(|n| *n < self.max)?;
|
||||
(self.default > 0.0).then_some(next)
|
||||
}
|
||||
/// The lines from `first` to `last` (inclusive) with a size above zero, in order. Runs of
|
||||
/// hidden lines cost one O(log n) step each, not one per line.
|
||||
pub fn visible(&self, first: u32, last: u32) -> impl Iterator<Item = u32> + '_ {
|
||||
let mut next = self.next_visible(first);
|
||||
std::iter::from_fn(move || {
|
||||
let line = next.filter(|l| *l <= last)?;
|
||||
next = line.checked_add(1).and_then(|n| self.next_visible(n));
|
||||
Some(line)
|
||||
})
|
||||
}
|
||||
/// The line containing `pos`.
|
||||
pub fn at(&self, pos: f64) -> u32 {
|
||||
if pos <= 0.0 {
|
||||
@@ -684,63 +644,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::cell::Formula;
|
||||
|
||||
/// `LineIndex::visible` lists exactly the lines a line-by-line size check shows, for hidden
|
||||
/// runs at the start, in the middle and up to the end of the axis, custom sizes (zero and
|
||||
/// negative included), lines with only a style, and a zero default size.
|
||||
#[test]
|
||||
fn visible_lines_match_a_line_by_line_check() {
|
||||
let max = 400u32;
|
||||
let mut seed = 0x2545_f491_4f6c_dd1du64;
|
||||
let mut rnd = |n: u64| {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 7;
|
||||
seed ^= seed << 17;
|
||||
seed % n
|
||||
};
|
||||
for case in 0..300 {
|
||||
let mut custom = BTreeMap::new();
|
||||
let runs = rnd(6);
|
||||
for _ in 0..runs {
|
||||
let start = rnd(u64::from(max)) as u32;
|
||||
let len = rnd(if case % 3 == 0 { 400 } else { 30 }) as u32;
|
||||
for l in start..(start + len).min(max) {
|
||||
custom.insert(l, LineInfo { hidden: true, ..Default::default() });
|
||||
}
|
||||
}
|
||||
// Runs that start at the first line or end at the last one.
|
||||
if case % 5 == 0 {
|
||||
for l in 0..rnd(20) as u32 {
|
||||
custom.insert(l, LineInfo { hidden: true, ..Default::default() });
|
||||
}
|
||||
}
|
||||
if case % 5 == 1 {
|
||||
for l in max - rnd(20) as u32 - 1..max {
|
||||
custom.insert(l, LineInfo { hidden: true, ..Default::default() });
|
||||
}
|
||||
}
|
||||
for _ in 0..rnd(8) {
|
||||
let size = [Some(0.0), Some(3.0), Some(20.0), Some(-5.0), None][rnd(5) as usize];
|
||||
custom.insert(rnd(u64::from(max)) as u32, LineInfo { size, ..Default::default() });
|
||||
}
|
||||
let default = if case % 7 == 0 { 0.0 } else { 15.0 };
|
||||
let size = |l: u32| match custom.get(&l) {
|
||||
Some(i) if i.hidden => 0.0,
|
||||
Some(LineInfo { size: Some(s), .. }) => s.max(0.0),
|
||||
_ => default,
|
||||
};
|
||||
let index = LineIndex::new(&custom, default, max);
|
||||
for (first, last) in [(0, max - 1), (rnd(u64::from(max)) as u32, max - 1), (5, 5), (max - 1, max - 1)] {
|
||||
let want: Vec<u32> = (first..=last).filter(|&l| size(l) > 0.0).collect();
|
||||
assert_eq!(index.visible(first, last).collect::<Vec<_>>(), want, "case {case}, {first}..={last}");
|
||||
}
|
||||
assert_eq!(index.next_visible(max), None);
|
||||
}
|
||||
// The row after a million hidden rows comes straight after the last shown one.
|
||||
let custom: BTreeMap<u32, LineInfo> = (47..1_048_422).map(|r| (r, LineInfo { hidden: true, ..Default::default() })).collect();
|
||||
let index = LineIndex::new(&custom, 15.0, 1_048_576);
|
||||
assert_eq!(index.visible(40, 1_048_424).collect::<Vec<_>>(), [40, 41, 42, 43, 44, 45, 46, 1_048_422, 1_048_423, 1_048_424]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sheet_keeps_its_identity_through_clones_and_a_new_sheet_gets_its_own() {
|
||||
let a = Sheet::new("A");
|
||||
|
||||
@@ -235,14 +235,6 @@ impl Trace {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GridState {
|
||||
/// The active sheet's line positions and outlines, kept with a weak reference to the sheet
|
||||
/// they were built from: while it still upgrades to the very sheet shown, the sheet hasn't
|
||||
/// changed (every change goes through `Arc::make_mut`, which moves or copies a sheet that
|
||||
/// has a weak reference to it), so they still hold.
|
||||
pub(crate) lines: Option<(std::sync::Weak<Sheet>, geo::SheetLines)>,
|
||||
/// The conditional formatting statistics (minimum, maximum, percentiles, duplicates) of the
|
||||
/// active sheet's rules, kept the same way: they are read from that sheet's cells alone.
|
||||
pub(crate) cf: Option<(std::sync::Weak<Sheet>, CfCache)>,
|
||||
pub drag: Drag,
|
||||
pub ensure_visible: bool,
|
||||
/// With `ensure_visible`: scroll to the moving end of the selection instead of the active cell
|
||||
|
||||
@@ -24,60 +24,28 @@ pub struct Geo {
|
||||
/// vertical centre line on screen: painted shapes by `mirror_painted`, widgets by
|
||||
/// `mirror_rect`/`mirror_pos`, and pointer input back by `mirror_pos` (an involution).
|
||||
pub rtl: bool,
|
||||
/// Prefix sums over custom row heights / column widths, from [`SheetLines`].
|
||||
/// Prefix sums over custom row heights / column widths, built once per frame.
|
||||
pub(super) ri: std::sync::Arc<LineIndex>,
|
||||
pub(super) ci: std::sync::Arc<LineIndex>,
|
||||
/// The sheet's used range, from [`SheetLines`].
|
||||
pub(super) used: Option<RangeRef>,
|
||||
/// Page Layout view: cells sit on pages (`scroll` is then in page-layout units).
|
||||
pub paper: Option<Arc<gridcraft_engine::paper::PageGrid>>,
|
||||
}
|
||||
|
||||
/// The parts of the grid's geometry that depend on the sheet alone: where each row and column
|
||||
/// starts, and the row and column outlines. Building them walks every row and column that has a
|
||||
/// size, a style or an outline level (a million rows in a file whose filter hides most of them),
|
||||
/// so the grid keeps them while the sheet stays the same (`show::sheet_lines`).
|
||||
#[derive(Clone)]
|
||||
pub struct SheetLines {
|
||||
pub(super) ri: Arc<LineIndex>,
|
||||
pub(super) ci: Arc<LineIndex>,
|
||||
pub(super) row_outline: Arc<crate::outline::Axis>,
|
||||
pub(super) col_outline: Arc<crate::outline::Axis>,
|
||||
/// The sheet's used range (it walks every stored row): how far the scrollbars reach.
|
||||
pub(super) used: Option<RangeRef>,
|
||||
}
|
||||
|
||||
impl SheetLines {
|
||||
pub fn of(sh: &Sheet) -> SheetLines {
|
||||
SheetLines {
|
||||
ri: Arc::new(LineIndex::new(&sh.rows, sh.default_row_height, MAX_ROWS)),
|
||||
ci: Arc::new(LineIndex::new(&sh.cols, sh.default_col_width, MAX_COLS)),
|
||||
row_outline: Arc::new(crate::outline::Axis::of(sh, true)),
|
||||
col_outline: Arc::new(crate::outline::Axis::of(sh, false)),
|
||||
used: sh.used_range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Geo {
|
||||
pub fn new(sh: &Sheet, rect: Rect, scroll: egui::Vec2) -> Geo {
|
||||
Geo::with_lines(sh, rect, scroll, &SheetLines::of(sh))
|
||||
}
|
||||
/// [`Geo::new`] with the sheet's `lines` already built (`SheetLines::of(sh)`).
|
||||
pub fn with_lines(sh: &Sheet, rect: Rect, scroll: egui::Vec2, lines: &SheetLines) -> Geo {
|
||||
let zoom = (sh.zoom as f32 / 100.0).clamp(0.1, 4.0);
|
||||
let z = zoom * DISPLAY_SCALE;
|
||||
let header_h = if sh.show_headings { (COL_HEADER_H * zoom.max(0.75)).round() } else { 0.0 };
|
||||
let ri = lines.ri.clone();
|
||||
let ci = lines.ci.clone();
|
||||
let ri = std::sync::Arc::new(LineIndex::new(&sh.rows, sh.default_row_height, MAX_ROWS));
|
||||
let ci = std::sync::Arc::new(LineIndex::new(&sh.cols, sh.default_col_width, MAX_COLS));
|
||||
let last_row = ri.at((scroll.y + rect.height() / z) as f64 + 200.0) + 1;
|
||||
let digits = (last_row as f32).log10().floor() as i32 + 1;
|
||||
let header_w = if sh.show_headings { ((digits.max(2) as f32 * 7.0 + 14.0) * zoom.max(0.75)).round() } else { 0.0 };
|
||||
let (fr, fc) = sh.freeze.unwrap_or((0, 0));
|
||||
let frozen_w = (ci.start(fc) as f32) * z;
|
||||
let frozen_h = (ri.start(fr) as f32) * z;
|
||||
let row_outline = lines.row_outline.clone();
|
||||
let col_outline = lines.col_outline.clone();
|
||||
let row_outline = Arc::new(crate::outline::Axis::of(sh, true));
|
||||
let col_outline = Arc::new(crate::outline::Axis::of(sh, false));
|
||||
// Show Outline Symbols off: no bands (the outline's hidden lines stay hidden).
|
||||
let symbols = sh.show_outline_symbols;
|
||||
let mut band_w = if symbols { crate::outline::band(row_outline.levels, zoom) } else { 0.0 };
|
||||
@@ -108,7 +76,6 @@ impl Geo {
|
||||
rtl: sh.right_to_left,
|
||||
ri,
|
||||
ci,
|
||||
used: lines.used,
|
||||
paper: None,
|
||||
}
|
||||
}
|
||||
@@ -123,11 +90,7 @@ impl Geo {
|
||||
/// Page Layout view geometry: `rect` excludes the rulers; cells are drawn at the print scale
|
||||
/// on pages laid out by `grid`; frozen panes don't apply (as in Excel).
|
||||
pub fn paged(sh: &Sheet, rect: Rect, scroll: egui::Vec2, grid: Arc<gridcraft_engine::paper::PageGrid>) -> Geo {
|
||||
Geo::paged_with_lines(sh, rect, scroll, grid, &SheetLines::of(sh))
|
||||
}
|
||||
/// [`Geo::paged`] with the sheet's `lines` already built.
|
||||
pub fn paged_with_lines(sh: &Sheet, rect: Rect, scroll: egui::Vec2, grid: Arc<gridcraft_engine::paper::PageGrid>, lines: &SheetLines) -> Geo {
|
||||
let mut g = Geo::with_lines(sh, rect, scroll, lines);
|
||||
let mut g = Geo::new(sh, rect, scroll);
|
||||
let zoom = (sh.zoom as f32 / 100.0).clamp(0.1, 4.0);
|
||||
g.z = zoom * DISPLAY_SCALE * grid.scale.clamp(0.1, 4.0);
|
||||
(g.fr, g.fc, g.frozen_w, g.frozen_h) = (0, 0, 0.0, 0.0);
|
||||
@@ -148,17 +111,17 @@ impl Geo {
|
||||
}
|
||||
}
|
||||
/// Scroll offset (in `scroll` units) at which `row` reaches the top of the scrolled area.
|
||||
pub fn scroll_top_of(&self, _sh: &Sheet, row: u32) -> f32 {
|
||||
pub fn scroll_top_of(&self, sh: &Sheet, row: u32) -> f32 {
|
||||
match &self.paper {
|
||||
Some(pg) => pg.row_y(row) as f32,
|
||||
None => (self.ri.start(row) - self.ri.start(self.fr)) as f32,
|
||||
None => (sh.row_top(row) - sh.row_top(self.fr)) as f32,
|
||||
}
|
||||
}
|
||||
/// Scroll offset (in `scroll` units) at which `col` reaches the left of the scrolled area.
|
||||
pub fn scroll_left_of(&self, _sh: &Sheet, col: u32) -> f32 {
|
||||
pub fn scroll_left_of(&self, sh: &Sheet, col: u32) -> f32 {
|
||||
match &self.paper {
|
||||
Some(pg) => pg.col_x(col) as f32,
|
||||
None => (self.ci.start(col) - self.ci.start(self.fc)) as f32,
|
||||
None => (sh.col_left(col) - sh.col_left(self.fc)) as f32,
|
||||
}
|
||||
}
|
||||
/// Screen x for a logical x (and back): identity unless the sheet is right-to-left.
|
||||
@@ -241,16 +204,6 @@ impl Geo {
|
||||
pub fn cell_at(&self, sh: &Sheet, p: Pos2) -> CellRef {
|
||||
CellRef::new(self.row_at(sh, p.y), self.col_at(sh, p.x))
|
||||
}
|
||||
/// The rows from `r0` to `r1` (inclusive) with a height above zero, in order. A run of
|
||||
/// hidden rows costs one O(log n) step, so a filter that hides a million rows doesn't make
|
||||
/// every frame walk them.
|
||||
pub(super) fn shown_rows(&self, r0: u32, r1: u32) -> impl Iterator<Item = u32> + '_ {
|
||||
self.ri.visible(r0, r1)
|
||||
}
|
||||
/// The columns from `c0` to `c1` (inclusive) with a width above zero (see `shown_rows`).
|
||||
pub(super) fn shown_cols(&self, c0: u32, c1: u32) -> impl Iterator<Item = u32> + '_ {
|
||||
self.ci.visible(c0, c1)
|
||||
}
|
||||
/// Visible scrolled rows (from the first visible row after the frozen ones).
|
||||
pub(super) fn scroll_rows(&self, _sh: &Sheet) -> (u32, u32) {
|
||||
let top = self.ri.start(self.fr) + self.scroll.y as f64;
|
||||
|
||||
@@ -150,12 +150,12 @@ fn paint_gridlines(q: &QuadrantPaint<'_>) {
|
||||
let bottom = geo.y_end(sh, r1);
|
||||
let left = geo.x(sh, c0);
|
||||
let right = geo.x_end(sh, c1);
|
||||
let xs = geo.shown_cols(c0, c1).map(|c| geo.x(sh, c)).chain([right]);
|
||||
let xs = (c0..=c1).filter(|&c| sh.col_width(c) > 0.0).map(|c| geo.x(sh, c)).chain([right]);
|
||||
for x in xs {
|
||||
let x = x.round() - 0.5;
|
||||
p.line_segment([pos2(x, top), pos2(x, bottom)], stroke);
|
||||
}
|
||||
let ys = geo.shown_rows(r0, r1).map(|r| geo.y(sh, r)).chain([bottom]);
|
||||
let ys = (r0..=r1).filter(|&r| sh.row_height(r) > 0.0).map(|r| geo.y(sh, r)).chain([bottom]);
|
||||
for y in ys {
|
||||
let y = y.round() - 0.5;
|
||||
p.line_segment([pos2(left, y), pos2(right, y)], stroke);
|
||||
@@ -164,14 +164,21 @@ fn paint_gridlines(q: &QuadrantPaint<'_>) {
|
||||
}
|
||||
|
||||
fn visible_cells(q: &QuadrantPaint<'_>) -> Vec<CellRef> {
|
||||
let geo = q.geo;
|
||||
let sh = q.sheet;
|
||||
let (r0, r1) = q.rows;
|
||||
let (c0, c1) = q.cols;
|
||||
// Merged areas: blank them (no inner gridlines) and paint as one cell.
|
||||
let cols: Vec<u32> = geo.shown_cols(c0, c1).collect();
|
||||
let mut cells: Vec<CellRef> = Vec::new();
|
||||
for r in geo.shown_rows(r0, r1) {
|
||||
cells.extend(cols.iter().map(|&c| CellRef::new(r, c)));
|
||||
for r in r0..=r1 {
|
||||
if sh.row_height(r) == 0.0 {
|
||||
continue;
|
||||
}
|
||||
for c in c0..=c1 {
|
||||
if sh.col_width(c) == 0.0 {
|
||||
continue;
|
||||
}
|
||||
cells.push(CellRef::new(r, c));
|
||||
}
|
||||
}
|
||||
cells
|
||||
}
|
||||
@@ -218,7 +225,10 @@ fn outside_text_cells(q: &mut QuadrantPaint<'_>, merges: &[RangeRef]) -> Vec<(Ce
|
||||
// Text cells just outside the quadrant whose text may overflow into it: the nearest non-empty
|
||||
// cell left of `c0` and right of `c1` in each row (within the same frozen / scrolled pane).
|
||||
let mut outside: Vec<(CellRef, Rect, Look, Option<CfLook>)> = Vec::new();
|
||||
for r in geo.shown_rows(r0, r1) {
|
||||
for r in r0..=r1 {
|
||||
if sh.row_height(r) == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let left = (c0.saturating_sub(20)..c0).rev().map(|col| CellRef::new(r, col)).find(|&n| !empty_ok(n));
|
||||
let right = (c1.saturating_add(1)..=c1.saturating_add(20).min(MAX_COLS - 1)).map(|col| CellRef::new(r, col)).find(|&n| !empty_ok(n));
|
||||
for n in [left, right].into_iter().flatten() {
|
||||
|
||||
@@ -15,33 +15,6 @@ use super::{
|
||||
#[cfg(test)]
|
||||
use super::{CellRef, Pos2};
|
||||
|
||||
/// The line positions and outlines of sheet `si`: the ones built for it before while it is the
|
||||
/// same sheet, else built now and kept (see `GridState::lines`).
|
||||
fn sheet_lines(app: &mut SheetApp, wb: &Workbook, si: usize) -> Option<super::geo::SheetLines> {
|
||||
let sheet = wb.sheets.get(si)?;
|
||||
if let Some((built_from, lines)) = &app.grid.lines
|
||||
&& is_sheet(built_from, wb, si)
|
||||
{
|
||||
return Some(lines.clone());
|
||||
}
|
||||
let lines = super::geo::SheetLines::of(sheet);
|
||||
app.grid.lines = Some((Arc::downgrade(sheet), lines.clone()));
|
||||
Some(lines)
|
||||
}
|
||||
|
||||
/// Whether `built_from` still is sheet `si` of `wb`, unchanged (see `GridState::lines`).
|
||||
fn is_sheet(built_from: &std::sync::Weak<Sheet>, wb: &Workbook, si: usize) -> bool {
|
||||
wb.sheets.get(si).is_some_and(|s| built_from.upgrade().is_some_and(|b| Arc::ptr_eq(&b, s)))
|
||||
}
|
||||
|
||||
/// The conditional formatting cache kept for sheet `si` while it is unchanged, else a new one.
|
||||
fn kept_cf(app: &mut SheetApp, wb: &Workbook, si: usize) -> CfCache {
|
||||
match app.grid.cf.take() {
|
||||
Some((built_from, cf)) if is_sheet(&built_from, wb, si) => cf,
|
||||
_ => CfCache::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut SheetApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let available = ui.available_rect_before_wrap();
|
||||
@@ -71,16 +44,15 @@ pub fn show(app: &mut SheetApp, ui: &mut egui::Ui) {
|
||||
} else {
|
||||
Rect::from_min_max(pos2((rect.left() + ruler).min(rect.right()), (rect.top() + ruler).min(rect.bottom())), rect.max)
|
||||
};
|
||||
let Some(lines) = sheet_lines(app, &wb, si) else { return };
|
||||
let make_geo = |app: &mut SheetApp, scroll: egui::Vec2| -> Geo {
|
||||
if paper {
|
||||
let probe = Geo::with_lines(sh, grid_rect, scroll, &lines);
|
||||
let probe = Geo::new(sh, grid_rect, scroll);
|
||||
if let Some(g) = crate::paper_view::grid_for(app, &wb, si, sh, probe.cells.size(), scroll) {
|
||||
let g = crate::paper_view::with_dragged_margin(app, &wb, si, g);
|
||||
return Geo::paged_with_lines(sh, grid_rect, scroll, g, &lines);
|
||||
return Geo::paged(sh, grid_rect, scroll, g);
|
||||
}
|
||||
}
|
||||
Geo::with_lines(sh, rect, scroll, &lines)
|
||||
Geo::new(sh, rect, scroll)
|
||||
};
|
||||
// Scrolling.
|
||||
let (scroll_delta, zoom_delta, modifiers, now) = ui.input(|i| (i.smooth_scroll_delta, i.zoom_delta(), i.modifiers, i.time));
|
||||
@@ -143,7 +115,7 @@ pub fn show(app: &mut SheetApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
update_zoom_glide(app, ui, place, sh.zoom, now);
|
||||
view.scroll = view.scroll.max(egui::Vec2::ZERO);
|
||||
let (mut max_x, mut max_y) = (lines.ci.start(MAX_COLS - 20) as f32, lines.ri.start(MAX_ROWS - 40) as f32);
|
||||
let (mut max_x, mut max_y) = (sh.col_left(MAX_COLS - 20) as f32, sh.row_top(MAX_ROWS - 40) as f32);
|
||||
if paper {
|
||||
// Page layout units run past sheet units (margins and gaps); the end is known once the
|
||||
// pages reach the last row / column.
|
||||
@@ -174,10 +146,7 @@ pub fn show(app: &mut SheetApp, ui: &mut egui::Ui) {
|
||||
painter.rect_filled(rect, 0.0, t.grid_bg);
|
||||
// Everything up to the headers is painted in logical (left-to-right) coordinates.
|
||||
let mirror_from = painter.add(egui::Shape::Noop);
|
||||
// A rule over a whole column scans it for its statistics: done once per change of the sheet,
|
||||
// not every frame.
|
||||
let cf_sheet = wb.sheets.get(si).map(Arc::downgrade);
|
||||
let mut cf = kept_cf(app, &wb, si);
|
||||
let mut cf = CfCache::new();
|
||||
let quads = if geo.paper.is_some() {
|
||||
crate::paper_view::paint_pages(
|
||||
crate::paper_view::PagePaintInput {
|
||||
@@ -199,7 +168,6 @@ pub fn show(app: &mut SheetApp, ui: &mut egui::Ui) {
|
||||
&mut cf,
|
||||
)
|
||||
};
|
||||
app.grid.cf = cf_sheet.map(|s| (s, cf));
|
||||
// Objects above cells. A hidden object can't stay selected: Delete or the Format pane would act
|
||||
// on something the user can't see.
|
||||
if crate::chartview::objects_hidden(&wb) {
|
||||
@@ -480,7 +448,7 @@ fn vertical_scrollbar(app: &mut SheetApp, ui: &mut egui::Ui, sh: &Sheet, geo: &G
|
||||
}
|
||||
let visible = track.height() / geo.z;
|
||||
let scroll = app.view().scroll.y;
|
||||
let last = geo.used.map(|r| r.end.row.saturating_add(10)).unwrap_or(100).clamp(100, MAX_ROWS - 1);
|
||||
let last = sh.used_range().map(|r| r.end.row.saturating_add(10)).unwrap_or(100).clamp(100, MAX_ROWS - 1);
|
||||
let content = geo.scroll_top_of(sh, last);
|
||||
let extent = (content - visible).max(scroll).max(0.0).min(limit);
|
||||
let id = ui.scope_id().with("vscroll");
|
||||
@@ -533,7 +501,7 @@ pub(crate) fn horizontal_scrollbar(app: &mut SheetApp, ui: &mut egui::Ui, track:
|
||||
let Some(sh) = wb.active() else { return };
|
||||
let visible = ((geo.cells.width() - geo.frozen_w) / geo.z).max(0.0);
|
||||
let scroll = app.view().scroll.x;
|
||||
let last = geo.used.map(|r| r.end.col.saturating_add(10)).unwrap_or(26).clamp(26, MAX_COLS - 1);
|
||||
let last = sh.used_range().map(|r| r.end.col.saturating_add(10)).unwrap_or(26).clamp(26, MAX_COLS - 1);
|
||||
let content = geo.scroll_left_of(sh, last);
|
||||
let extent = (content - visible).max(scroll).max(0.0);
|
||||
let id = ui.scope_id().with("hscroll");
|
||||
@@ -606,7 +574,7 @@ pub(crate) fn paint_headers(
|
||||
for (rows, cols, clip) in quads {
|
||||
let pc = p.with_clip_rect(Rect::from_min_max(pos2(clip.left(), top.top()), pos2(clip.right(), top.bottom())));
|
||||
if cols.0 <= cols.1 {
|
||||
for c in geo.shown_cols(cols.0, cols.1) {
|
||||
for c in cols.0..=cols.1 {
|
||||
if !done_cols.insert((c, clip.left() as i32)) {
|
||||
continue;
|
||||
}
|
||||
@@ -647,7 +615,7 @@ pub(crate) fn paint_headers(
|
||||
}
|
||||
let pr = p.with_clip_rect(Rect::from_min_max(pos2(left.left(), clip.top()), pos2(left.right(), clip.bottom())));
|
||||
if rows.0 <= rows.1 {
|
||||
for rr in geo.shown_rows(rows.0, rows.1) {
|
||||
for rr in rows.0..=rows.1 {
|
||||
if !done_rows.insert((rr, clip.top() as i32)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -350,86 +350,3 @@ fn upward_text_with_middle_align_is_centred_in_a_merged_block() {
|
||||
assert!((bb.center().y - rect.center().y).abs() < 0.01, "{bb:?} not centred in {rect:?}");
|
||||
assert!((bb.center().x - rect.center().x).abs() < 0.01, "{bb:?} not centred in {rect:?}");
|
||||
}
|
||||
|
||||
/// One grid frame of `app` in a headless context.
|
||||
fn grid_frame(app: &mut crate::SheetApp) {
|
||||
let ctx = egui::Context::default();
|
||||
let screen = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(1200.0, 800.0));
|
||||
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| super::show(app, ui));
|
||||
out.textures_delta.clear(); // nothing consumes the uploads in a headless test
|
||||
}
|
||||
|
||||
/// The line positions, used range and conditional formatting statistics the grid keeps between
|
||||
/// frames are rebuilt after every change of the sheet, whichever way it is changed, and match
|
||||
/// what a fresh build gives.
|
||||
#[test]
|
||||
fn kept_sheet_geometry_and_cf_statistics_follow_every_change() {
|
||||
use gridcraft_engine::cf::CfCache;
|
||||
use gridcraft_engine::core::CellRef;
|
||||
let mut session = gridcraft_engine::Session::new();
|
||||
session.new_workbook();
|
||||
let mut app = crate::SheetApp::new(session, Default::default());
|
||||
app.run("range.setValues", json!({"range": "A1:A3", "values": [[1], [2], [3]]})).unwrap();
|
||||
app.run("home.conditionalFormat", json!({"range": "A:A", "rule": {"type": "colorScale", "colors": ["#FFFFFF", "#FF0000"]}})).unwrap();
|
||||
let kept = |app: &crate::SheetApp| app.grid.lines.as_ref().map(|(_, l)| (Arc::clone(&l.ri), l.used)).unwrap();
|
||||
// The kept statistics give the look a fresh cache gives.
|
||||
let check_cf = |app: &mut crate::SheetApp| {
|
||||
let wb = app.session.active().unwrap().wb.clone();
|
||||
for row in 0..4 {
|
||||
let c = CellRef::new(row, 0);
|
||||
let kept = app.grid.cf.as_mut().map(|(_, cf)| cf.look(&wb, 0, c));
|
||||
assert_eq!(kept, Some(CfCache::new().look(&wb, 0, c)), "A{}", row + 1);
|
||||
}
|
||||
};
|
||||
grid_frame(&mut app);
|
||||
let (ri, used) = kept(&app);
|
||||
assert_eq!(used.map(|r| r.a1()).as_deref(), Some("A1:A3"));
|
||||
check_cf(&mut app);
|
||||
grid_frame(&mut app);
|
||||
assert!(Arc::ptr_eq(&kept(&app).0, &ri), "an unchanged sheet keeps its line positions");
|
||||
|
||||
// A command: the new value moves the colour scale's maximum and the used range.
|
||||
app.run("cell.set", json!({"cell": "A5", "input": "30"})).unwrap();
|
||||
grid_frame(&mut app);
|
||||
assert_eq!(kept(&app).1.map(|r| r.a1()).as_deref(), Some("A1:A5"));
|
||||
check_cf(&mut app);
|
||||
// Hiding a row moves the rows under it.
|
||||
app.run("selection.set", json!({"range": "A2"})).unwrap();
|
||||
app.run("home.hideRows", json!({})).unwrap();
|
||||
grid_frame(&mut app);
|
||||
let geo = app.grid.geo.clone().unwrap();
|
||||
let sh = app.session.active().unwrap().wb.sheet(0).unwrap().clone();
|
||||
assert_eq!(geo.y(&sh, 2), geo.y(&sh, 1), "row 2 is hidden");
|
||||
assert!(!Arc::ptr_eq(&kept(&app).0, &ri));
|
||||
// Undo brings the row back.
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
grid_frame(&mut app);
|
||||
let geo = app.grid.geo.clone().unwrap();
|
||||
let sh = app.session.active().unwrap().wb.sheet(0).unwrap().clone();
|
||||
assert!(geo.y(&sh, 2) > geo.y(&sh, 1), "row 2 is shown again");
|
||||
let fresh = super::geo::SheetLines::of(&sh);
|
||||
for row in [0, 1, 2, 3, 100, 1_048_575] {
|
||||
assert_eq!(kept(&app).0.start(row), fresh.ri.start(row), "row {row}");
|
||||
}
|
||||
assert_eq!(kept(&app).1, fresh.used);
|
||||
|
||||
// A change with nothing else holding the sheet (no undo step, no saved copy): the grid's weak
|
||||
// reference makes `Arc::make_mut` move the sheet to a new allocation, and the grid notices.
|
||||
let ri = kept(&app).0;
|
||||
{
|
||||
let d = app.session.active_mut().unwrap();
|
||||
d.undo.clear();
|
||||
d.redo.clear();
|
||||
d.saved = Arc::new(gridcraft_engine::model::Workbook::default());
|
||||
let sheet = Arc::make_mut(&mut d.wb).sheet_mut(0).unwrap();
|
||||
sheet.rows.entry(0).or_default().hidden = true;
|
||||
sheet.rows.entry(1).or_default().hidden = true;
|
||||
sheet.set_value(CellRef::new(2, 0), gridcraft_engine::core::Value::Number(300.0));
|
||||
}
|
||||
grid_frame(&mut app);
|
||||
let geo = app.grid.geo.clone().unwrap();
|
||||
let sh = app.session.active().unwrap().wb.sheet(0).unwrap().clone();
|
||||
assert_eq!(geo.y(&sh, 2), geo.y(&sh, 0), "rows 1 and 2 are hidden");
|
||||
assert!(!Arc::ptr_eq(&kept(&app).0, &ri));
|
||||
check_cf(&mut app);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user