mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-03 08:51:30 -06:00
1e4bfecdf3ac75169c6900141a6dd0d845b11c19
261
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7bb397a3c1 |
UI: RefEdit reference boxes in dialogs (Goal Seek, Solver, Data Analysis) (#494)
* UI: RefEdit reference boxes in dialogs; Goal Seek picks its cells on the grid A reference box (RefEdit) is a text field that takes a range by selection on the grid. While a box is the one being picked into (it was clicked into or collapsed), clicking or dragging on the sheet writes the absolute reference into it, with the sheet name when the selection is on another sheet; typing still edits it, and the box takes the keyboard back after a pick. Focusing another text field of the dialog ends picking. The collapse button beside each box shrinks the dialog to that box, at the dialog's top-left corner, so the sheet is free for selecting. Escape, Enter or the expand button restore the dialog. The sheet and selection from before picking are restored before the dialog runs its command (references without a sheet name mean the sheet the dialog was opened on) and when it closes. The button is focusable, works with Space and Enter, and is named for screen readers; the icons are drawn in code. Goal Seek's Set cell and By changing cell are reference boxes. The snapshot example's --refedit key=range collapses the open dialog to a box and selects a range, to render the collapsed state. * UI: Solver and Data Analysis take their ranges with RefEdit boxes Form dialogs gain a Ref field, drawn as a reference box with its collapse button. Data Analysis uses it for the input, variable, X and Y and bin ranges, and for Output Range, where picking or typing a reference also chooses that output option. Solver Parameters uses reference boxes for the objective, the variable cells and both sides of a new constraint. * UI: Data Analysis keeps New Worksheet Ply when chosen after picking an Output Range The Output Range box chose its option on every frame while it was the box being picked into, so clicking New Worksheet Ply after using the box reverted to Output Range on the next frame. The box now chooses Output Range only when it is clicked into or its text changes (typed or picked), and choosing another output option ends picking into it, so a later selection on the grid doesn't fill it either. * UI: RefEdit boxes remember the dialog's sheet by name The sheet a dialog was opened on was kept by index, so a sheet inserted or moved while picking (the ribbon stays usable) made a range on the new sheet lose its sheet name, and closing the dialog went back to whichever sheet now had that index. The sheet is now kept by name, as New Formatting Rule's formula box does. * UI: switching sheets while picking into a RefEdit box doesn't fill it A reference on another sheet carries the sheet name, so after a click on another sheet tab the selection never matched the one last seen, and the switch alone wrote that sheet's remembered selection into the box, replacing a typed reference. The box now notes which sheet its last seen selection was on: showing another sheet only refreshes it, and a selection made on that sheet still fills the box. * UI: a RefEdit box for one reference takes only the area being selected A selection of several areas (Ctrl+click) was written into every box as a comma-joined union, which Goal Seek, Solver's objective and constraints, and Data Analysis then refused as an invalid reference. Solver's variable cells still take every area; the other boxes take the area being selected. * UI: RefEdit boxes follow the dialog's sheet when it is renamed The dialog's own sheet was kept by name only, so renaming it while a reference box picked made references on it carry the new name as if it were another sheet, and running or closing the dialog could not put the selection back. The sheet's index and the sheet count are now kept with the name: when the name disappears while the count stays the same, the sheet at that index was renamed and its new name is taken. Inserting, moving or deleting other sheets still follows the name. * UI: a deleted dialog sheet is never taken for another sheet The dialog's own sheet was taken as renamed whenever its name was gone and the sheet count matched the last frame's, so deleting it was read as a rename a frame later (or after a sheet was inserted), and the surviving sheet at that index became the dialog's sheet: unqualified references ran against it and Cancel put the old selection on it. The sheet names of the last frame are now kept in order. A rename is taken only when the list keeps its length and only the home sheet's entry changed; any other loss of the name marks the sheet as deleted for good. Once it is deleted, picks carry their sheet's name, closing leaves the selection where it is, and OK is refused with a message, since references without a sheet name would otherwise run against whichever sheet is shown. * UI: RefEdit dialogs keep their sheet until they close OK put the saved selection back and dropped the dialog's sheet with it. When the command then failed, the dialog stayed open without its sheet: a later deletion of that sheet went unnoticed and a second OK ran references without a sheet name against the sheet shown, as it also did when the person had only moved to another sheet after the failed OK. The sheet is now kept, and followed through renames and deletion, until the dialog closes; only the saved selection is put back once. A later OK or Cancel shows the dialog's sheet again (or is refused once it is deleted), and picking again on that sheet saves its selection anew. * UI: Solver's Model Range is a RefEdit box * UI: a collapsed RefEdit box keeps picking when something else is clicked * UI: a dialog opened over a picking RefEdit dialog puts the selection back * UI: arrow keys select on the grid while a dialog is collapsed to a RefEdit box * UI: F4 cycles the anchors of the reference in a RefEdit box * UI: tests for Enter while collapsed and OK after a cross-sheet pick * Docs: RefEdit boxes' keyboard route, Model Range and click-elsewhere behaviour * UI: a confirmed RefEdit dialog stays on the sheet its command showed * UI: scripted dialog cancel and confirm end a RefEdit pick like the buttons * UI: RefEdit boxes run their selection and sheet commands through the app * Docs: RefEdit dialogs after OK, scripted cancel and confirm * UI: a RefEdit dialog's OK stops when the picked sheet's draft cannot be saved * Docs: ui.dialog.confirm applies to command-backed dialogs; a draft that cannot be saved stops OK * UI: Cancel of a RefEdit dialog is never a confirmation, and says when it cannot go home * Docs: cancelling a RefEdit dialog when its sheet cannot be shown again * UI: a RefEdit dialog follows its sheet's rename only through the engine's recorded steps * UI: F4 in a RefEdit box splits areas only at commas outside quoted sheet names * Docs: how a RefEdit dialog follows its sheet's rename * UI: a RefEdit dialog's sheet is told from a new sheet given its name The dialog followed its sheet by name and read the recorded steps only once the name was gone, so a sheet deleted and recreated under its own name between two frames was taken for the dialog's sheet, and OK resolved unqualified references against the replacement. The home sheet is now followed through every recorded step since the dialog last looked: a sheet with its name is the same sheet only while its file id is unchanged (a sheet inserted in the session has none), a rename keeps the file id and the cells, and a deleted sheet is remembered as it was, so only an undo that brings that very sheet back revives it. When the steps are not accounted for, a sheet with the name is taken for it, as before. A sheet never saved to a file and replaced inside one undo group is the one case nothing in the model tells apart; the code comment says so. * Docs: how a RefEdit dialog tells its sheet from a new sheet given its name * UI: a RefEdit dialog follows a sheet that came from a file by its file id A sheet read from a file keeps the file's sheetId, which no step changes and which a sheet inserted in the session lacks, so the dialog's sheet is found by it wherever a recorded step puts it and whatever the step renames it to: a group that renames the sheet and inserts a new one under its old name follows the rename instead of refusing OK. When the step also copied the sheet (a copy keeps the id), the one keeping the name is taken. A sheet never saved to a file is still followed by name, with the rename rule as before. * Docs: a RefEdit dialog follows a sheet that came from a file by its file id * UI: a RefEdit box the dialog stops showing is no longer picked into A reference box compares the grid selection with the one it last saw only while it is drawn, so a box hidden while it was the one being picked into (Solver's Options view, the constraint value once the operator is int or bin) took a selection made meanwhile when it came back. After the dialog's window is drawn, a target box that was not drawn in the frame ends the pick, whatever hid it; Model Range no longer needs its own rule when the Load/Save area is hidden. * UI: the Name Box keeps its keys while a dialog is collapsed to a RefEdit box The collapsed picker took Escape, Enter and the arrow keys out of the frame's input whoever had the keyboard. The Name Box, drawn before the dialogs, had already read an arrow when the picker removed the same event and moved the grid selection, and the box then took that selection and the keyboard; its Escape expanded the dialog instead of only leaving the box. The picker now acts on those keys only when no other text field owns the keyboard, judged by the focus at the end of the frame before as well as now, since egui drops the focus for Escape before anything is drawn. The other field's Escape and Enter are still used up, so the dialog neither closes nor confirms on them. * Docs: a sentence break in the ui.dialog row of the control protocol * UI: a RefEdit dialog keeps its sheet by the sheet's identity, not by file id or history The dialog's sheet was followed by name through the recorded undo steps, with the file's sheet id telling a saved sheet from a new one; a copy made with sheet.move keeps that id, so once the original was deleted the copy, as the only sheet of the id, was taken for the dialog's sheet and OK ran against it. The model now gives every sheet an identity while the app runs (SheetUid: fresh for every sheet added, inserted, copied or read, kept through renames, moves, undo and redo), so Home keeps the sheet's uid and finds the sheet by it each time it is needed. The history walk, the rename rule and the file-id and Arc comparisons are gone, with the undetectable single-step case they left: a sheet given the name, a replacement inside one undo group and a copy are all other sheets, and an undo that brings the original back makes it the dialog's again. Nothing of the unaccounted-steps fallback is kept, since the identity always answers. Regression test: copy the dialog's sheet, delete the original, OK is refused; undo, OK runs on the original. The identity tests no longer need a workbook that came through a file. * UI: Solver's Reset All, Load, Save and Replace act on the dialog's sheet, as Solve does Only Solve went through the dialog's home-sheet checks before running (dialogs::show): Reset All, Load Model, Save Model and Replace ran their commands straight from the form, on whichever sheet was active. After a reference box picked on another sheet and the dialog expanded, that sheet was still shown, so Reset All removed its model instead of the dialog's sheet's, and Load and Save read and wrote the model range on it. The four buttons now run only after the same checks as Solve: the dialog's sheet must still exist (else the command is refused with the deleted-sheet message) and is shown again with the selection from before picking put back (a failure to show it, a text-box draft that could not be saved, is shown as the dialog's error and the command does not run). Goal Seek has no other buttons, and Data Analysis's Replace goes through the confirm path that Solve uses, so neither needed a change. Regression tests: after a cross-sheet pick, Reset All clears the dialog's sheet's model and leaves the other sheet's; Save and Replace write the dialog's sheet; Load reads it. * UI: Protect Sheet opened while a reference box picks on another sheet acts on the dialog's sheet * Docs: a scripted Goal Seek after a cross-sheet pick cancels the dialog before data.goalSeek * UI: the Protect Sheet labels follow the dialog's sheet while a reference box picks on another * Docs: a scripted Goal Seek checks that the dialog's sheet still exists after cancel * Control: ui.dialog.cancel replies sheetDeleted when the dialog's own sheet is gone, even if its name was reused * M2: allow shared-runner variance in the pathological math timing gate * M2: refuse sheet protection actions after a reference dialog home is deleted --------- Co-authored-by: FredApps <FredApps@users.noreply.github.com> |
||
|
|
e9430daa9a |
Merge the updated Python cells branch (7ddcb800, main 6f52cc50) into the Python mode layer
Conflicts in ROADMAP.md, docs/architecture.md and docs/ui-parity.md only: header lines combined, both sides' sections and rows kept. |
||
|
|
7ddcb800ea |
Merge origin/main (6f52cc50) into the Python cells branch
Main gained #471 (equations) and #2729 (command palette shortcuts). Conflicts in ROADMAP.md, docs/architecture.md and docs/file-format-parity.md only: header lines combined, both sides' sections and rows kept. |
||
|
|
6f52cc5090 |
Equations: Insert › Equation with a LaTeX editor, Office Math in xlsx and equations in PDF (#471)
* Objects: one selection for charts, pictures, shapes and slicers, with eight resize handles
The sheet tracked the selected object as a bare id in `selected_chart`, so
only charts and text boxes drew a selection frame, only the bottom-right
corner resized, and the kind of the selected object had to be guessed from
the id. The selection is now `SheetApp::selected_object: Option<ObjRef>`,
with `selected_chart()` kept for the Chart Design tab and the Format Chart
pane.
- Model: `ObjKind` (chart, image, shape, slicer) and `ObjRef { kind, id }`;
`Sheet` helpers find, move and remove an object by `ObjRef` and walk every
object's anchor in a fixed order. `next_object_id` saturates instead of
overflowing on an id at `u32::MAX` read from a file.
- Commands: `object.delete`, `object.move` and `object.setAnchorMode` accept
the four kinds through one parameter check, refuse an unknown kind or id
instead of succeeding silently, and clamp hostile offsets. `object.delete`
replies with the deleted object. Arrange and the Selection Pane include
slicers, which stack above the other objects.
- Sheet: the selected object of any kind draws a frame with eight handles
over every object. Each handle resizes only its own edges, which never
cross; the body moves the object, and the first drag frame already applies
the pointer's movement. The pointer shows the handle's resize cursor or a
move cursor, with the diagonals swapped on a right-to-left sheet, where the
frame moves with its object.
- Keys: Escape drops the object selection; Delete and Backspace keep
deleting the selected object (#213). A selection whose object disappears
(undo, redo, delete, sheet switch) is cleared.
- Double-click: `open_object` routes by kind (a text box edits in place, a
chart opens Format Chart).
- Control protocol: `ui.inspect` reports `selectedObject {kind, id}` and
keeps `selectedChart`; `ui.object.select` selects any object and replies
with its rect. The snapshot example takes `--object <id>`.
Tests: engine tests for every kind by kind and id, the Selection Pane and
hostile parameters; UI tests for selecting each kind (a slicer over a chart
takes the click), all eight handles on four kinds, cursors, Delete,
Backspace and Escape, undo and redo, double-click routing, and resizing on a
right-to-left sheet. Existing tests use the new selection API.
* Objects: every kind follows edits; chart series follow cut-moves and sheet deletes
Structural edits and sorts kept three hand-indexed lists (charts, images,
shapes) in step in two places, and slicers were left out of both, so a
slicer stayed put when rows were inserted above it. Chart series were
rewritten for inserted and deleted lines and for sheet renames, but not for
cut-moves or sheet deletes, which left them pointing at the old cells.
- Inserting and deleting rows and columns and sorting walk
`Sheet::object_anchors` and `object_anchors_mut`, one fixed order over
every kind, so slicers now follow their cells by their anchor mode like
the other objects, and a new kind cannot be forgotten in one of the
passes.
- `rewrite_cell_links` is the one place that rewrites the references
floating objects hold (chart series values, categories and names,
and the source block a chart was made from).
`rewrite_all_formulas` calls it, which covers inserting and deleting
lines and cells and cut-moves on one sheet or across sheets; renaming and
deleting a sheet call it too, so a series into a deleted sheet becomes
#REF!, as a formula does.
- Copying a sheet gives its charts, pictures, shapes and slicers fresh ids,
keeping ids unique across the workbook, which object selection and the
`{kind, id}` commands rely on.
Tests: every kind through row and column inserts and deletes, an absolute
shape, a sort and undo; a slicer through inserted rows; chart series
through a cut-move, a sheet rename, a sheet delete and undo; ids after a
sheet copy.
* Objects: bubble sizes follow edits like the other series references
`rewrite_cell_links` rewrote a chart series' values, categories and name
but not its bubble sizes, so inserting or deleting lines, a cut-move or a
sheet rename left a bubble chart's sizes pointing at the old cells. They
are now rewritten with the rest.
Test: bubble sizes through an inserted row, a cut-move and a sheet rename.
* Objects: a resize handle under the headers leaves the press to them
A selected object's handles reach a few points outside it, and a press
was matched against them before any check of where it landed, so a press
on the column or row header beside an object at the sheet's edge resized
the object instead of selecting the column or row. The cursor already
ignored handles outside the cells.
`chartview::selected_handle` finds the selected object's handle under a
point only over the cell area, as `hit` does, and not while objects are
hidden; the press and the cursor both use it.
Test: presses within a handle's reach over the column header and the row
header neither move nor resize the object.
* Objects: each drag of an object is its own undo step
A drag merges its frames into one undo step by dropping a new step whose
label matches the one before it. Its first frame did the same, so a second
drag of an object (a move after a move, or a resize after a move) merged
into the first drag's "Move Object" step, and one undo took back both.
`Drag::Object` records whether the drag has made its step; only the later
frames of the same drag merge into it.
Test: a move and then a resize of a picture take two undos to reverse,
the first restoring the moved picture.
* Objects: the parity and gap docs no longer list chart resizing as missing
gaps.md (§4), target-app-parity.md (the Essentials tier's work, the
Mainstream chart row, the interaction discount and the Essentials chart
task) and chart-parity.md (the element table and the work list) still
described charts as impossible to resize (#190). Charts move and resize
from eight handles like every floating object, so those statements now
say so; the 2026-10-10 revision rows and the 10-10 re-check stay as
history. Scores and the 40-60 h chart estimate are unchanged (resizing
was 1-2 h, within the rounding).
* Objects: one chart evidence paragraph in gaps.md; trendlines are not missing
gaps.md §4 carried two evidence paragraphs, an older one saying PRs #158
and #161 were open and a newer one saying all 2-D kinds survive XLSX round
trip, both repeating the resize, smooth-line, Switch Row/Column and
trendline statements. It is one paragraph with the current status again.
The Mainstream chart row in target-app-parity.md listed trendlines as
missing; they are read, written and drawn (`Series::trendlines`, the XLSX
chart reader and writer, `chart.addElement`), as chart-parity.md records.
The row now says so and keeps "no axis options". No score change.
* Objects: only the selected object's handles resize; a press elsewhere on an object moves it
A primary drag looked for a handle under the press on whatever object it
grabbed, so pressing near the edge or corner of an object that was not
selected started a resize, while the pointer over that spot showed the
Move cursor (handles are only shown, and only offered by the cursor, on
the selected object). The press now takes its handle from
`chartview::selected_handle`, the same lookup the cursor uses; a press on
any other object selects and moves it.
Test: dragging an unselected shape by its bottom-right corner shows the
Move cursor, then selects and moves the shape without resizing it.
* Equations: gridcraft-math lays LaTeX out with RaTeX, behind caps and a panic guard
Equations need a LaTeX layout engine. RaTeX (ratex-parser, ratex-layout,
ratex-types, ratex-font; MIT) turns LaTeX into positioned glyphs, rules and
paths. Before adopting it, a gate compared it with writing an in-house
subset:
- Source scan: about 31,700 lines, 95 non-test panic sites (87 in the
parser, mostly unwraps on arguments already counted and fixed regular
expressions), no unsafe code, no clocks, threads, file or environment
access.
- Fuzzing: 70,000 random token strings and 50,000 mutations of valid
equations with no panic and no input slower than 5 ms. Macro bombs stop
at 1,000 expansions; nesting deeper than 32 is refused, and the deepest
accepted input needs under 256 KB of stack.
- The crates build for wasm32-unknown-unknown.
- Syntax coverage: 56 of 57 probes of everyday notation lay out.
The gate passes, so RaTeX is used, pinned exactly at 0.1.14. The new
gridcraft-math crate (layer 1, registered in the layering table) is the only
code that calls it:
- `layout` turns LaTeX into a `MathList` of plain glyph, rule and path data
in em units, with faces (roman, italic, bold, sans, mono, large operator
sizes), explicit colours and a plain fallback character for glyphs a font
may lack. `check` only parses. Both cap the source at 4 KB and the nesting
at 32 before RaTeX sees it, cap coordinates and item counts in the result,
and run under `catch_unwind`, so a panic becomes a `MathError` with the
offending span when RaTeX reports one.
- `to_omml` writes the common subset as Office Math (ECMA-376 Part 1, 22.1):
runs and font styles, fractions and binomials, radicals, scripts, n-ary
operators with limits, functions and limits, delimiters, matrices,
accents, bars and braces. Anything else is an error, so the caller can
save the equation as text instead.
crates/math/tests/gate.rs re-runs a smaller, deterministic version of the
gate (fuzzing, depth, macro bombs, coverage) on every test run, so a version
bump has to pass it. docs/decisions.md records the evidence and the guard
design. RaTeX positions glyphs with KaTeX's font metrics (MIT); no KaTeX
font files are bundled.
* Equations: Insert › Equation, an equation editor, and equations on the sheet
Equations are shapes (`ShapeKind::Equation`) whose text is their LaTeX
source; they select, open and delete as their own object kind
(`ObjKind::Equation`).
- Model: `Shape::math`, an `EquationCache` holding the layout of the
source (or the reason it has none), keyed by a hash of the source. It is
never saved and is ignored when shapes are compared.
`Workbook::refresh_equations` lays out only the equations whose source
changed, copying only the sheets that hold one. Every new or opened
document (`DocState::new`, `DocState::from_saved`) and every commit
call it, so files, pastes and undo snapshots always arrive with a layout
and painting never lays anything out.
- Commands: `insert.equation {latex, cell?|at?, size?, color?}` sizes the
frame for the requested font size (11 pt by default) and anchors it to
move, but not size, with cells. `equation.edit {id, latex}` keeps the
scale the equation is drawn at and refits the frame, in one undo step.
Missing source, or source that does not lay out, is an error naming the
character position ("equation error at character 5: Undefined control
sequence: \foo"). An equation is drawn to fit its frame, so resizing the
frame scales it.
- Painting reads only the cache: glyphs in system maths font roles
(`theme::MATH`, `MATH_ITALIC`, `MATH_BOLD`: STIX Two, Cambria Math, Latin
Modern, DejaVu or Liberation Serif, then Times; nothing is bundled), large
operators and delimiters stretched to their TeX boxes, rules, and
outlines filled by ear clipping. A glyph no face draws becomes a [?] box;
since egui's `has_glyph` misreports the glyphs of the face that supplies
the replacement glyph, a negative answer is confirmed by comparing atlas
slots. Ink defaults to the sheet's text colour. Source that does not lay
out (from a file) is shown as text in a dashed frame.
- Ribbon: Insert gains a Symbols group with an Equation split button before
Symbol. The main half (access keys N E, previously reserved in
keytips.json) opens the editor; the menu has Insert New Equation... and a
gallery of GridCraft's own examples (`gridcraft_math::GALLERY`), each
drawn as a preview and inserted with one click. The new labels are
translated for every interface language, and the Equation icon is drawn
in code.
- Editor (dialog "equation", also driven by `ui.dialog.set` and
`ui.dialog.confirm`): quick-insert buttons for common constructs, Greek
letters and relations, inserted at the caret with the caret moved into the
first `{}`; the code editor in LaTeX mode with the error's span underlined
(`code_editor_marked` and `code_editor::underline`); the error with its
character position, or a live preview on a sheet-coloured card; OK is
disabled while the source has an error, and Cmd/Ctrl+Enter runs
`insert.equation` at the active cell or `equation.edit`. Double-clicking an
equation opens the editor on its source.
- xlsx writes an equation as a text box holding its source for now.
The keytip test that used `insert.equation` as its stand-in for a pending
command now uses `insert.object`. Docs: architecture (the math crate and the
cache), UI, gaps and target-app parity, ROADMAP; the parity checklist is
regenerated.
Tests: engine tests for insert sizing, size and colour, undo, hostile and
bad source with positions, edit scale and refit, no-op edits, refusals,
protected sheets, the cache rebuilt on load and not saved, a bad source
from a file, and equations in the object tests; UI tests for opening the
editor from the ribbon and with N E, typing an equation in, the error
position blocking OK, quick-insert at the caret, double-click editing as
one undo step, the gallery, an equation in the selection and resize tests,
ear clipping, curve flattening and underline splitting.
* Equations: Office Math in xlsx, and the Symbol font in PDF
xlsx: an equation is written as mc:AlternateContent. The a14 choice is a
text box holding the equation as Office Math (a14:m with m:oMathPara, from
gridcraft_math::to_omml at the size the equation is drawn at, in its ink
colour); the fallback is the same text box with the LaTeX as plain text, so
other readers still show the source. Both carry
descr="gridcraft:latex:<source>". Source outside the Office Math subset is
written as the text box alone.
On reading, the descr source wins, exactly (quotes and newlines included).
Without it, as in files from other writers, the new xlsx::omml reader
converts the common subset back to LaTeX: runs (upright, text,
double-struck, script, Fraktur, sans and bold styles, operator names),
fractions, radicals, scripts, n-ary operators with limits, delimiters with
separators, matrices, equation arrays, accents, bars, group characters,
lower and upper limits, functions, border boxes and phantoms. Anything else,
or a result that does not parse, stays a text box. The reader is
depth-bounded. The drawing reader now takes an mc:Choice that holds Office
Math, not only chart frames. xlsx may now depend on gridcraft-math
(FORMAT_ALLOWED), a pure layer-1 crate.
PDF: crates/pdf gains Font::Symbol (the thirteenth standard font, written
without WinAnsiEncoding) with its Unicode-to-Symbol encoding table and the
advance widths of Adobe's Core 14 Symbol.afm, cited in symbol.rs as
metrics.rs cites the other faces; Font::can_encode tells callers what a face
can show. Symbol text is written as one run in Symbol's own encoding, not
split off to the CID font that the text faces use for characters WinAnsi
lacks. Printing and PDF export draw an equation's cached layout: letters
and digits in Times (italic for variables) or Helvetica and Courier for sans
and monospace styles, Greek letters and operators in Symbol, large operators
and delimiters stretched to their TeX boxes, the radical fitted to meet its
vinculum, rules as filled rectangles and outlines as filled polygons. A
glyph outside WinAnsi and Symbol (after its plain fallback, R for a
double-struck R) prints as a [?] box.
Docs: file-format parity, gaps, target-app parity and ROADMAP.
Tests: xlsx round trips (choice and fallback markup, descr, colour, anchor
mode, an equation outside the subset, hostile sources, a text box left
alone), Office Math without a source read back to the same glyphs, the
reader's subset, unsupported constructs and the depth bound, and gallery
write-then-read; Symbol encoding and widths for every mapped character; a
PDF with Symbol and Times-Italic, the summation and alpha codes, and a [?]
box.
* Equations: the error underline snaps to char boundaries, so a stale mark cannot crash the editor
The equation editor computes the error's byte range from the source as it
was before this frame's edit, and `code_editor::underline` only clamped the
range to the text's length. Typing a multibyte character in front of the
error (`a\foo` with its error at 1..5, then `é` at the start) put a section
boundary inside the `é`, and egui's layout panicked slicing the text there,
in the paint path, where no guard catches it.
`underline` now moves the mark's start down and its end up to the nearest
char boundaries, so any byte range is safe.
Test: a_stale_mark_inside_a_multibyte_char_snaps_to_char_boundaries marks
every range over texts with two-, three- and four-byte characters, checks
that every section is on char boundaries and covers the text, and lays each
job out; it fails without the fix.
* Equations: the editor focuses its source on every opening, however the last one closed
The editor kept a "focused once" flag in egui memory and removed it only
on OK, Cancel and Cmd/Ctrl+Enter. Escape and the window's close button
also close the dialog, and left the flag set, so the next Insert ›
Equation or Edit Equation opened without focus on the LaTeX source and
typing went nowhere.
The flag is now the dialog's own `focus_initialized` (made visible to the
crate), as Command Search uses it: every opening builds a new dialog with
it unset, whatever closed the previous one.
Test: reopening_after_escape_focuses_the_source_again opens the editor
twice, types into it and closes it with Escape each time; it fails
without the fix (the second round's typing is lost).
* Equations: Office Math the reader cannot convert keeps the writer's fallback
The drawing reader took an `mc:Choice` whenever its text box held Office
Math, before knowing whether that Office Math converts to LaTeX. Office
Math outside the reader's subset, without GridCraft's source in `descr`,
then became a text box with no text, and the `mc:Fallback` (the picture or
text the writer saved for readers like this one) was never read.
The choice is now taken only when `omml::to_latex` converts its Office
Math; otherwise the fallback is read. GridCraft's own files are
unaffected: their fallback carries the same source in `descr`.
Test: office_math_outside_the_subset_reads_the_fallback replaces a saved
equation's Office Math with a pre-subscript (outside the subset), removes
the source and gives the fallback its own text; it fails without the fix
(the shape is read with empty text).
* Equations: hostile LaTeX through the guarded entry points and the xlsx round trip
The gate fuzzes RaTeX itself. These tests fuzz what GridCraft's callers
reach:
- the_guarded_entry_points_survive_hostile_input runs `layout`, `check`
and `to_omml` over random token soup (any Unicode scalar included),
inputs at and past the nesting and length caps, huge rules and a macro
bomb: none panics, every layout stays within `MAX_EM` and `MAX_ITEMS`,
every error span is on char boundaries inside the source, and what lays
out also checks.
- hostile_sources_round_trip_exactly saves 200 sources built from XML and
LaTeX specials (quotes, `<`, `&`, `]]>`, comments, CR, LF, tab, emoji,
CJK, unbalanced braces) to xlsx and reads each back as the same equation
source.
Both pass on the current code; they guard the caps and the writer's
escaping against regressions.
* Equations: opening a file lays out a bounded amount of equation source
`Workbook::refresh_equations` laid out every stale equation on load and
on every commit. One 4 KB equation can take about a tenth of a second to
lay out, so a hostile or simply large file with thousands of equations
held the app for minutes before it showed anything.
One call now lays out at most `MAX_EQUATION_LAYOUT_BYTES` (128 KB, about
2,500 typical equations) of source. Equations past it get an error
(`EquationCache::refuse`, without running the layout engine), so they show
their source in the dashed frame and later commits do not retry them.
`equation.edit` with an unchanged source now lays out an equation that has
no layout, so OK in the editor brings one back. Architecture docs note the
budget.
Test: a_file_with_too_many_equations_lays_out_a_bounded_amount opens a
workbook whose equations exceed the budget, checks that the laid-out
source stays within it, that the rest carry the budget's error and are not
retried by a later edit, and that `equation.edit` lays one out; it fails
without the budget (every equation is laid out).
* Equations: the layout cache answers only for the source it was built from
`EquationCache` matched an entry by a 64-bit hash of the source
(`DefaultHasher`, fixed keys). A crafted pair of sources with the same
hash, for example one in a file and one typed by the user, would make the
cache return one source's drawing for the other.
An entry now keeps its source and compares it in full on every lookup;
the hash and length are only a quick first check. A source over the
4 KB length cap is not copied into the entry: it never lays out, so its
entry is always an error, and it is matched by length and hash, and only
by another source over the cap. Kept sources are shared (`Arc<str>`), so
copies of a sheet and undo snapshots do not duplicate them.
Tests: a_hash_collision_never_returns_another_sources_layout forges a
collision by giving an entry for `x^2` the hash of `y^3` (same length) and
checks that `y^3` misses, is laid out afresh, and that a refusal does not
answer for a colliding source either; it fails without the fix (`y^3` gets
the layout of `x^2`). sources_over_the_cap_are_not_copied_and_never_lay_out
checks the cap path.
* Equations: a text box with Office Math beside text or other maths stays a text box
The drawing reader made any shape whose text held Office Math into one
equation, taken from the first `m:oMath`. A text box from another writer
with text beside the maths ("Solve: " and an inline equation), or with two
equations, lost the text, the other equation and the shape's formatting.
The `mc:Choice` filter used the same check, so the writer's fallback was
skipped as well.
`omml::equation_source` now converts a text body only when it holds one
equation and nothing else: no text in its runs or fields and no second
`m:oMath`/`m:oMathPara`. The Choice filter and `read_shape` both use it, so
a mixed box reads the fallback (inside alternate content) or stays a text
box with its text.
Test: office_math_beside_text_or_other_math_is_not_an_equation covers a
text run before the maths in the a14 choice (the fallback's text is read),
the same box without alternate content (a text box that keeps "Solve:"),
and two equations in one box (not an equation). It fails without the fix
(the first case reads an equation); the two-equation case fails when only
the second-equation check is removed.
* Equations: a new object id is a free one even when a file used the largest id
`Workbook::next_object_id` returned one more than the largest id in use,
saturating at `u32::MAX`. A workbook read from a JSON file can carry an
object with that id; every later insert (an equation, chart, picture,
shape, table or slicer) then got `u32::MAX` again, and so did each object
of a copied sheet, whose ids also came from a saturating counter. Lookups
and selection take the first object with an id, and `object.delete`
removes every one.
`Workbook::free_object_ids(count)` now returns distinct unused ids: those
above the largest in use while they last, then the lowest free ones from
1. `next_object_id` takes the first of them, and the sheet copy takes one
per copied object (and refuses if the id space were ever exhausted). The
model test that recorded the saturation now checks the free ids instead.
Test: ids_stay_unique_when_a_file_used_the_largest_id opens a workbook
whose equation has id u32::MAX, inserts two equations and copies the
sheet; every id is distinct. It fails without the fix (both new equations
get 4294967295). object_ids_never_overflow_or_repeat covers the gaps and
the order.
* Equations: the control protocol and UI parity list equation among the object kinds
`ui.inspect` reports a selected equation as `selectedObject: {kind:
"equation", id}`, and `object.delete`, `object.move`,
`object.setAnchorMode` and the `arrange.*` commands take `equation` as a
`kind`, but docs/control-protocol.md listed only chart, image, shape and
slicer. It now lists equation too, as does the floating-objects row of
docs/ui-parity.md.
* Docs: ui.object.select says what it replies when it clears the selection
* Objects: a test pins that both anchor accessors visit the same objects in the same order
Sorting and row or column edits read every anchor through
`object_anchors`, compute the new ones and write them back through
`object_anchors_mut` in step. Nothing checked that the two accessors
visit the same objects in the same order; a kind of floating object
reaching only one of them would have paired objects with other objects'
anchors silently.
Test: two objects of every kind are inserted in a mixed order, each
anchor is marked with its position through the mutable accessor, the
marks are read back through the shared one in the same sequence, the
count matches the per-kind collections, and the mark is found on the
object `object_anchor` names by kind and id. The test's constructor
matches on `ObjKind` exhaustively, so a new kind does not compile until
the test covers it.
* Objects: sorting keeps the size of a "move and size" picture whose lines swap places
A picture anchored to move and size with cells and spanning two rows
(or columns) was re-fitted between its top-left and bottom-right lines
after a sort. When the sort put the bottom line above the top one, the
fitted extent was negative and the picture collapsed to a zero height
or width. It now keeps its old size on that axis and travels with its
top-left line, like a "move only" picture.
Tests: for a sort by rows and one by columns, "move and size" and
"move only" pictures inside one line travel with it (the first
re-fitted to the same line, the second at its own size), an absolute
picture stays while the cells move under it, a two-line picture whose
lines swap keeps its size, and undo restores every anchor.
* Equations: Office Math whose LaTeX passes the source cap reads the writer's fallback
The OMML reader (crates/xlsx/src/omml.rs) bounded nesting (`MAX_DEPTH`
64, `find_math` depth 8) but not the length of the LaTeX it produced: a
drawing with 100,000 sibling runs, or one run of megabytes of text, was
converted in full and only then refused by `gridcraft_math::check`,
which stops at `MAX_SOURCE_BYTES` (4,096). A hostile file paid for the
whole string, and each backslash in a `\text` run grew to
`\textbackslash ` first.
`convert` (the conversion before the final check) now stops with `None`
as soon as the LaTeX produced passes `MAX_SOURCE_BYTES`: after every
node in a sequence, and after every character of a run's text, in the
escaped `\text{…}` form as well as the plain one. The check reads the
output, not the run's byte length, because a character may become a
longer macro (`\alpha `) or a shorter sign (a minus sign becomes `-`).
The caller then keeps the writer's text fallback, as for any equation
outside the subset. An equation exactly at the cap still reads back.
Regression test: `omml::tests::latex_past_the_source_cap_stops_converting_and_falls_back`.
Without the checks it fails with `left: Some("xxx…")` (a 100,000-byte
string), `right: None`. The test reads the clock, which clippy
disallows for the sake of wasm; tests never run there, and the lint is
allowed on that function.
* Equations: the hostile-source test covers equation.edit as well as insert.equation
`insert.equation` and `equation.edit` both lay a source out through
`cmd::equation::lay_out`, so both refuse a source over 4,096 bytes or
nested deeper than 32 levels, but only the insert side was asserted.
`insert_refuses_missing_bad_or_hostile_source` now edits an equation
with the same over-long and over-deep sources, expects the same errors,
and checks that the equation's source is unchanged afterwards.
* Equations: ATTRIBUTION.md records the Core 14 AFM numbers behind the PDF Symbol font
PDF export draws an equation's Greek letters and operators with the
standard Symbol font and its letters with the Times faces; no font file
is embedded (the base-14 fonts are the reader's own, and text outside
WinAnsiEncoding uses a predefined, non-embedded Adobe-GB1 CID font). What
the repository holds are numbers: Symbol's character codes and advance
widths in crates/pdf/src/symbol.rs, and the Helvetica, Times and Courier
widths in crates/pdf/src/metrics.rs, typed in from Adobe's Core 14 AFM
metrics, which Adobe distributes for use, copying and distribution for
any purpose (the notice is quoted in metrics.rs). ATTRIBUTION.md now
carries a row for those numbers, their source and that notice, so the
licence is recorded where every other third-party input is.
* Equations: the gate times pathological sources at the cap, chemistry included
`ratex-parser` uses `fancy-regex`, a backtracking engine, for its port of
mhchem: anchored patterns with lookahead tokenise the argument of `\ce`
and `\pu`, so user LaTeX does reach them. The engine stops a match after
its default 1,000,000 backtracking steps and the match counts as none,
and `gridcraft-math` caps the source at 4,096 bytes before the parser
sees it, but nothing measured how long a source built to make those
patterns (or the hand-written parser) work hardest could take.
`pathological_source_at_the_cap_finishes_in_bounded_time` in
crates/math/tests/gate.rs lays out, checks and converts 38 sources of
exactly `MAX_SOURCE_BYTES`: groups, delimiters, fractions, roots,
scripts, accents, `\text` and matrices nested to `MAX_DEPTH`; runs of
`\\`, `\{`, `{}`, primes, thin spaces and alignment tabs; a `(a+)+b`
shape; wide and tall environments; macro definitions, colours and
combining marks; and chemistry (`\ce` with digit runs, decimals,
parentheses, arrows, scripts, dollars, formulas, nested `\ce`, charges
and fractions; `\pu` with charges, exponents and unit slashes). Each
must finish inside 10 s through the guarded entry points; the slowest,
`\ce{+-+-…}`, takes 0.8 s in a debug build on an Apple M series
machine, so catastrophic backtracking would fail the test without a
slow CI runner failing it. The test reads the clock, which clippy
disallows for the sake of wasm; tests never run there, and the lint is
allowed on that function. docs/decisions.md records the finding in the
RaTeX gate.
* Objects: a copied sheet's objects get free ids even when a file used the largest id
`Workbook::next_object_id` returned one more than the largest id in use,
saturating at `u32::MAX`. A workbook read from a file can carry an object
with that id; every later insert (a chart, picture, shape, table or
slicer) then got `u32::MAX` again, and so did each object of a copied
sheet, whose ids also came from a saturating counter. Lookups and
selection take the first object with an id, and `object.delete` removes
every one.
`Workbook::free_object_ids(count)` now returns distinct unused ids: those
above the largest in use while they last, then the lowest free ones from
1. `next_object_id` takes the first of them, and the sheet copy takes one
per copied object (and refuses if the id space were ever exhausted). The
model test that recorded the saturation now checks the free ids instead,
and a new one covers a file that used the largest ids: the free ids come
from below, 100,000 of them distinct and in order.
Test: object_ids_stay_unique_when_a_file_used_the_largest_id gives a
shape id u32::MAX, inserts two shapes and copies the sheet; every shape id
is distinct across both sheets. It fails without the fix (both new shapes
get 4294967295).
* Equations: PDF prints an equation without a layout one source line at a time
When an equation has no layout (its source does not parse, or the file
was past the open-time layout budget), PDF export printed the source in
one `pg.text` call. A newline is not a WinAnsi character, so each one
became a literal `?`, and a multi-line source ran on in one line. The
xlsx plain-text fallback already writes one paragraph per line.
`fallback_lines` in crates/engine/src/cmd/print/objects.rs splits the
source at newlines and yields the lines the frame holds at 12 pt per
line (at least the first), never more than `MAX_FALLBACK_LINES` (500);
the fallback arm prints each on its own baseline.
Regression test:
`cmd::print::tests::an_equation_without_a_layout_prints_its_source_one_line_per_line`.
Without the change it fails at "the first line is its own text run"
(the PDF holds `(frac{?second line) Tj`).
* Equations: Office Math runs drop the characters XML cannot carry
`esc` in crates/math/src/omml.rs dropped C0 controls other than tab and
newline but kept U+FFFE, U+FFFF and the carriage return as they were.
The xlsx writer embeds the OMML in the drawing part as it is, so a
source holding one of them produced XML no parser accepts, where the
rest of the drawing part (escaped through the xlsx crate's `esc`, which
filters through `is_xml_char`) would have dropped it.
`esc` now keeps exactly the XML 1.0 character set the xlsx escapes keep
(tab, newline, carriage return, U+0020 to U+D7FF, U+E000 to U+FFFD and
the supplementary planes), writes the carriage return as ` ` and
drops the rest. The predicate is a copy of the xlsx one: the math crate
sits below the xlsx crate and cannot import it.
Regression test: `omml::tests::characters_xml_cannot_carry_are_dropped_from_runs`.
Without the change it fails with `left: "a\u{fffe}b\u{ffff}cde\u{7f}f"`,
`right: "abcde\u{7f}f"`.
* Equations: the matrix writer writes every row RaTeX gives it
The OMML matrix writer skipped a row whose only cell was an empty
`OrdGroup` whenever the matrix had more than one row, meant to drop the
empty last row a trailing `\\` leaves. Read against RaTeX 0.1.14 the
branch was unreachable: the parser removes that trailing empty row
itself (its `environments.rs`, "Check for trailing empty row and remove
it"), and every cell it does give is a `Styling` node around the group,
never a bare `OrdGroup`. A blank row in the middle, written as `\\ \\`
or as `{}`, was therefore already kept, but the code said otherwise and
would have dropped such a row had the parser's shape matched.
The skip is gone; the writer writes every row it is given. The new
tests pin the behaviour so that a RaTeX release that stopped dropping
the trailing row would show up: `omml::tests::only_a_trailing_empty_row_is_dropped`
(two rows for `a \\ b \\`, three for `x \\ \\ y` and for `a \\ {} \\ b`,
one for a lone `\\`) and, through the xlsx reader,
`omml::tests::a_blank_middle_row_comes_back_and_a_trailing_one_does_not`.
* Equations: to_omml takes the ink colour instead of the xlsx writer patching its output
The xlsx writer coloured the Office Math runs by replacing the exact
text `<a:latin typeface="Cambria Math"/></a:rPr>` in `to_omml`'s output
with the fill in front of it. Had that text changed, `replace` would
have done nothing, the Office Math choice would have lost its colour,
and nothing would have reported it; the round-trip test could not have
caught it either, because the plain-text fallback carried the same
colour and the reader would have returned it.
`gridcraft_math::to_omml` now takes the ink as `Option<[u8; 3]>` and
writes `<a:solidFill><a:srgbClr …/></a:solidFill>` into every run's
properties itself; the xlsx writer passes the resolved shape colour and
the string patch is gone. Tests: `omml::tests::runs_carry_the_ink_colour_when_one_is_given`
(three coloured runs for `x+1`, none without an ink), and
`equations_write_office_math_with_a_text_fallback_and_read_back` now
asserts the colour inside the `<a14:m>` paragraph. Without the fill in
the writer both fail (`left: 0, right: 3`; "the Office Math runs carry
the ink colour").
* Equations: what shows a source as text shows its first 4,096 bytes
The PDF fallback for an equation without a layout capped the number of
lines it printed but not their length: `fallback_lines` scanned the
whole source and handed a complete line to `Page::text`, which formats
it before the page-content limit can refuse it. A source read from a
file is not bounded by `MAX_SOURCE_BYTES` (the editor and the commands
refuse a longer one, but a drawing part may be 512 MiB), so a hostile
file put megabytes into one text run. The sheet painter laid the whole
source out every frame for the same reason.
`cmd::equation::source_excerpt` returns the first `MAX_SOURCE_BYTES`
bytes of a source, cut at a char boundary; a longer source never lays
out, so that is all a reader of the text can use. `fallback_lines`
splits the excerpt, and the sheet painter's dashed frame shows it.
The stored source is left as the file wrote it: the xlsx reader keeps a
`descr` source whole, as it keeps every text box's text, so a file is
not changed by opening it, and `check_caps` refuses an over-long source
by its length before reading it, so opening costs nothing more.
Regression tests: `cmd::print::tests::an_equation_source_of_a_megabyte_prints_a_bounded_excerpt`
(a 1 MiB single-line source: the longest text run in the PDF is 4,096
bytes; without the excerpt it is 1,048,576) and
`tests::equation::the_source_excerpt_is_at_most_the_cap_and_whole_chars`.
* Equations: the layout cache answers for a source over the cap without reading it
`EquationCache::get` matched a source by length, then by a hash of the
whole source (`DefaultHasher`), then in full. For a source over
`MAX_SOURCE_BYTES` the entry is always the same error, yet every lookup
still hashed it: the sheet painter asks the cache every frame and the
PDF export every draw, and a source read from a file can be megabytes,
so a hostile file was rescanned on every repaint.
`get` now answers for a source over the cap at once, by its length,
with `gridcraft_math::SOURCE_TOO_LONG` (one shared value, the same error
`check_caps` returns), whatever the entry holds; `holds` and `store`
neither hash nor copy such a source. Within the cap the lookup is
unchanged. Both callers already route anything but `Some(Ok(_))` to the
4,096-byte excerpt, so the painter and the PDF fallback do no work on
the rest of the source. `Workbook::refresh_equations` no longer carries
a branch for over-cap sources: they are never stale, so it never saw
them. Nothing else reads a source per frame: `source_excerpt` slices at
most 4,096 bytes and `check_caps` tests the length first.
Tests: `equation_cache_tests::a_source_of_a_megabyte_is_looked_up_without_being_read`
(a 1 MiB source looked up 1,000 times: no hash, counted by a test-only
counter in `source_key`, and under 0.5 s; on the previous cache it fails
with `left: 1000, right: 0` after 0.20 s) and
`sources_over_the_cap_are_not_copied_hashed_or_laid_out`.
* Equations: the source cap's message says bytes, which is what is counted
check_caps compares the UTF-8 byte length with MAX_SOURCE_BYTES, so 2,049 two-byte characters are refused although the message said 4,096 characters were allowed. The message now names the unit the check uses.
* Equations: ATTRIBUTION.md records Unicode's terms for the Symbol encoding table
The Unicode values of Symbol's character codes in crates/pdf/src/symbol.rs
come from `symbol.txt` ("Adobe Symbol Encoding to Unicode", table version
1.0, 2011-07-12, at unicode.org/Public/MAPPINGS/VENDORS/ADOBE/), which
carries Unicode, Inc.'s own copyright and terms, not Adobe's Core 14
metrics notice: Unicode grants the right to freely use the information
in the file in products supporting the Unicode Standard, and to copy the
file as long as its notice remains attached, as-is and without
warranties. ATTRIBUTION.md now has a row for that table with those
terms, separate from Adobe's row, which keeps to the AFM character codes
and widths; the module comment of symbol.rs credits both.
* Equations: a literal space in text mode reaches Office Math
RaTeX gives the space in `\text{if } x` as a spacing node whose text is
" ", and the OMML writer's spacing match mapped that to "", which `run`
omits, so the saved equation read `ifx`. The match now maps " " to a
space, and a run that starts or ends with whitespace is written with
`xml:space="preserve"` on its `m:t`, so Word keeps it; the reader never
trimmed, so it reads the run back as `\text{ }`.
Tests: `omml::tests::a_literal_space_in_text_mode_is_kept` (the space is
its own marked run in `\text{a b}` and `\text{if } x`; runs without
surrounding whitespace are not marked) and, through the xlsx reader,
`omml::tests::a_space_in_text_mode_survives_the_round_trip` (the same
number of space glyphs before and after). Without the mapping both fail;
the round trip came back as `\text{i}\text{f}x`.
* Equations: the PDF row says what the writer does with fonts and scripts
docs/file-format-parity.md said PDF export used "only the 14 standard
PDF fonts" and that "non-Latin text does not export". That wording
predates this branch, but the branch added the equation note to the same
row, and both halves were wrong against crates/pdf: the writer has 13
standard-font entries (the Helvetica, Times and Courier faces and
Symbol), none embedded, and sets characters outside WinAnsiEncoding in a
predefined, non-embedded CID font (STSong-Light, Adobe-GB1,
`UniGB-UTF16-H`), so Simplified Chinese displays where the reader
supplies that font, while scripts outside Adobe-GB1 export as code
points and show as the reader's missing-glyph box. The row and the
equation revision row now say so, with Symbol for the Greek letters and
operators it has, the Times faces for letters and digits, and `[?]` for
the rest. "Would pass the cap" reads "would exceed the cap" in the
revision row and the matching code comments.
* Equations: the gate checks nesting that macro expansion builds
`check_caps` counts the braces, `\left`s and `\begin`s of the source
text, so nesting that macros build reaches RaTeX, whose recursion guard
is the one that must hold. `nesting_built_by_macros_is_refused_by_the_parser`
builds `\left(`/`\right)` pairs and `\frac` levels through `\def`: 32
levels lay out, 64 are refused with "Recursion limit exceeded", inside
the 1,000-expansion limit, and the wrapper refuses the 64-level source
as well.
* Equations: the description carries a marker of the Office Math, so an edit made in Excel reads back
The xlsx reader took GridCraft's `gridcraft:latex:` description whenever
a shape had one. Excel edits an equation's Office Math and leaves the
description alone, so a file edited there came back as the old equation,
and the next save overwrote the edit.
The writer now puts a marker of the text body it writes into the
description, `gridcraft:latex:<marker>:<source>`: 16 hex digits of an
FNV-1a 64-bit hash (`crate::omml::body_marker`) over what carries the
equation's meaning, the OMML element names in document order and the
text of the runs. It leaves out what a re-serialisation may change
without changing the equation: property elements (`*Pr`), extension
lists, the DrawingML containers, attributes, namespace prefixes and
whitespace between elements; adjacent runs are joined, so a writer that
splits or merges runs changes nothing, while a text change, an added run
or a structural change does. The hash is computed over the parsed body,
the same way on both sides, and FNV is fixed, so the marker survives
Rust and library versions.
The reader takes the source only when the shape holds Office Math and
the marker matches the body now in the file; otherwise it converts the
Office Math with `equation_source` and drops the stale description,
falling back as before when the maths is outside the subset. A body
without Office Math (the plain-text fallback of an equation outside the
subset) keeps the source as it did, and every branch stays
`ShapeKind::Equation`. A description without a well-formed marker is not
GridCraft's source.
Tests: `office_math_edited_elsewhere_reads_back_as_edited` (an `m:t`
changed in the drawing part reads back converted, and a save writes the
edited equation with the old source gone; with the marker ignored it
fails with `left: "x^2", right: "{y}^{2}"`),
`a_marker_over_garbage_office_math_falls_back_without_panicking`,
`omml::tests::the_body_marker_follows_the_equation_not_its_serialisation`,
and the existing round trips, whose exact sources still come back.
* Equations: the PDF fallback text is clipped to the equation frame
The source lines printed for an equation without a layout were drawn at
full width under the block's clip alone, so a long line ran past the
frame over neighbouring content. The lines are now drawn inside a clip
of the frame, saved and restored around the loop, which nests in the
block's clip.
Test: `cmd::print::tests::the_fallback_text_is_clipped_to_the_equation_frame`
(a 40 by 100 px frame gives a `30 75 re W n` clip before the text run,
restored after it); without the clip it fails at "a clip the size of the
frame".
* Equations: the body marker hashes the properties the converter reads
`body_marker` skipped every `*Pr` element, yet `to_latex` reads several
property values: a run's `sty`, `scr` and `nor`, a fraction's `type`, a
radical's `degHide`, an operator's `chr`, `limLoc`, `subHide` and
`supHide`, a delimiter's `begChr`, `sepChr` and `endChr`, an accent's
`chr`, a bar's `pos` and a group character's `chr` and `pos`. An edit in
Excel to one of them (upright to bold, parentheses to brackets, a
hidden limit) left the marker unchanged, so the stale source won.
The properties the converter reads are one table, `PROPERTIES`, that
both sides use: `val` and `on` read a property only when the table lists
it (a read of an unlisted property is absent, which the conversion tests
would show), and `mark` hashes, for each property element, exactly the
listed properties present as `name=val` in the table's order, closing
the run text gathered so far, so the same property moving to another
run is a change too. `ctrlPr`, extension lists and properties the
converter never reads (DrawingML size, colour and fonts) stay out, so
Excel's re-serialisation still leaves the marker alone.
`the_body_marker_follows_the_equation_not_its_serialisation` now checks
that a changed `sty`, a style moved between runs, an accent or delimiter
character, and a hidden operator limit change the marker, while an
added `ctrlPr`, an extension list, DrawingML run properties and an
unread `rFonts` do not. Without the property hashing it fails at the
`sty` case with two equal markers.
* Equations: the clip test checks the graphics state at a later object
The test for the fallback's frame clip counted `q` lines in two ranges
that always added up, and looked for any `Q` after the text, which the
block's own restore supplies, so a missing `pg.restore()` in the
fallback passed it. It now puts a text box below the equation in the
same block and compares the net `q`/`Q` depth at three points: the
text is drawn one level inside the clip's state, and the later object
at the level before it. With the fallback's restore removed it fails
with `left: 2, right: 1` at the later object.
* Equations: the body marker hashes a styled run as a unit, as the converter converts it
The marker joined the text of adjacent runs so that Excel's splitting
and merging of runs would not change it. The converter, though,
converts each run on its own and recognises function names per run: an
upright `sin` becomes `\sin `, while an upright `s` beside `in`, styled
or not, becomes `\mathrm{s}in` or `\mathrm{s}\mathrm{in}`. An edit that
split a styled run therefore kept the marker equal and the stale `\sin `
source was reused.
A run with a property the converter reads is now hashed as a unit: the
property already closed the text gathered before it, and the run's end
closes its own text as well, so a styled run split in two, or a styled
run merged with a plain neighbour, changes the marker. Runs without
read properties convert character by character and stay joined, so
splitting or merging them still changes nothing. This mirrors the
converter exactly, including adjacent runs with identical properties,
which it converts separately.
`the_body_marker_follows_the_equation_not_its_serialisation` now checks
that upright `sin` differs from upright `s` plus plain `in` and from
upright `s` plus upright `in`, that `sin` + `x` differs from `s` +
`inx`, and that plain `ab` equals plain `a` + `b` beside a styled run or
alone. Without the after-run flush it fails at the first new case with
two equal markers.
* Equations: a brace keeps the script on its other side in Office Math
The writer put the label of `\underbrace{x}_{n}` in `m:limLow` and
that of `\overbrace{x}^{n}` in `m:limUpp`, then returned, so a script
on the brace's other side was dropped: `\underbrace{x}_{n}^{2}` lost
the exponent in the Office Math. The source in `descr` hid this in a
GridCraft-to-GridCraft round trip; an application reading the Office
Math saw an incomplete equation.
The other script now wraps the whole: `m:sSup` around `m:limLow` for
an underbrace with an exponent, `m:sSub` around `m:limUpp` for an
overbrace with a subscript. A brace with only the other script
(`\underbrace{x}^{2}`) gets the script around the bare `m:groupChr`,
with no empty `m:lim`; a brace with its label alone, or with neither
script, is written as before. Each wrapper is a level of the depth
accounting.
The reader converted a limit over a brace into `\underset`/`\overset`
around `\underbrace`/`\overbrace`. It now reads a limit whose base is a
brace alone as the brace with the label as its own script, the
writer's inverse, so `\underbrace{x}_{n}^{2}` reads back as
`{\underbrace{x}_{n}}^{2}`; a limit over any other base is still
`\underset` or `\overset`. The body marker hashes element names and
the properties the converter reads, which this does not change: the
stored source is still taken while the marker matches, and an
equation edited elsewhere still reads back as edited.
Tests: `an_underbrace_with_both_scripts_keeps_its_exponent` and
`an_underbrace_without_a_label_keeps_its_superscript` in the math
crate check the exact Office Math for both braces and for nested ones;
`a_labelled_brace_reads_back_with_a_script_on_its_other_side` checks
the reader on hand-written Office Math and the LaTeX round trip (same
glyphs); `a_brace_with_scripts_on_both_sides_round_trips` saves an
xlsx, reads it with the source (taken as written) and without it
(converted, exponent kept). Without the fix the first two fail on the
missing `m:sSup`, the third reads `\underset{n}{\underbrace{x}}`, and
the fourth finds no `m:sSup` in the drawing.
* Objects: main's chart data table compiles with the object selection and the model's object test
Two leftovers of the merge of origin/main (
|
||
|
|
5579ec9a65 |
Merge the updated Python cells branch (7c5f417a, main 3379f12a) into the Python mode layer
Conflicts: control.rs (main's ui.object.select kept beside editor_json), docs/control-protocol.md (the ui.inspect row keeps this branch's editor text and gains main's selected object; main's ui.object.select row added), ROADMAP.md and docs/ui-parity.md (header lines combined, both sides' rows kept). |
||
|
|
7c5f417a5d |
Merge origin/main (3379f12a) into the Python cells branch
Main gained #470 (floating objects) and #501 (Solver Sensitivity report). Conflicts: - cmd/edit.rs, edit/paste_special.rs, sheet/tabs.rs: #470 adds rewrite_cell_links for chart references beside this branch's rewrite_python_refs; every structural edit, sheet rename and sheet delete runs both, the Python one last where it can refuse the edit (the edit then changes nothing). - cmd/sheet.rs: rewrite() keeps this branch's Result with main's doc comment. - model/src/sheet.rs and engine tests.rs: both sides' additions kept. - ROADMAP.md: header combined, both sides' rows kept. Main's new code writes no cells, so no Python handling was needed beyond these. |
||
|
|
3379f12a56 |
Data: Solver Sensitivity report (#501)
* Data: Solver Sensitivity report Builds on the Answer and Limits reports. reports: [sensitivity] (on data.solver and data.solverReport) writes a Sensitivity report for models solved by Simplex LP or GRG Nonlinear without integer constraints and with a Max or Min objective. Simplex LP: the model is linearised at the solution and solved again, and the report is read from the final tableau: each variable's reduced cost and the range of its objective coefficient over which the basis stays optimal, and each constraint's shadow price and the range of its right side over which that price holds (1E+30 for no limit). GRG Nonlinear: difference gradients at the solution give each binding constraint's Lagrange multiplier (a least-squares fit on the variables inside their bounds, dropping constraints whose multiplier has the wrong sign) and each variable's reduced gradient. The Solver Results dialog offers the report. * Data: Solver Results offers the Sensitivity report only where it applies Integer and binary models, the Evolutionary method and Value Of goals have no Sensitivity report, and choosing it with other reports made the whole report request fail. Solver Results now disables the Sensitivity choice for such models (with the reason on hover) and leaves it out of the request, so the other chosen reports are still written. sensitivity_applies (in cmd::solver) reads the model from data.solver params, as the dialog holds them; a test checks that it agrees with the report for each kind of model. * Data: Solver bounds the nonlinear Sensitivity report's work The Lagrange multipliers of a GRG Nonlinear model are found by least squares over the binding constraints; a constraint whose multiplier has the wrong sign is dropped and the rest are solved again. Each pass costs about 2·k²·f operations for k binding constraints and f free variables, and each pass drops at most one constraint, so a model at the 1,000-constraint limit could take minutes and hold the window. The passes now share a limit of about 10⁹ operations (about a second). Past it the report is refused with a message, before anything changes: the workbook and the Undo history stay as they were, as for the other report refusals. The limit is lowered in tests so the refusal can be reached with a small model. * Data: Solver refuses a Simplex Sensitivity report for a point that is not optimal The Simplex LP Sensitivity report solves the linearised model again to read its final tableau, but shows the final values it was given. data.solverReport accepts any values, so a report could pair values that are not optimal (or not feasible) with the shadow prices, reduced costs and ranges of the true optimum. The report now checks that the given point satisfies every constraint and bound and reaches the optimal objective (both within the constraint precision; another optimal point with the same objective is accepted) and is refused otherwise, before anything changes. * Data: Solver compares the Sensitivity report's objective in both directions The check that a point is optimal compared its objective with the optimum in one direction only, as for an inequality. A point just outside a bound (within the constraint precision) can still have an objective far below the minimum when its costs are large, and that point was accepted. The objective is now compared by the size of the difference, so such a point is refused. * Data: Solver refuses a nonlinear Sensitivity report for a point that is not optimal data.solverReport accepts any finite variable values. The Simplex LP report already refuses values that are not optimal, but the GRG Nonlinear report described whatever point it was given: for example, a minimisation of A1^2 given A1 = 1 found no binding rows and reported a reduced gradient of 2 instead of requiring the optimum at A1 = 0. The nonlinear report now checks, from the gradients it already computes, that the values are a Karush-Kuhn-Tucker point before anything changes: every constraint holds within the tolerance the nonlinear method accepts a solution within, every bound holds within the constraint precision, each variable inside its bounds has a reduced gradient near zero, and a variable at a bound has a reduced gradient that points only out of that bound for the objective's direction. A binding row whose multiplier has the wrong sign is still dropped, and the gradient it would have explained now causes the refusal. The stationarity tolerance is 1E-3 of the largest objective-gradient component (at least 1), well above the reduced gradients left at optima the method solves. * Data: Solver writes the other reports when the Sensitivity report does not apply to the solution Since the nonlinear Sensitivity report refuses values that are not optimal, data.solver with reports that include sensitivity failed as a whole when the run stopped before an optimum (an evaluation limit) or the values were not one, so the solution and the Answer and Limits reports were lost. Solver Results also offered Sensitivity after any solve, and ticking it with Answer after such a run wrote no report. data.solver now writes the Sensitivity report only when the run reached an optimal or converged solution that the report accepts. Otherwise the solve still applies, the other reports are written, and the result lists the report in skippedReports with the reason. Models that have no Sensitivity report (integer, Evolutionary, Value Of) are refused as before. data.solverReport still refuses values that are not optimal; its new skipNotOptimal parameter leaves the report out instead. Solver Results offers Sensitivity only after an optimal or converged status, with the reason on hover, passes skipNotOptimal, and shows the reason when the report is left out. * Data: Solver skips a Sensitivity report in a protected workbook without refusing the call Structure protection is now checked only when a report sheet will be written. A data.solverReport request with skipNotOptimal whose only report is skipped changes nothing, so it succeeds in a workbook whose structure is protected and returns skippedReports; a request that would write a sheet is still refused. * Data: Solver refuses report sheets in a protected workbook before building them A request that must write a report sheet (any Answer or Limits report, or a Sensitivity report that is not allowed to be skipped) is refused before any report is built, so a protected workbook does not run the Limits report's optimisations first. The check after the reports are built remains for a Sensitivity report that may be skipped. * Data: Solver reports Simplex sensitivity only at the vertex it describes The Simplex LP Sensitivity report solves the linearised model again and takes its prices and ranges from that final tableau, but it discarded the vertex the solve returned and showed the values it was given. When the optimum is not unique, another optimal point passed the optimality check, so the report paired those values with the ranges of a different vertex. The report now also requires the given values to match the re-solved vertex within the constraint precision, and refuses another optimum with a message that says so. A Sensitivity report that data.solver or Solver Results may skip is left out instead, as for other points the report does not support. Values from a Simplex LP solve still match, so the usual Solver Results flow is unchanged. * Data: Solver gives a redundant constraint's right side no allowable change The Simplex LP Sensitivity report ranges each right side over the basic values of the real columns only. A row that the other rows make redundant (max x with x = 2 and 2x = 4) keeps its artificial column basic at zero after phase one, and changing either right side alone moves that artificial, so no change has a solution. The report showed 1E+30 and 2 for the first row instead. When a basic artificial has a nonzero entry in the column of the right side being changed, both allowable changes are now 0. The objective-coefficient ranges are unaffected: such a row has no cost and no entries in the real columns, and a cost change does not affect feasibility. * Data: Solver builds the Simplex Sensitivity report's program at the solve's start values The Simplex LP solve linearises the model at the start values, but the Sensitivity report linearised it again at the solution. The difference coefficients then differ in their last bits, and when the optimum is not unique that can break a tie between pivots the other way: the report's solve returned the other vertex and refused the solve's own values. With max 0.1x + 0.1y over a tied edge, more than half of the sampled starting points were refused. The solve and the report now build the program with one function, simplex_program, and the report builds it from the start values it is given (data.solver's start values, which Solver Results passes back as original), so its solve repeats the solve's pivots and returns the same vertex. Start values at which the model cannot be evaluated fall back to the solution, and values that are not the solve's are still refused by the vertex check. * Data: Solver's Sensitivity report sets its noise levels from the coefficients, not the objective's value The figures a Sensitivity report prints as 0 were those below 1E-9 of the objective's value (1E-6 for the nonlinear figures), so a constant in the objective hid real prices: max 1E9 + x with x <= 10 wrote the row's shadow price, exactly 1, as 0, although the constant changes nothing about the optimisation. A shadow price and a reduced cost are combinations of the objective coefficients, so Simplex LP now takes the noise level from the largest coefficient; a reduced gradient is a part of the objective's gradient and a Lagrange multiplier objective per unit of its row's gradient, so GRG Nonlinear takes it from the gradient's size, and the multipliers' sign test compares lambda times the row's gradient size with it. Allowable changes keep the tableau's pivot tolerance as before. * Data: Solver Sensitivity report tests for variables at a bound and for both goals Hand-derived cases pin the report's conventions. Simplex LP: a maximum's variable at its upper bound (reduced cost 1, Allowable Increase 1E+30, Allowable Decrease 1), a minimum with a >= row and a variable at its upper bound (reduced cost -1, Allowable Increase 1, Allowable Decrease 1E+30), and Winston's Dakota Furniture problem, whose variable at zero has reduced cost -5 with Allowable Increase 5 and Allowable Decrease 1E+30, with every shadow price and allowable range worked out from the final basis. GRG Nonlinear: the Lagrange multiplier is the derivative of the optimal objective with respect to the right side and the reduced gradient that with respect to the variable's bound for a minimum (-1) and the negated maximum (+1), and the mirror image of the existing minimum with a variable at its bound (multiplier +2, reduced gradient -4). * Docs: ROADMAP.md records the Solver Sensitivity report The detail documents (docs/gaps.md, docs/target-app-parity.md) record the report, but the roadmap summary did not: its header change note, progress log and revision history now carry it. * Data: Solver's Sensitivity report bounds each figure's rounding noise by its own terms, not the model's largest coefficient or gradient The noise level a figure had to exceed to print was 1E-9 of the largest objective coefficient (Simplex LP) or 1E-6 of the largest gradient component (GRG Nonlinear) for every figure, so an unrelated large coefficient hid real values: max 1E9*x + y with x <= 1 and y <= 1 as rows wrote the second row's shadow price, exactly 1, as 0, and the same model's Lagrange multiplier 1 and a third variable's reduced cost or gradient -1 went the same way. The rounding error of a sum scales with the sum of its terms' sizes, so each figure now has its own bound from the terms it is made of. Simplex LP: a shadow price y_i = sum over the basis rows of c_B(r) * Binv(r, i) is bounded by 1E-9 of those terms' sizes (the row's unit column of the final tableau holds Binv's column i); a reduced cost cf_j - sum of a_ij * y_i by 1E-9 of |cf_j| plus |a_ij| times each price's term sizes, so a price that is a small difference of large terms carries their error into the reduced costs it is used in. GRG Nonlinear: a multiplier solved from its row of the normal equations is bounded by 1E-6 of its own terms (the row's gradient times the objective's, and its coupling terms with the other active rows times their multipliers) over the row's squared norm; the multipliers' sign test uses the same bound, so a row leaves the active set on the criterion the report rounds with; a reduced gradient is bounded by 1E-6 of its own component and the multipliers' terms plus the multipliers' bounds times |g_ij|. Nearly parallel active gradients amplify every multiplier's error beyond a bound local to a row; such multipliers print as computed. A constant in the objective still changes no bound, since none depends on the objective's value. The nonlinear method stops on the 1E9 model from (0, 0) with "unbounded", so its two regression tests ask for the report at the optimum through data.solverReport. * Data: Solver's Sensitivity report tests a nonlinear point's optimality by each variable's and row's own scale, not the model's largest The Karush-Kuhn-Tucker check that refuses a Sensitivity report for a point that is not optimal accepted a reduced gradient within 1E-3 of the largest gradient component, and a row violation within ten times the constraint precision of the largest constant right side. Both scales were the model's, not the figure's: on max 1E9*x + y with x <= 1 and y <= 1 as rows the tolerance was 1E6, so (1, 0.5), where raising y gains 1 per unit, passed as optimal and the report was written for it; on max y - (x - 2)^2 with x <= 1E9 and y <= 1, (2, 1.0001) passed although the second row is over by 1E-4 against its own precision of 2E-5. Each reduced gradient is now tested against 1E-3 of the terms it is made of (the objective's component and the multipliers' parts, at least 1, the same floor as before), and each row against ten times the constraint precision of its own right side. The nonlinear method itself accepts a solution relative to the largest constant right side; a row over by more than its own precision is not satisfied whatever the other rows' sizes, so the report refuses it. The term sizes are shared with the reduced gradients' rounding bound. Two regression tests, each proving the workbook is left as it was and that the optimum is still reported. * Data: Solver refuses a Sensitivity report the model has none of before any report is built or the solve runs The refusals that apply to the model rather than the solution (integer or binary constraints, the Evolutionary method, a Value Of goal) were made inside the report loop, when it reached the Sensitivity report, so a request listing the Limits or Answer report first built those reports for an error: a Value Of model asking for limits and sensitivity ran the Limits report's two optimisations per variable and received only the refusal, and data.solver ran its whole solve before it. prepare_reports now checks the model's refusal before the loop, in both modes (a refusal of the model was already an error in both; only the solution's not-optimal refusal is skipped under skipNotOptimal), and data.solver checks it before the solve. The regression test asks for limits and sensitivity on an integer model, which now gets the Sensitivity refusal instead of the Limits report's, and on a Value Of model, which gets the Sensitivity refusal with no sheet written. --------- Co-authored-by: FredApps <FredApps@users.noreply.github.com> |
||
|
|
f6a0556b9e |
Objects: one selection for every floating object, eight resize handles, and objects that follow edits (#470)
* Objects: one selection for charts, pictures, shapes and slicers, with eight resize handles
The sheet tracked the selected object as a bare id in `selected_chart`, so
only charts and text boxes drew a selection frame, only the bottom-right
corner resized, and the kind of the selected object had to be guessed from
the id. The selection is now `SheetApp::selected_object: Option<ObjRef>`,
with `selected_chart()` kept for the Chart Design tab and the Format Chart
pane.
- Model: `ObjKind` (chart, image, shape, slicer) and `ObjRef { kind, id }`;
`Sheet` helpers find, move and remove an object by `ObjRef` and walk every
object's anchor in a fixed order. `next_object_id` saturates instead of
overflowing on an id at `u32::MAX` read from a file.
- Commands: `object.delete`, `object.move` and `object.setAnchorMode` accept
the four kinds through one parameter check, refuse an unknown kind or id
instead of succeeding silently, and clamp hostile offsets. `object.delete`
replies with the deleted object. Arrange and the Selection Pane include
slicers, which stack above the other objects.
- Sheet: the selected object of any kind draws a frame with eight handles
over every object. Each handle resizes only its own edges, which never
cross; the body moves the object, and the first drag frame already applies
the pointer's movement. The pointer shows the handle's resize cursor or a
move cursor, with the diagonals swapped on a right-to-left sheet, where the
frame moves with its object.
- Keys: Escape drops the object selection; Delete and Backspace keep
deleting the selected object (#213). A selection whose object disappears
(undo, redo, delete, sheet switch) is cleared.
- Double-click: `open_object` routes by kind (a text box edits in place, a
chart opens Format Chart).
- Control protocol: `ui.inspect` reports `selectedObject {kind, id}` and
keeps `selectedChart`; `ui.object.select` selects any object and replies
with its rect. The snapshot example takes `--object <id>`.
Tests: engine tests for every kind by kind and id, the Selection Pane and
hostile parameters; UI tests for selecting each kind (a slicer over a chart
takes the click), all eight handles on four kinds, cursors, Delete,
Backspace and Escape, undo and redo, double-click routing, and resizing on a
right-to-left sheet. Existing tests use the new selection API.
* Objects: every kind follows edits; chart series follow cut-moves and sheet deletes
Structural edits and sorts kept three hand-indexed lists (charts, images,
shapes) in step in two places, and slicers were left out of both, so a
slicer stayed put when rows were inserted above it. Chart series were
rewritten for inserted and deleted lines and for sheet renames, but not for
cut-moves or sheet deletes, which left them pointing at the old cells.
- Inserting and deleting rows and columns and sorting walk
`Sheet::object_anchors` and `object_anchors_mut`, one fixed order over
every kind, so slicers now follow their cells by their anchor mode like
the other objects, and a new kind cannot be forgotten in one of the
passes.
- `rewrite_cell_links` is the one place that rewrites the references
floating objects hold (chart series values, categories and names,
and the source block a chart was made from).
`rewrite_all_formulas` calls it, which covers inserting and deleting
lines and cells and cut-moves on one sheet or across sheets; renaming and
deleting a sheet call it too, so a series into a deleted sheet becomes
#REF!, as a formula does.
- Copying a sheet gives its charts, pictures, shapes and slicers fresh ids,
keeping ids unique across the workbook, which object selection and the
`{kind, id}` commands rely on.
Tests: every kind through row and column inserts and deletes, an absolute
shape, a sort and undo; a slicer through inserted rows; chart series
through a cut-move, a sheet rename, a sheet delete and undo; ids after a
sheet copy.
* Objects: bubble sizes follow edits like the other series references
`rewrite_cell_links` rewrote a chart series' values, categories and name
but not its bubble sizes, so inserting or deleting lines, a cut-move or a
sheet rename left a bubble chart's sizes pointing at the old cells. They
are now rewritten with the rest.
Test: bubble sizes through an inserted row, a cut-move and a sheet rename.
* Objects: a resize handle under the headers leaves the press to them
A selected object's handles reach a few points outside it, and a press
was matched against them before any check of where it landed, so a press
on the column or row header beside an object at the sheet's edge resized
the object instead of selecting the column or row. The cursor already
ignored handles outside the cells.
`chartview::selected_handle` finds the selected object's handle under a
point only over the cell area, as `hit` does, and not while objects are
hidden; the press and the cursor both use it.
Test: presses within a handle's reach over the column header and the row
header neither move nor resize the object.
* Objects: each drag of an object is its own undo step
A drag merges its frames into one undo step by dropping a new step whose
label matches the one before it. Its first frame did the same, so a second
drag of an object (a move after a move, or a resize after a move) merged
into the first drag's "Move Object" step, and one undo took back both.
`Drag::Object` records whether the drag has made its step; only the later
frames of the same drag merge into it.
Test: a move and then a resize of a picture take two undos to reverse,
the first restoring the moved picture.
* Objects: the parity and gap docs no longer list chart resizing as missing
gaps.md (§4), target-app-parity.md (the Essentials tier's work, the
Mainstream chart row, the interaction discount and the Essentials chart
task) and chart-parity.md (the element table and the work list) still
described charts as impossible to resize (#190). Charts move and resize
from eight handles like every floating object, so those statements now
say so; the 2026-10-10 revision rows and the 10-10 re-check stay as
history. Scores and the 40-60 h chart estimate are unchanged (resizing
was 1-2 h, within the rounding).
* Objects: one chart evidence paragraph in gaps.md; trendlines are not missing
gaps.md §4 carried two evidence paragraphs, an older one saying PRs #158
and #161 were open and a newer one saying all 2-D kinds survive XLSX round
trip, both repeating the resize, smooth-line, Switch Row/Column and
trendline statements. It is one paragraph with the current status again.
The Mainstream chart row in target-app-parity.md listed trendlines as
missing; they are read, written and drawn (`Series::trendlines`, the XLSX
chart reader and writer, `chart.addElement`), as chart-parity.md records.
The row now says so and keeps "no axis options". No score change.
* Objects: only the selected object's handles resize; a press elsewhere on an object moves it
A primary drag looked for a handle under the press on whatever object it
grabbed, so pressing near the edge or corner of an object that was not
selected started a resize, while the pointer over that spot showed the
Move cursor (handles are only shown, and only offered by the cursor, on
the selected object). The press now takes its handle from
`chartview::selected_handle`, the same lookup the cursor uses; a press on
any other object selects and moves it.
Test: dragging an unselected shape by its bottom-right corner shows the
Move cursor, then selects and moves the shape without resizing it.
* Docs: ui.object.select says what it replies when it clears the selection
* Objects: a test pins that both anchor accessors visit the same objects in the same order
Sorting and row or column edits read every anchor through
`object_anchors`, compute the new ones and write them back through
`object_anchors_mut` in step. Nothing checked that the two accessors
visit the same objects in the same order; a kind of floating object
reaching only one of them would have paired objects with other objects'
anchors silently.
Test: two objects of every kind are inserted in a mixed order, each
anchor is marked with its position through the mutable accessor, the
marks are read back through the shared one in the same sequence, the
count matches the per-kind collections, and the mark is found on the
object `object_anchor` names by kind and id. The test's constructor
matches on `ObjKind` exhaustively, so a new kind does not compile until
the test covers it.
* Objects: sorting keeps the size of a "move and size" picture whose lines swap places
A picture anchored to move and size with cells and spanning two rows
(or columns) was re-fitted between its top-left and bottom-right lines
after a sort. When the sort put the bottom line above the top one, the
fitted extent was negative and the picture collapsed to a zero height
or width. It now keeps its old size on that axis and travels with its
top-left line, like a "move only" picture.
Tests: for a sort by rows and one by columns, "move and size" and
"move only" pictures inside one line travel with it (the first
re-fitted to the same line, the second at its own size), an absolute
picture stays while the cells move under it, a two-line picture whose
lines swap keeps its size, and undo restores every anchor.
* Objects: a copied sheet's objects get free ids even when a file used the largest id
`Workbook::next_object_id` returned one more than the largest id in use,
saturating at `u32::MAX`. A workbook read from a file can carry an object
with that id; every later insert (a chart, picture, shape, table or
slicer) then got `u32::MAX` again, and so did each object of a copied
sheet, whose ids also came from a saturating counter. Lookups and
selection take the first object with an id, and `object.delete` removes
every one.
`Workbook::free_object_ids(count)` now returns distinct unused ids: those
above the largest in use while they last, then the lowest free ones from
1. `next_object_id` takes the first of them, and the sheet copy takes one
per copied object (and refuses if the id space were ever exhausted). The
model test that recorded the saturation now checks the free ids instead,
and a new one covers a file that used the largest ids: the free ids come
from below, 100,000 of them distinct and in order.
Test: object_ids_stay_unique_when_a_file_used_the_largest_id gives a
shape id u32::MAX, inserts two shapes and copies the sheet; every shape id
is distinct across both sheets. It fails without the fix (both new shapes
get 4294967295).
* Objects: main's chart data table compiles with the object selection and the model's object test
Two leftovers of the merge of origin/main (
|
||
|
|
9a094696fe |
Merge the updated Python cells branch (57d7ea9b, main 25881a8e) into the Python mode layer
Conflicts: theme.rs (main's trace-arrow tokens kept beside the Python tokens), ROADMAP.md and docs/ui-parity.md (header lines combined, both sides' rows kept). |
||
|
|
b78afc9b3c |
Merge origin/main (25881a8e) into the Python cells branch
Conflicts: ROADMAP.md and docs/file-format-parity.md only (header lines combined, both sides' rows kept). |
||
|
|
3bec72f643 |
Docs: Python mode in the formula bar (UI parity, functions, architecture, control protocol, roadmap)
ui-parity.md gains a Python mode row and a shortcut row for Ctrl+Alt+Shift+P with its Mac form. function-parity.md's Python in cells row names Python mode. architecture.md describes Python mode on the shared editor, code_field and its stop keys, and drafts that follow their sheet by its identity. control-protocol.md lists ui.pythonMode, the editor's python flag in ui.inspect, and when ui.inspect leaves a draft out. ROADMAP.md records the progress. |
||
|
|
b6c4500fa7 |
M7: add data tables beneath charts
Add command and UI controls for chart data tables, shared chart and PDF rendering, and native XLSX persistence. Cover live values, blanks, formatting, alignment, undo/redo, bounds and table flags with regression tests. Co-Authored-By: Codex <noreply@openai.com> |
||
|
|
b3a3590db6 | Merge main into Python cells as data | ||
|
|
10b763e7a4 | Merge current main into PR467 and preserve both documentation histories | ||
|
|
bc173ead8f |
Merge PR #3062
# Conflicts: # ROADMAP.md # docs/file-format-parity.md |
||
|
|
7f1574b573 |
Merge PR #3052
# Conflicts: # docs/file-format-parity.md |
||
|
|
6a3aa58a07 |
Merge PR #3043
# Conflicts: # ROADMAP.md |
||
|
|
c03c61fd05 | Merge main into Python cells as data | ||
|
|
60a267b4ef | #3057: select visible workbook views and normalize tab ratios | ||
|
|
f2bef898e8 | #3035: let Esc leave File-page fields before closing | ||
|
|
72b1310a5a | #2765: CSV and text open detect each column's date order | ||
|
|
474472f999 |
Engine: Recently Used follows the formulas entered; Insert Function category list
formulas.recentlyUsed scanned the session journal for cell.set inputs. It missed formulas entered by range.* commands, lost them when the journal dropped its oldest entries (a large file open or a long script is enough), and started empty in every session. The Function Library's Recently Used menu and the Insert Function dialog list what it returns. - Prefs::recent_functions (a new engine module, recent.rs) holds the ten functions most recently entered in a formula, newest first. Session::execute updates it for every cell.* and range.* command it journals, from any source (the interface, the control channel, MCP or a script). It reads at most 2,000 of the formulas a command enters, starting from the last one; formulas longer than Excel's limit are skipped. formulas.recentlyUsed returns it. It never reads the journal back, so the journal's caps do not affect it. - The list is kept with the other engine preferences (prefs.json, the browser's storage on the web), so it survives a restart as Excel's does. It is read leniently: a malformed value loses only this list. A fresh install starts from common functions. The Insert Function dialog is titled "Insert Function", as in Excel (it said "Formula Builder"), and gains a working category list: Most Recently Used, All, then each category in Excel's order. A category may be given by label or by engine id, so a Function Library menu or the control channel (ui.dialog.set) opens it on one, and a new search switches to All. The dialog keeps its translated labels and its search in the interface language. Each row shows the function's name and its category in two columns; the category column starts after the widest name shown, so a long name in any font or formula language stays clear of it. A function counts as recently used once its formula is entered, not when OK is pressed. The unused "Formula Builder" translations are removed. Tests: engine unit tests for reading functions from formulas (strings, unknown names, over-long and malformed formulas), the formulas a command enters (newest first, within the budget), the ten-name list and the lenient read of prefs.json; tests/it/recently_used.rs for cell.set and range.setValues from the session, a failed command, a cleared journal and a save and reload of the preferences. UI tests for the menu's order and the dialog's title, categories, engine-id category, search and OK, and for the category column with a wide font. |
||
|
|
6aa5a78828 |
UI: Formulas › Function Library as labelled buttons with category menus
The Function Library group was a big Insert Function button followed by AutoSum and seven big category buttons that all drew the same icon, the widest group on the Formulas tab. It is now laid out as Excel lays it out when the window is not wide enough for a big button per category: big Insert Function and AutoSum buttons, then the categories as labelled small buttons in columns of three: Recently Used Text Math & Trig Financial Date & Time More Functions Logical Lookup & Reference - The category buttons draw a new code-drawn icon, Icon::FnBook, a book whose spine and glyph (star, coins, question mark, letter, clock, magnifier, theta, dots) take the category's colour from the theme's category palette. icons::paint_accent draws an icon with an accent colour, and widgets::small_button_accent is a small button that uses it; small_button is unchanged for its callers. - AutoSum stays the big split button: the icon inserts SUM, the label and arrow open the AutoSum menu, which now reads Sum, Average, Count Numbers, Max, Min and More Functions… and is shared with Home › Editing. Its tooltip names the control only, so the shortcut comes from the platform lookup (Alt+= on Windows and Linux). - Each category menu lists its functions with their descriptions on hover and ends with Insert Function…, which opens the dialog with that category. More Functions cascades into Statistical, Engineering, Cube, Information, Compatibility and Web. Recently Used lists what the engine's formulas.recentlyUsed returns. - The function categories, the function list (built once) and the menus live in a new function_library module; the dialog's category labels come from it. - keytips.json gains MR for Recently Used, Excel's sequence. Labelled small and big buttons report their label to accessibility. - The one-line labels (Date & Time, Lookup & Reference, Math & Trig, More Functions) replace the two-line keys in every interface language, with the same words. Recently Used is translated in Czech, German, Spanish, French, Japanese, Korean, Portuguese, Russian and Chinese; the other languages show it in English, their fallback. Tests: tests/it/function_library.rs covers the layout (columns of three level with the tab's other columns), each category menu and its Insert Function… row, the More Functions cascade, the AutoSum split button and its menu, and the MR key tip. A ribbon unit test keeps Command-key glyphs out of the ribbon's text. chart_resize picks the chart menu's Delete now that Home › Cells' Delete has an accessible name. |
||
|
|
2329904024 | docs: tidy wording | ||
|
|
5f7137ca01 | docs: tidy agent, contributor and asset docs | ||
|
|
9587f08253 | Merge upstream main into gc-xls-cell-styles | ||
|
|
0e643e9026 | Merge remote-tracking branch 'upstream/main' into gc-xls-cell-styles | ||
|
|
1d7206ebdd |
Docs: Python cells as data (architecture, file formats, functions, roadmap)
architecture.md gains a Python cells section: the model type, the text forms and the size limit, the xlsx Python part, calc laying out kept results as graph nodes, and the limits on open. file-format-parity.md gains a Python cells table (xlsx as [MS-XLSX] 2.3.10 specifies, the formula naming its code in the Python part, with cached values and spills; other formats; GridCraft JSON; the limits on open; and rich-value object results as not kept). function-parity.md marks the four Python errors done with their ERROR.TYPE numbers and adds a Python in cells row (stored, not run). ROADMAP.md records the progress. The return types are written as "0 or 1" so no table cell holds an unescaped pipe. architecture.md's "Last updated" label loses the space inside its bold markers. |
||
|
|
c801e50e40 |
Merge #2152 (f8e1f8df, with main at f6b0cec6) into #478
# Conflicts: # ROADMAP.md # crates/engine/src/io.rs # crates/ui-egui/src/dialogs.rs # crates/ui-egui/src/lib.rs # docs/target-app-parity.md # docs/ui-parity.md |
||
|
|
f8e1f8dfc0 |
Merge #2145 (4d26e395, with main at f6b0cec6) into #2152
# Conflicts: # crates/engine/src/io.rs # crates/engine/src/lib.rs # docs/target-app-parity.md |
||
|
|
1cd45cc909 | Merge branch 'main' into pr/macros-ui | ||
|
|
b552b7a929 | Merge branch 'main' into pr/macros-record | ||
|
|
f102fe29a1 |
Merge g3 into g4
# Conflicts: # ROADMAP.md # crates/engine/src/cmd/edit.rs # crates/engine/src/cmd/formulas.rs # crates/engine/src/cmd/inspect.rs # crates/engine/src/cmd/pivot.rs # crates/ui-egui/src/lib.rs # crates/ui-egui/src/tabs.rs # docs/gaps.md # docs/ui-parity.md |
||
|
|
015b9abe5b |
Merge g2 into g3
# Conflicts: # Cargo.lock # Cargo.toml # ROADMAP.md # apps/gridcraft/src/main.rs # crates/ui-egui/src/lib.rs # docs/ui-parity.md |
||
|
|
929495e274 |
Merge g1 into g2
# Conflicts: # ROADMAP.md # crates/engine/src/cmd/extra.rs # crates/engine/src/lib.rs # crates/engine/src/tests.rs # docs/control-protocol.md # docs/ui-parity.md |
||
|
|
903bece86f |
Merge upstream main into g1
# Conflicts: # ROADMAP.md # crates/engine/src/tests.rs # crates/ui-egui/src/lib.rs # docs/ui-parity.md |
||
|
|
2fbbed33bc |
Merge pull request #783 from vangel76/fix/control-auth-777
Security: the --control server requires an auth token and closes bad connections (#777) |
||
|
|
0665476b35 |
Merge pull request #2493 from Harrison-Booth:feat/options-iteration-fields
# Conflicts: # docs/function-parity.md # docs/ui-parity.md |
||
|
|
802c782360 |
AutoSum: a selected row gets its total to the right, as in Excel
AutoSum over any selection of more than one cell wrote a total under each column, so a row selected (B2:E2) got four one-cell totals in row 3 instead of =SUM(B2:E2) in F2. As Excel does, totals now go in the selection's empty last row or column, and otherwise past it: - a single row: its total in its last cell when that is empty, else just to its right; - a block whose last column is empty: each row's total there; whose last row is empty: each column's; both: both, and the block's total in the corner; - otherwise totals under each column, as before. A whole-row selection still stops at the data's last column, and all totals are one undo step. Tests: autosum_over_a_row_totals_it_to_the_right, autosum_over_a_whole_row_totals_it_after_the_data and autosum_fills_a_selections_empty_last_column_and_row; each fails without the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1443ec417c |
UI: macro step editor with a debug bar and grid overlays
New Macro (Automate › Macros) and Step Into, Edit and Create in the Macros dialog open a resizable step editor. It works on a draft of the macro and saves it with macro.save. The step list shows each step's number, a breakpoint dot in the gutter (click to set or clear), a category icon, a summary, an error marker and an enabled checkbox; Repeat bodies and If branches are indented under their step, and rows drag to reorder within their list. Add Step opens a command picker (control steps first; commands a macro may never run are left out) and fills in the command's required parameters; Duplicate and Delete act on the selected step. The inspector's Details tab edits the selected step. Reference parameters are reference boxes that take a typed reference or a selection on the grid, with a Rel/Abs switch between a token relative to the example input and an absolute reference, and token suggestions. The Inputs tab sets each input's shape, header and column types (Use current selection as example infers them); the Output tab sets the anchor and the conflict rule. The debug bar has Check (a dry run that colours the grid: inputs blue, writes green, writes over data red), Run, Step, Continue, Run to Here and Stop, which asks whether to keep the changes or undo them. The debugger receives the editor's breakpoints with breakpointsOnly, so a dot cleared while paused takes effect at once; the current step is selected, its parameters are shown as they will run, and its target is outlined on the grid. While a macro is debugged, the status bar says so and Show reopens the editor. List drag and drop lives in a small dnd module. The debugger icons are drawn in code, and the breakpoint and sheet overlay colours are theme tokens covered by the contrast test. |
||
|
|
5512c1a269 |
UI: Automate › Macros group, Record Macro and Macros dialogs, recording status, macro shortcuts
The Automate tab gains a Macros group: Macros opens the Macros dialog, Record Macro opens the Record Macro dialog (name, Ctrl+Shift shortcut letter, description, Use Relative References) and turns into Stop Recording while recording, Use Relative References sets how the next recording starts, and Run Macro runs the macro run last; its menu lists the recent macros, then every macro. While recording, the status bar shows a red dot, "Recording · N steps" and Stop. The Macros dialog (Automate › Macros, View › Macros, Alt+F8) lists the workbook's macros with their shortcuts, then its Automate scripts, and says when the file carries a VBA project, which is kept and not run. Run, Delete (which asks first) and Options (shortcut letter and description) act on the selected row. The dialog keeps the result of view.macros, so the scripts and the VBA entry are listed as before. A macro that needs approval asks with Run, and one whose output would land on data asks with Overwrite, Shift Down and Shift Right; each button runs the command again with its answer. Ctrl+Shift+letter (Cmd+Shift on a Mac) runs the macro with that shortcut; the letters GridCraft uses keep their meaning. The View › Macros keytips (WMR, WMV) now open the Record Macro and Macros dialogs. Icons for macros, recording and relative references are drawn in code, the recording dot is a theme token covered by the contrast test, and large and small ribbon buttons and icon buttons carry accessible names. |
||
|
|
b40fa4e065 |
Merge #2145 (2fb31f11, with main at 17b9f6ff) into #2152
Main refuses window.* steps in scripts, so the recorder test's script step that a macro may not run is app.commands. |
||
|
|
f65e9f6bed |
Merge upstream main into the batch
# Conflicts: # crates/ui-egui/src/formula_bar.rs # crates/ui-egui/src/ribbon.rs # crates/ui-egui/tests/it/main.rs |
||
|
|
e1a12fa337 |
UI: theme tokens with a contrast test, ribbon overflow arrows and split button, multi-line formula bar, code editor widget (#456)
* UI: theme tokens for icon, reference and sheet-control colours; WCAG contrast test
Tokens gains the colours the chrome was hard-coding, each with a light
and a dark value: on_accent (text on an accent fill), an eight-colour
category palette (theme::Cat), the paper colour inside icons, reference
colours for the formula bar and for the sheet, the message box warning
disc, the tab bar scroll thumb, and sheet_control_* colours that follow
the sheet and so stay light in both themes.
What now comes from the tokens:
- icons: paper (white pages glared in dark mode) and the accent
constants; icons placed on the sheet use icons::paint_on_sheet, which
keeps the light colours;
- reference colours (editor::REF_COLORS), which were hard to read on the
dark formula field;
- white text on the accent in OK, Solve, slicer tiles and the AutoSave
switch, 2:1 on the dark accent;
- the comments pane (a white card with light text in dark mode), gallery
hover and swatch outlines, the message box disc, the tab bar thumb,
chart selection frames, and the sheet's filter and validation buttons.
A unit test (theme::contrast_tests) checks every token pair used by the
ribbon, popups, dialogs, panes, formula bar, status bar and sheet in
both themes against WCAG 2: 4.5:1 for text and 3:1 for glyphs. It caught
the dark danger red (3.5:1 on hover) and the selected header text (4.1:1
light), which are adjusted.
* UI: ribbon scroll arrows, large split button for Formulas › AutoSum
- widgets::big_split_button: the icon runs the default action, the
label and its arrow open the menu. It returns both responses, with
accessible names ("AutoSum" and "AutoSum options"). big_button shares
its label layout.
- Formulas › AutoSum uses it: the icon inserts SUM as before, and the
arrow, which used to do the same, opens the Sum / Average / Count /
Max / Min / More Functions menu that Home › AutoSum already had
(shared as autosum_menu). Its keytip is unchanged.
- When a tab's groups are wider than the window, arrows at the card's
edges scroll them; before, only the mouse wheel did and nothing showed
that the tab went on. ribbon::RibbonOverflow records the groups' and
the card's sizes each frame.
- A test checks every tab fits the card's three rows. It found View ›
Show stacking four checkboxes, with Headings cut off; Show is now two
columns (Ruler, Gridlines / Formula Bar, Headings).
Tests: tests/ribbon_layout.rs (the split button's two parts, AutoSum's
icon and menu, the scroll arrows, the card height).
* UI: formula bar grows from one to five lines; code editor widget
The formula bar is a top-aligned row: the Name Box and the cancel,
enter and fx buttons stay on the first line while the field grows. Its
height is the content's laid-out line count (Alt+Enter or wrapped text)
times the font's row height, from one line (30 px, as before) to five,
then the field scrolls with the caret in view. This holds while editing
and while viewing a cell. The height eases over 120 ms when the line
count changes, and the chevron pins it at five lines (it used to give a
fixed 72 px). The in-cell editor likewise stops at five lines and
scrolls. formula_bar::BarLayout records where the parts went, for tests.
widgets::code_editor(ui, id, &mut text, Lang, max_lines) is a small
code editor for features that edit code: the monospace font, syntax
colours from Tokens::syntax (new, with light and dark values in the
contrast test) for Python (keywords, strings including triple-quoted
ones, numbers, comments and builtins) and LaTeX (commands, scripts and
comments), Tab inserting four spaces (or indenting every selected line,
Shift+Tab dedenting), Enter keeping the line's indentation (one level
more after a Python colon), and a scroll area that follows the caret.
The tokenizer and the edits are pure functions with tests, including
hostile input that must keep spans on char boundaries.
kittest (tests/formula_bar.rs) covers Alt+Enter up to eight lines with
the Name Box and buttons fixed and the caret visible, the commit,
viewing the committed cell, the chevron, the ease, the in-cell cap and
the code editor's Tab and Enter.
* UI: Shift+Tab in the code editor dedents instead of indenting
The editor consumed plain Tab before Shift+Tab, and egui's key match ignores Shift
when the pattern lacks it, so a Shift+Tab was taken as Tab and indented the line.
Shift+Tab is now consumed first; a plain Tab still inserts spaces.
* UI: the ribbon card stays inside a narrow window, so its right scroll arrow is on screen
The row above the ribbon can be wider than a narrow window, and the card took that width, so the
right scroll arrow was drawn past the window's edge and couldn't be clicked: below about 640 px on
macOS, and at 700 px with Linux's wider default fonts, where CI's ribbon test failed. The card is
now kept within the window with the same margin it has on the left.
* UI: each ribbon tab keeps a scroll offset of its own
The ribbon's scroll area used one identity for every tab, so scrolling an overflowing tab and
switching to another overflowing one hid that tab's first groups. Each tab now has its own scroll
state: a tab opened after another was scrolled starts at its left edge, and a tab keeps its offset
while others are used.
* UI: the in-cell editor keeps its whole width for the text and shows every line
Merged with main, the grid's own test asked the in-cell editor to wrap its
text at the field's full width and to show every line (the editor never
scrolls); the editor widget wrapped it inside the formula bar's scroll area,
which took the bar's width off the text (168 px tall where 156 was right),
and the grid capped it at five lines. The in-cell editor now shows the text
field on its own, through the one show_edit helper the formula bar also
uses, and grows with every line; the formula bar keeps its five lines and
its caret scroll. The branch's five-line test for the cell editor becomes a
grows-with-every-line test.
* Docs: the UI building blocks' revision rows carry the date they land
The rows said 2026-10-11, older than upstream rows above them, and the
ui-parity row said the in-cell editor is capped at five lines.
* UI: the formula bar's view of a cell starts at the cell's top
The bar's read-only view kept one scroll offset for every cell, so a cell
shown after scrolling another's text started scrolled. The offset is kept
per cell now.
* UI: the editor's field does nothing with Enter on its own
The editor handles Enter (commit) and Alt+Enter (a line break) itself, and a
message box over the editor takes Enter to close. The field under it is a
multi-line TextEdit since the bar grew, whose own return key would add a
line if it ever saw Enter; return_key(None) keeps that from depending on
where focus sits. A test shows Enter over a message box adds nothing to the
entry (focus is off the field then, so it passed before too: this is
hardening, not a repair). The doc comment split by the show_edit helper is
mended.
* Docs: ui-parity says the in-cell editor shows every line, and its revision table is whole
A sentence still said the in-cell editor stops at five lines, and a blank
line between the table's separator and its first row ended the table.
* UI: the split button's tooltip carries the shortcut hint like every ribbon button's
big_split_button built its tooltip with the bare translation, so AutoSum,
which became a split button, lost its shortcut hint. It uses tip_text now;
the split-button test hovers the button and looks for the hint.
* UI: a selection that ends at the start of a line leaves that line alone
Selecting "a\n" in "a\nb" and pressing Tab also indented b, and Shift+Tab
could take its indentation: the line after the selection was counted as
touched. It is touched only when the selection reaches into it.
* UI: the code editor applies its keys in input order, with the typing around them
The editor consumed Enter, Tab and Shift+Tab before the text field saw the
frame's events, so a frame carrying typed text followed by Enter (events
gather between repaints, and under key repeat) put the newline before the
text, and two Enters in one frame made one line. The editor now walks the
frame's events in order and applies typed text, pastes, Enter, Tab,
Shift+Tab, Backspace and Delete itself, taking them from the queue; the
first event it does not handle and everything after it stay for the field,
which runs next, so the order holds either way.
* UI: the code editor keeps the input order past the keys the text field handles
The editor applied its keys up to the first event it did not handle and
left that event and everything after it to the text field, which has
its own Tab and Enter: select all followed by Tab in one frame replaced
the code with a tab character, and since the platform sends a key event
before each text event, an Enter after ordinary typing lost its
indentation.
The editor now applies its keys up to the first event the field acts on
(a caret or selection key, a key under a command modifier, IME, a click);
that event and the run after it stay for the field; from the editor's
next key on, the keyboard events are held back under the editor's id for
its next pass, where it puts them ahead of that pass's input and splits
again. They go only to the editor they were typed into, only while it
still has the focus, and only in the pass right after; otherwise they
are dropped rather than typed into another widget. Printable key presses
do not split the stream. Tab under a command or option modifier is taken
off the queue, and Enter under Shift or Option is the editor's newline,
so the field never gets a Tab or an Enter of a focused editor.
* UI: the ribbon layout and formula bar tests join the crate's integration test binary
Since #2372 the crate's integration tests are one binary built from tests/it/, one module per file; only tests that set process-wide state stay separate. These two set none.
* UI: the formula bar's one-line height follows the font, so five lines fit on Windows
One line was a fixed 30 px and the bar grew by one row height per line.
The panel's and the field's margins take 12 px of that, and rounding to
the pixel grid up to another, so a row taller than 17 px lost the last
counted line: with the Windows UI font (17.28 px rows) five lines showed
four, and bar_grows_to_five_lines_then_scrolls_with_name_box_pinned
failed there.
formula_bar::one_line(row_h) is 30 px unless the rows need more, and
every counted line fits whatever the font. The kittest heights are
computed from it rather than from 30, and a unit test checks the
arithmetic for rows of 15.6 to 20 px.
* UI: Paste Special's OK button uses the on-accent colour
It was the one OK button still drawn white on the accent: 2.14:1 in the dark theme, where every other OK uses on_accent (7.1:1).
|
||
|
|
6194f8241d |
Merge storytold/gridcraft#2522: Charts: 100% Stacked Area draws each value as a share of its category's total
# Conflicts: # ROADMAP.md |
||
|
|
1713334579 |
Merge storytold/gridcraft#2477: Sheet tabs: drag the handle at the scrollbar's left edge to give the tabs more or less room
# Conflicts: # ROADMAP.md # docs/ui-parity.md |
||
|
|
cb1fb89549 |
Merge storytold/gridcraft#2467: PivotTables: right-clicking a value cell offers Show Details in the cell menu
# Conflicts: # ROADMAP.md # crates/engine/src/cmd/pivot.rs # docs/ui-parity.md |
||
|
|
75f7df9da6 |
Merge storytold/gridcraft#2461: PivotTables: Enable show details turns Show Details off for a PivotTable
# Conflicts: # ROADMAP.md |
||
|
|
8356b33c78 |
Merge storytold/gridcraft#2456: Fill handle: Fill Days, Fill Weekdays, Fill Months and Fill Years for a date
# Conflicts: # ROADMAP.md # docs/ui-parity.md |
||
|
|
2741680144 |
Merge storytold/gridcraft#2427: Keyboard: Ctrl+Shift+U expands and collapses the formula bar instead of underlining
# Conflicts: # ROADMAP.md # docs/ui-parity.md |