Compare commits

..
Author SHA1 Message Date
dependabot[bot] 6f57b54ab9 chore(deps): bump the cargo-dependencies group across 1 directory with 7 updates
Bumps the cargo-dependencies group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [log](https://github.com/rust-lang/log) | `0.4.34` | `0.4.35` |
| [quick-xml](https://github.com/tafia/quick-xml) | `0.41.0` | `0.42.0` |
| [zip](https://github.com/zip-rs/zip2) | `3.0.0` | `9.1.0` |
| [aes](https://github.com/RustCrypto/block-ciphers) | `0.9.3` | `0.9.4` |
| [ratex-types](https://github.com/erweixin/RaTeX) | `0.1.14` | `0.1.15` |
| [ratex-font](https://github.com/erweixin/RaTeX) | `0.1.14` | `0.1.15` |
| [skrifa](https://github.com/googlefonts/fontations) | `0.44.0` | `0.48.0` |



Updates `log` from 0.4.34 to 0.4.35
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.34...v0.4.35)

Updates `quick-xml` from 0.41.0 to 0.42.0
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.41.0...v0.42.0)

Updates `zip` from 3.0.0 to 9.1.0
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v3.0.0...v9.1.0)

Updates `aes` from 0.9.3 to 0.9.4
- [Commits](https://github.com/RustCrypto/block-ciphers/compare/aes-v0.9.3...aes-v0.9.4)

Updates `ratex-types` from 0.1.14 to 0.1.15
- [Release notes](https://github.com/erweixin/RaTeX/releases)
- [Changelog](https://github.com/erweixin/RaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/erweixin/RaTeX/compare/v0.1.14...v0.1.15)

Updates `ratex-font` from 0.1.14 to 0.1.15
- [Release notes](https://github.com/erweixin/RaTeX/releases)
- [Changelog](https://github.com/erweixin/RaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/erweixin/RaTeX/compare/v0.1.14...v0.1.15)

Updates `skrifa` from 0.44.0 to 0.48.0
- [Release notes](https://github.com/googlefonts/fontations/releases)
- [Changelog](https://github.com/googlefonts/fontations/blob/main/docs/RELEASE.md)
- [Commits](https://github.com/googlefonts/fontations/compare/skrifa-v0.44.0...skrifa-v0.48.0)

---
updated-dependencies:
- dependency-name: aes
  dependency-version: 0.9.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-dependencies
- dependency-name: log
  dependency-version: 0.4.35
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-dependencies
- dependency-name: quick-xml
  dependency-version: 0.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-dependencies
- dependency-name: ratex-font
  dependency-version: 0.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-dependencies
- dependency-name: ratex-types
  dependency-version: 0.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-dependencies
- dependency-name: skrifa
  dependency-version: 0.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-dependencies
- dependency-name: zip
  dependency-version: 9.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: cargo-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-09 04:01:08 +00:00
8 changed files with 88 additions and 355 deletions
Generated
+46 -56
View File
@@ -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
View File
@@ -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
+1 -98
View File
@@ -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");
-8
View File
@@ -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
+10 -57
View File
@@ -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;
+17 -7
View File
@@ -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() {
+9 -41
View File
@@ -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;
}
-83
View File
@@ -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);
}