mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-10 20:40:05 -06:00
Merge origin/main (6f52cc50, the squash of upstream pull request 471) into w/form-controls
Both sides added a kind of floating object (equation, form control); both are kept everywhere. ROADMAP.md: header combined (form controls first, then equations), catalog 281 / 296 as `cargo xtask parity` measures, the form-controls progress-log and revision rows above main's equation rows. crates/engine/src/cmd.rs: both spec lists (controls and equation). crates/engine/src/cmd/extra.rs: the arrange params docs list both kinds. crates/engine/src/cmd/extra/objects.rs: main's `found` match and `anchors_of` on ObjKind gain the Control arms; the unlocked check takes the parsed kind directly. crates/engine/src/cmd/insert.rs: the object params docs and the kind error name both kinds. crates/engine/src/tests.rs: both test modules. crates/engine/src/tests/objects.rs: every_kind returns six objects (equation at L18, slider at P18) and the anchor lists carry both. crates/model/src/features.rs: ObjKind has Equation and Control, ALL lists six kinds, name() both. crates/model/src/sheet.rs: both imports, both accessor arms, the order test inserts equations (6, 8) and controls (13, 15) and documents controls after slicers with equations counted as shapes. crates/model/src/workbook.rs: main's refresh_equations and this branch's free-id doc comment. crates/ui-egui/src/chartview.rs: both doc comments combined. crates/ui-egui/src/objects.rs: both OpenAction variants and arms. crates/ui-egui/src/ribbon/insert.rs: the import list is the union. docs/control-protocol.md: the kind list names equation and control; main's equation sentences and this branch's form-controls sentence. docs/file-format-parity.md: header combined; the xlsx row carries both additions; the form-controls revision row above main's equation rows. docs/parity-checklist.md: regenerated with `cargo xtask parity` (281 / 296). docs/target-app-parity.md: header combined; catalog 281 / 296; the Insert objects row carries both additions; the form-controls revision row (277 / 292 → 281 / 296) above main's. docs/ui-parity.md: header combined with catalog 281 / 296; main's move-and-resize row with equations plus the form-controls row; the form-controls revision row above main's.
This commit is contained in:
@@ -631,6 +631,9 @@ pub enum ShapeKind {
|
||||
/// A freehand ink stroke; `Shape::text` holds the points as `x,y;x,y;…` relative to the
|
||||
/// anchor (sheet points), `Shape::line` the ink colour and `fill` unused.
|
||||
Ink,
|
||||
/// An equation: `Shape::text` holds its LaTeX source, `Shape::line` its ink colour
|
||||
/// (`Color::Auto` for the sheet's text colour) and `Shape::math` its cached layout.
|
||||
Equation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -644,6 +647,97 @@ pub struct Shape {
|
||||
/// Outline visibility and text formatting (DrawingML `a:ln`, `a:bodyPr`, `a:pPr`, `a:rPr`).
|
||||
#[serde(default)]
|
||||
pub format: ShapeFormat,
|
||||
/// An equation's layout, rebuilt from `text` and never saved.
|
||||
#[serde(skip)]
|
||||
pub math: EquationCache,
|
||||
}
|
||||
|
||||
/// The laid-out form of an equation's LaTeX source. It is derived data: rebuilt by
|
||||
/// [`EquationCache::refresh`] whenever the source changes, never saved, and ignored when shapes
|
||||
/// are compared. Painters read it and never lay equations out themselves, so nothing in a paint
|
||||
/// path can reach the layout engine.
|
||||
///
|
||||
/// An entry answers only for the source it was built from, compared in full: a hash match alone
|
||||
/// is not enough, so two sources whose hashes collide never share a layout. A source over
|
||||
/// [`gridcraft_math::MAX_SOURCE_BYTES`] never lays out, so [`EquationCache::get`] answers for it
|
||||
/// by its length alone, with [`gridcraft_math::SOURCE_TOO_LONG`], and an entry built from one holds
|
||||
/// neither the source nor its hash: a painter asking every frame about a source of megabytes read
|
||||
/// from a file does no work on it.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct EquationCache {
|
||||
/// A hash of the source the entry was built from (a quick first check).
|
||||
key: u64,
|
||||
/// The source's length in bytes.
|
||||
len: usize,
|
||||
/// The source itself, when it is within the length cap.
|
||||
source: Option<std::sync::Arc<str>>,
|
||||
layout: Option<Result<std::sync::Arc<gridcraft_math::MathList>, gridcraft_math::MathError>>,
|
||||
}
|
||||
|
||||
impl PartialEq for EquationCache {
|
||||
fn eq(&self, _: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn source_key(src: &str) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
#[cfg(test)]
|
||||
equation_cache_tests::HASHES.with(|h| h.set(h.get() + 1));
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
src.hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
impl EquationCache {
|
||||
/// Whether this entry was built from `src`. Over the cap the length is the match (an entry for
|
||||
/// such a source is always the same error); within it, the hash and then the source itself.
|
||||
fn holds(&self, src: &str) -> bool {
|
||||
if self.layout.is_none() || self.len != src.len() {
|
||||
return false;
|
||||
}
|
||||
if src.len() > gridcraft_math::MAX_SOURCE_BYTES {
|
||||
return self.source.is_none();
|
||||
}
|
||||
self.key == source_key(src) && self.source.as_deref() == Some(src)
|
||||
}
|
||||
|
||||
/// Makes this the entry for `src`, with `layout`. A source over the cap is neither copied nor
|
||||
/// hashed.
|
||||
fn store(&mut self, src: &str, layout: Result<std::sync::Arc<gridcraft_math::MathList>, gridcraft_math::MathError>) {
|
||||
let within = src.len() <= gridcraft_math::MAX_SOURCE_BYTES;
|
||||
self.key = if within { source_key(src) } else { 0 };
|
||||
self.len = src.len();
|
||||
self.source = within.then(|| std::sync::Arc::from(src));
|
||||
self.layout = Some(layout);
|
||||
}
|
||||
|
||||
/// The layout of `src`, or why it has none; `None` until [`EquationCache::refresh`] has run
|
||||
/// for this source. A source over [`gridcraft_math::MAX_SOURCE_BYTES`] is answered at once,
|
||||
/// whatever the entry holds, by its length alone.
|
||||
pub fn get(&self, src: &str) -> Option<Result<&gridcraft_math::MathList, &gridcraft_math::MathError>> {
|
||||
if src.len() > gridcraft_math::MAX_SOURCE_BYTES {
|
||||
return Some(Err(&*gridcraft_math::SOURCE_TOO_LONG));
|
||||
}
|
||||
if !self.holds(src) {
|
||||
return None;
|
||||
}
|
||||
self.layout.as_ref().map(|r| r.as_ref().map(|l| l.as_ref()))
|
||||
}
|
||||
|
||||
/// Lays `src` out again unless the cache already holds its layout. True when it did.
|
||||
pub fn refresh(&mut self, src: &str) -> bool {
|
||||
if self.holds(src) {
|
||||
return false;
|
||||
}
|
||||
self.store(src, gridcraft_math::layout(src).map(std::sync::Arc::new));
|
||||
true
|
||||
}
|
||||
|
||||
/// Records that `src` was not laid out, and why, without running the layout engine.
|
||||
pub fn refuse(&mut self, src: &str, message: &str) {
|
||||
self.store(src, Err(gridcraft_math::MathError { span: None, message: message.to_string() }));
|
||||
}
|
||||
}
|
||||
|
||||
/// What a form control on the sheet is.
|
||||
@@ -813,6 +907,8 @@ impl FormControl {
|
||||
|
||||
/// The kinds of floating object a sheet holds. Commands name an object by kind and
|
||||
/// workbook-unique id (`{kind: "chart", id}`); [`ObjKind::name`] is that parameter's spelling.
|
||||
/// Equations are stored as shapes ([`ShapeKind::Equation`]) but select, open and delete as their
|
||||
/// own kind.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ObjKind {
|
||||
@@ -820,11 +916,12 @@ pub enum ObjKind {
|
||||
Image,
|
||||
Shape,
|
||||
Slicer,
|
||||
Equation,
|
||||
Control,
|
||||
}
|
||||
|
||||
impl ObjKind {
|
||||
pub const ALL: [ObjKind; 5] = [ObjKind::Chart, ObjKind::Image, ObjKind::Shape, ObjKind::Slicer, ObjKind::Control];
|
||||
pub const ALL: [ObjKind; 6] = [ObjKind::Chart, ObjKind::Image, ObjKind::Shape, ObjKind::Slicer, ObjKind::Equation, ObjKind::Control];
|
||||
|
||||
/// The name used in command parameters.
|
||||
pub fn name(self) -> &'static str {
|
||||
@@ -833,6 +930,7 @@ impl ObjKind {
|
||||
ObjKind::Image => "image",
|
||||
ObjKind::Shape => "shape",
|
||||
ObjKind::Slicer => "slicer",
|
||||
ObjKind::Equation => "equation",
|
||||
ObjKind::Control => "control",
|
||||
}
|
||||
}
|
||||
@@ -989,3 +1087,75 @@ mod trendline_forecast_tests {
|
||||
assert_eq!(Trendline::clamp_forecast(1e300), Trendline::MAX_FORECAST);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod equation_cache_tests {
|
||||
use super::{EquationCache, source_key};
|
||||
|
||||
thread_local! {
|
||||
/// How many sources `source_key` has hashed on this thread.
|
||||
pub(super) static HASHES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
fn glyphs(c: &EquationCache, src: &str) -> Option<String> {
|
||||
let list = c.get(src)?.ok()?;
|
||||
Some(list.items.iter().filter_map(|i| if let gridcraft_math::Item::Glyph(g) = i { Some(g.ch) } else { None }).collect())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hash_collision_never_returns_another_sources_layout() {
|
||||
let (a, b) = ("x^2", "y^3");
|
||||
let mut c = EquationCache::default();
|
||||
assert!(c.refresh(a));
|
||||
assert_eq!(glyphs(&c, a).as_deref(), Some("x2"));
|
||||
// Forge a collision: the entry claims `b`'s hash; `b` has `a`'s length, so only the
|
||||
// full comparison tells them apart.
|
||||
c.key = source_key(b);
|
||||
assert!(c.get(b).is_none(), "`b` is a miss, not `a`'s layout");
|
||||
assert!(c.refresh(b), "`b` is laid out afresh");
|
||||
assert_eq!(glyphs(&c, b).as_deref(), Some("y3"));
|
||||
assert!(c.get(a).is_none(), "the entry is `b`'s now");
|
||||
// A refusal does not answer for a colliding source either.
|
||||
c.refuse(a, "not laid out");
|
||||
c.key = source_key(b);
|
||||
assert!(c.get(b).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sources_over_the_cap_are_not_copied_hashed_or_laid_out() {
|
||||
let long = "x".repeat(gridcraft_math::MAX_SOURCE_BYTES + 1);
|
||||
let mut c = EquationCache::default();
|
||||
HASHES.with(|h| h.set(0));
|
||||
assert!(matches!(c.get(&long), Some(Err(e)) if e.message.contains("4096")), "answered before any refresh");
|
||||
assert!(c.refresh(&long));
|
||||
assert!(c.source.is_none(), "an over-cap source is not kept");
|
||||
assert_eq!(c.key, 0, "nor hashed");
|
||||
assert!(matches!(c.get(&long), Some(Err(_))));
|
||||
assert!(!c.refresh(&long), "still cached");
|
||||
let other = "y".repeat(gridcraft_math::MAX_SOURCE_BYTES + 1);
|
||||
assert!(matches!(c.get(&other), Some(Err(_))), "another over-cap source gets the same answer");
|
||||
assert_eq!(HASHES.with(|h| h.get()), 0, "nothing over the cap is hashed");
|
||||
// Within the cap the lookup is unchanged: a hash, then the source compared in full.
|
||||
assert!(c.refresh("x"));
|
||||
assert!(c.get("x").is_some_and(|r| r.is_ok()));
|
||||
assert!(HASHES.with(|h| h.get()) >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::disallowed_methods)] // tests run only off wasm, where the clock exists
|
||||
fn a_source_of_a_megabyte_is_looked_up_without_being_read() {
|
||||
// The sheet painter asks the cache every frame and the PDF export every draw; a source
|
||||
// read from a file can be megabytes, and the lookup must not scan it each time.
|
||||
let big = "l".repeat(1 << 20);
|
||||
let mut c = EquationCache::default();
|
||||
c.refuse(&big, "budget");
|
||||
HASHES.with(|h| h.set(0));
|
||||
let start = std::time::Instant::now();
|
||||
for _ in 0..1000 {
|
||||
assert!(matches!(c.get(&big), Some(Err(_))));
|
||||
}
|
||||
let took = start.elapsed();
|
||||
assert_eq!(HASHES.with(|h| h.get()), 0, "a thousand lookups read nothing");
|
||||
assert!(took < std::time::Duration::from_millis(500), "{took:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,10 @@ pub use style::{
|
||||
Alignment, BorderLine, BorderStyle, Borders, Color, Fill, Font, HAlign, NumFmt, PatternType, Style, StyleId, StyleTable, Theme, Underline,
|
||||
VAlign, VertAlign,
|
||||
};
|
||||
pub use workbook::{CalcMode, CalcSettings, DefinedName, DocProps, PhoneticProps, PhoneticRun, PhoneticString, ShowObjects, Workbook, WorkbookView};
|
||||
pub use workbook::{
|
||||
CalcMode, CalcSettings, DefinedName, DocProps, EQUATION_BUDGET_MESSAGE, MAX_EQUATION_LAYOUT_BYTES, PhoneticProps, PhoneticRun, PhoneticString,
|
||||
ShowObjects, Workbook, WorkbookView,
|
||||
};
|
||||
|
||||
/// Font used for new workbooks. Only a name: renderers substitute an available open font
|
||||
/// (e.g. the metric-compatible Carlito) when it isn't installed.
|
||||
|
||||
+25
-14
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::cell::{Cell, CellPicture};
|
||||
use crate::features::{
|
||||
Anchor, AutoFilter, Chart, Comment, CondFormat, FormControl, Hyperlink, Image, LineInfo, ObjKind, ObjRef, PivotTable, PrintSettings, Shape,
|
||||
SheetProtection, Slicer, Sparkline, Table, Validation,
|
||||
ShapeKind, SheetProtection, Slicer, Sparkline, Table, Validation,
|
||||
};
|
||||
use crate::store::CellStore;
|
||||
use crate::style::{Color, StyleId};
|
||||
@@ -213,8 +213,8 @@ impl Sheet {
|
||||
ObjKind::Chart
|
||||
} else if self.images.iter().any(|o| o.id == id) {
|
||||
ObjKind::Image
|
||||
} else if self.shapes.iter().any(|o| o.id == id) {
|
||||
ObjKind::Shape
|
||||
} else if let Some(s) = self.shapes.iter().find(|o| o.id == id) {
|
||||
if s.kind == ShapeKind::Equation { ObjKind::Equation } else { ObjKind::Shape }
|
||||
} else if self.slicers.iter().any(|o| o.id == id) {
|
||||
ObjKind::Slicer
|
||||
} else if self.controls.iter().any(|o| o.id == id) {
|
||||
@@ -225,7 +225,8 @@ impl Sheet {
|
||||
Some(ObjRef::new(kind, id))
|
||||
}
|
||||
|
||||
/// Whether the floating object `o` exists on this sheet.
|
||||
/// Whether the floating object `o` exists on this sheet. `Shape` accepts any shape (an
|
||||
/// equation included); `Equation` only an equation.
|
||||
pub fn has_object(&self, o: ObjRef) -> bool {
|
||||
self.object_anchor(o).is_some()
|
||||
}
|
||||
@@ -237,6 +238,7 @@ impl Sheet {
|
||||
ObjKind::Image => self.images.iter().find(|c| c.id == o.id).map(|c| c.anchor),
|
||||
ObjKind::Shape => self.shapes.iter().find(|c| c.id == o.id).map(|c| c.anchor),
|
||||
ObjKind::Slicer => self.slicers.iter().find(|c| c.id == o.id).map(|c| c.anchor),
|
||||
ObjKind::Equation => self.shapes.iter().find(|c| c.id == o.id && c.kind == ShapeKind::Equation).map(|c| c.anchor),
|
||||
ObjKind::Control => self.controls.iter().find(|c| c.id == o.id).map(|c| c.anchor),
|
||||
}
|
||||
}
|
||||
@@ -247,6 +249,7 @@ impl Sheet {
|
||||
ObjKind::Image => self.images.iter_mut().find(|c| c.id == o.id).map(|c| &mut c.anchor),
|
||||
ObjKind::Shape => self.shapes.iter_mut().find(|c| c.id == o.id).map(|c| &mut c.anchor),
|
||||
ObjKind::Slicer => self.slicers.iter_mut().find(|c| c.id == o.id).map(|c| &mut c.anchor),
|
||||
ObjKind::Equation => self.shapes.iter_mut().find(|c| c.id == o.id && c.kind == ShapeKind::Equation).map(|c| &mut c.anchor),
|
||||
ObjKind::Control => self.controls.iter_mut().find(|c| c.id == o.id).map(|c| &mut c.anchor),
|
||||
}
|
||||
}
|
||||
@@ -263,6 +266,7 @@ impl Sheet {
|
||||
ObjKind::Image => take(&mut self.images, |c| c.id == o.id),
|
||||
ObjKind::Shape => take(&mut self.shapes, |c| c.id == o.id),
|
||||
ObjKind::Slicer => take(&mut self.slicers, |c| c.id == o.id),
|
||||
ObjKind::Equation => take(&mut self.shapes, |c| c.id == o.id && c.kind == ShapeKind::Equation),
|
||||
ObjKind::Control => take(&mut self.controls, |c| c.id == o.id),
|
||||
}
|
||||
}
|
||||
@@ -714,12 +718,13 @@ mod tests {
|
||||
assert_eq!(s.col_left(2), 128.0);
|
||||
}
|
||||
|
||||
use crate::features::{ChartKind, LegendPos, ShapeFormat, ShapeKind};
|
||||
use crate::features::{ChartKind, EquationCache, LegendPos, ShapeFormat, ShapeKind};
|
||||
|
||||
/// Adds a floating object of `kind` with id `id` at `anchor`. The match is exhaustive on
|
||||
/// purpose: a new kind of floating object does not compile here until this test covers it,
|
||||
/// and the counts in `object_anchor_accessors_visit_the_same_objects_in_the_same_order`
|
||||
/// then catch an accessor that was not taught about it.
|
||||
/// then catch an accessor that was not taught about it. An equation is a shape of
|
||||
/// `ShapeKind::Equation`, so it lives in `shapes` and is visited among them.
|
||||
fn push_object(s: &mut Sheet, kind: ObjKind, id: u32, anchor: Anchor) {
|
||||
match kind {
|
||||
ObjKind::Chart => s.charts.push(Chart {
|
||||
@@ -740,14 +745,15 @@ mod tests {
|
||||
pivot: None,
|
||||
}),
|
||||
ObjKind::Image => s.images.push(Image { id, anchor, data: Arc::new(vec![]), mime: String::new(), alt: String::new() }),
|
||||
ObjKind::Shape => s.shapes.push(Shape {
|
||||
ObjKind::Shape | ObjKind::Equation => s.shapes.push(Shape {
|
||||
id,
|
||||
kind: ShapeKind::default(),
|
||||
kind: if kind == ObjKind::Equation { ShapeKind::Equation } else { ShapeKind::default() },
|
||||
anchor,
|
||||
fill: Color::default(),
|
||||
line: Color::default(),
|
||||
text: String::new(),
|
||||
format: ShapeFormat::default(),
|
||||
math: EquationCache::default(),
|
||||
}),
|
||||
ObjKind::Slicer => {
|
||||
s.slicers.push(Slicer { id, table: String::new(), column: String::new(), caption: String::new(), anchor, selected: None, columns: 1 })
|
||||
@@ -773,9 +779,11 @@ mod tests {
|
||||
(ObjKind::Chart, 9),
|
||||
(ObjKind::Slicer, 1),
|
||||
(ObjKind::Image, 4),
|
||||
(ObjKind::Control, 6),
|
||||
(ObjKind::Equation, 6),
|
||||
(ObjKind::Control, 13),
|
||||
(ObjKind::Shape, 12),
|
||||
(ObjKind::Control, 10),
|
||||
(ObjKind::Equation, 8),
|
||||
(ObjKind::Control, 15),
|
||||
];
|
||||
for &(kind, id) in &inserted {
|
||||
push_object(&mut s, kind, id, Anchor { cell: CellRef::new(id, 0), ..Default::default() });
|
||||
@@ -791,10 +799,13 @@ mod tests {
|
||||
}
|
||||
let marks: Vec<f32> = s.object_anchors().map(|a| a.dx).collect();
|
||||
assert_eq!(marks, (0..per_kind).map(|i| i as f32).collect::<Vec<f32>>());
|
||||
// The sequence is the documented one (charts, images, shapes, slicers, controls, each in insertion
|
||||
// order) and every object appears in it once.
|
||||
let documented: Vec<u32> =
|
||||
ObjKind::ALL.iter().flat_map(|&k| inserted.iter().filter(move |(kind, _)| *kind == k).map(|&(_, id)| id)).collect();
|
||||
// The sequence is the documented one (charts, images, shapes, slicers, controls, each in
|
||||
// insertion order; equations are shapes) and every object appears in it once.
|
||||
let collection = |k: ObjKind| if k == ObjKind::Equation { ObjKind::Shape } else { k };
|
||||
let documented: Vec<u32> = [ObjKind::Chart, ObjKind::Image, ObjKind::Shape, ObjKind::Slicer, ObjKind::Control]
|
||||
.iter()
|
||||
.flat_map(|&k| inserted.iter().filter(move |(kind, _)| collection(*kind) == k).map(|&(_, id)| id))
|
||||
.collect();
|
||||
let visited: Vec<u32> = s.object_anchors().map(|a| a.cell.row).collect();
|
||||
assert_eq!(visited, documented);
|
||||
// The mark written through the bulk accessor landed on the object `object_anchor` finds
|
||||
|
||||
@@ -11,6 +11,13 @@ use crate::cell::Formula;
|
||||
use crate::sheet::Sheet;
|
||||
use crate::style::{StyleTable, Theme};
|
||||
|
||||
/// Most equation source, in bytes, one [`Workbook::refresh_equations`] lays out: about 2,500
|
||||
/// typical equations, or a few seconds of the slowest 4 KB sources.
|
||||
pub const MAX_EQUATION_LAYOUT_BYTES: usize = 128 * 1024;
|
||||
|
||||
/// Why an equation past [`MAX_EQUATION_LAYOUT_BYTES`] has no layout.
|
||||
pub const EQUATION_BUDGET_MESSAGE: &str = "This workbook has more equations than are laid out at once; edit this equation to lay it out.";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CalcMode {
|
||||
#[default]
|
||||
@@ -463,6 +470,30 @@ impl Workbook {
|
||||
}
|
||||
None
|
||||
}
|
||||
/// Rebuilds the cached layout of every equation whose source changed (or was never laid
|
||||
/// out, as after loading a file). One call lays out at most [`MAX_EQUATION_LAYOUT_BYTES`] of
|
||||
/// source: one equation can take a tenth of a second to lay out, so a file with thousands of
|
||||
/// them would otherwise hold the app for minutes. Equations past the budget get an error
|
||||
/// instead (they show their source; editing one lays it out).
|
||||
pub fn refresh_equations(&mut self) {
|
||||
let stale = |s: &crate::Shape| s.kind == crate::ShapeKind::Equation && s.math.get(&s.text).is_none();
|
||||
let mut budget = MAX_EQUATION_LAYOUT_BYTES;
|
||||
// Only sheets with a stale equation are copied (sheets are shared with undo snapshots).
|
||||
for sh in self.sheets.iter_mut().filter(|sh| sh.shapes.iter().any(stale)) {
|
||||
for s in Arc::make_mut(sh).shapes.iter_mut().filter(|s| stale(s)) {
|
||||
// A source over the length cap never gets here: the cache answers for it without
|
||||
// a lookup. An empty source still costs a call.
|
||||
let cost = s.text.len().max(1);
|
||||
if cost > budget {
|
||||
s.math.refuse(&s.text, EQUATION_BUDGET_MESSAGE);
|
||||
continue;
|
||||
}
|
||||
budget -= cost;
|
||||
s.math.refresh(&s.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A free id for a new chart, picture, shape, table, slicer or form control: one more than the largest in
|
||||
/// use, or, when that is `u32::MAX` (a file can carry any id), the lowest free one.
|
||||
pub fn next_object_id(&self) -> u32 {
|
||||
|
||||
Reference in New Issue
Block a user