Commit Graph

538 Commits

Author SHA1 Message Date
Alexander Alber 32d0a866f9 XLSX: write one cfvo per icon in icon-set rules (#235)
An icon-set rule without stored thresholds (every rule made by
home.conditionalFormat, and the sales sample) was written as an empty
<iconSet>, which CT_IconSet forbids (cfvo minOccurs 2); openpyxl drops it.
The writer now always emits one cfvo per icon: the stored thresholds when
there is one per icon, the lower bound prepended when it is missing, else
Excel's defaults (percent 0/33/67, 0/25/50/75, 0/20/40/60/80).

Evaluation read the thresholds as one fewer than the icons, but files
carry one per icon (the first is the lower bound), so thresholds read from
a file were ignored. Both now share icon_count and default_icon_thresholds
in gridcraft-model; icon_count is clamped to 3..=5 so a set name like
"0Foo" can't underflow k - 1.

generated with Claude Code
2026-10-11 09:26:00 +02:00
Bryan Mark 5967b5656f Glide the selection outline to a new selection; View > Animations turns it off
Excel's cell cursor doesn't jump: press an arrow key or click a cell and the
border slides over; extend the selection and it grows. The outline of the
current area now does the same, in 0.12 s with a decelerating curve, from
wherever it is drawn (a held arrow key reads as one motion), with its fill
and fill handle. The timing is an estimate, not yet measured against Excel.

The same cells moving on screen (scrolling, zoom, a resized column), another
sheet or workbook, an outline that wasn't drawn the frame before, and moves
that start or end off screen don't glide. Only painting waits: the
selection, hit testing and the cell editor use the real cells at once.

View > Animations (under Display theme) turns it off; view.animations {on?}
and ui.set {animations?} do the same for agents. Saved with the UI
preferences, on by default; translated in the five catalogs.

Scripted pictures never catch an outline in flight: it stays on its cells
while a ui.screenshot is on its way and when egui's animations are off (the
headless harness, so the offscreen snapshot). The marching ants around a
copied range now advance by the clock instead of once per painted frame,
since a glide repaints every frame and made them lurch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:18:29 +09:00
AlissonFA 9518b041d5 M2: fix cell vertical alignment positioning and add keytips
Closes #241
2026-10-11 04:18:16 -03:00
Bryan Mark a721b087a1 Add arcanii to the contributor names
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:15:57 +09:00
Bryan Mark 4517d8be65 Leave the active cell unshaded inside a selected range
The active cell of a selected range was shaded like the rest of the range:
the line meant to clear it painted a fully transparent rectangle, which
changes nothing. The fill now goes around the active cell (the whole merged
cell when it is one) in every area of the selection, so it is plain where
typing goes. A lone active cell among several areas isn't shaded at all,
and neither is a single selected merged cell.

The fill is translucent, so the cell can't be cleared by painting over it:
each area is shaded as up to four rectangles around it that never overlap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:15:57 +09:00
Sebastian Whincop cf1696aff3 Add MacJedi42 to the contributor names
Adds my own entry to contributors/people.toml, as docs/contributors.md asks: my GitHub
username with `sync_github_name = true`.
2026-10-11 17:36:25 +13:00
nihilist-narwhal 6e31ad9ea8 Typed times of 24 hours or more keep their hours
Typing 25:00 or 36:30:15 gave the right value but the h:mm format, so
the cell showed 1:00 and 12:30:15. Excel formats such entries as
[h]:mm:ss (25:00:00).
2026-10-10 22:52:59 -05:00
nihilist-narwhal ce83871f2b Remove Duplicates: compare the text cells show
Excel decides duplicates by what a cell displays, not the value stored:
the same date shown as 3/8/2006 and as Mar 8, 2006 is two values, while
1.01 and 1.02 both shown as 1.0 are duplicates. Remove Duplicates
compared stored values, so it did the opposite in both cases.
2026-10-10 22:52:59 -05:00
nihilist-narwhal 6fd41d626b Typed dates before 1900 stay text
7/4/1776 was entered as 7/4/3676 and DATEVALUE("12/31/1899") gave
693962: the text parser built dates with DATE()'s rule that adds 1900 to
years below 1900. Excel keeps such entries as text, and DATEVALUE gives
#VALUE!.
2026-10-10 22:52:59 -05:00
nihilist-narwhal 5e22f9b373 Sort: case-sensitive order puts lowercase before uppercase
With Case sensitive checked, Sort compared text by code point, so every
uppercase word came before every lowercase one (A, B, a, b). Excel
compares ignoring case first and only then puts lowercase before
uppercase: a, A, b, B.
2026-10-10 22:52:59 -05:00
nihilist-narwhal a8a4c5dab3 Conditional formatting: accept Excel's operator names, refuse unknown ones
home.conditionalFormat took any operator it didn't know as "greater",
so a Cell Value rule given Excel's own names (lessThan, greaterThan,
lessThanOrEqual, greaterThanOrEqual, as in the file format and VBA)
silently became "greater than". Those names are now accepted, as data
validation already does, and anything else is an error.
2026-10-10 22:52:58 -05:00
nihilist-narwhal 060889afb2 CHOOSE: pick element by element when the index is an array
The evaluator took a single index, so CHOOSE({1,2},"a","b") gave "a"
and VLOOKUP(x,CHOOSE({1,2},B1:B3,A1:A3),2,FALSE) gave #REF!. An array
index now goes through the library CHOOSE, which lifts over its
arguments as Excel does.
2026-10-10 22:52:58 -05:00
nihilist-narwhal d75b6b73b4 Criteria: text patterns don't match blank cells
COUNTIF(range,"*") counted empty cells and COUNTIF(range,"<>*") skipped
them. Excel matches text patterns against text only: an empty string is
text, a blank cell is not.
2026-10-10 22:52:58 -05:00
nihilist-narwhal 4e7379bd68 Conditional formatting: cell value rules on blank and error cells
Excel compares a blank cell as 0 (or empty text) in Cell Value rules,
so "less than 5" or "equal to 0" also formats blank cells, and a
cell holding an error never matches (the comparison is an error).
GridCraft skipped blank cells and matched errors as if they sorted
above every number ("greater than 5" formatted #N/A).

The No Blanks rule also counted a cell holding only spaces as not
blank, while the Blanks rule (like Excel's LEN(TRIM())) counted it as
blank, so such a cell matched both.
2026-10-10 22:52:58 -05:00
nihilist-narwhal 2b1f851d0a Conditional formatting: apply every matching rule's format
Only the first matching rule's format was used, so with a light red
fill for >300 and a red border for >500, 510 showed only the border.
Excel applies every matching rule; where two rules set the same part
(fill, font colour, a border edge…), the rule with the higher priority
wins, and Stop If True still ends the list.
2026-10-10 22:52:58 -05:00
Diego Ospina cbf56f0707 i18n: Spanish interface translation
Language::Es was registered and switches formulas to Spanish (SUMA, decimal comma, semicolon
arguments), but every interface label still fell back to English. This adds es.rs with all 266
labels in Excel's Spanish wording (Combinar y centrar, Inmovilizar paneles, Análisis de
hipótesis, Buscar objetivo, Rastrear precedentes…) and covers Spanish in the i18n tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012JC2QZ1Qc5KQMBswS38GKy
2026-10-10 22:36:49 -05:00
Sebastian Whincop aee898595f Accuracy: round an exact tie at the 16th significant digit toward zero
Excel shows 15 significant digits. When a number is an exact tie at the 16th
(its exact decimal expansion is 15 digits, a 5, then only zeros), Excel
rounds it toward zero: TEXT(1234567890123455,"0") is 1234567890123450.
GridCraft took Rust's `{:.14e}`, which rounds that tie to even, so any tie
with an odd 15th digit came out one unit too high (…460).

decimal_digits now formats 16 digits (correct unless the 16th is a 5),
and only on a 5 checks the exact expansion (a 30-digit screen, then the
full one) and truncates a true tie. Non-ties, including values just above
or below a tie, round as before. One format serves almost every number.

Tests: ties of every 15th-digit parity, negatives, fractional ties, the
neighbours just above and below a tie, the carry after fifteen 9s
(999999999999999.5 shows 999999999999999), and unchanged ordinary cases.

Evidence: Windows Excel 16.0.20430 TEXT() over a 174-row probe and a 24-row
carry probe agree on every row except those under a percent format and
General over negatives (the two bugs named below). The suite's number-format
golden goes from 183,843 to 184,900 of 187,810 passing with no row that
passed before failing now.

	modified:   crates/numfmt/src/decimal.rs
	modified:   crates/numfmt/src/tests.rs
2026-10-11 16:22:36 +13:00
webdevtodayjason fae90c8e4a selection.move: clamp extreme dr/dc to the sheet edge instead of overflowing 2026-10-10 21:05:58 -05:00
pkibiego 2f078962a0 xlsx: bump quick-xml to 0.41 for the namespace-declaration DoS (RUSTSEC-2026-0195)
The ODS and XLSB readers parse untrusted package metadata and content through
`quick_xml::NsReader`. In 0.38.4 that is RUSTSEC-2026-0195 (CVSS 7.5): the namespace
resolver pushes a binding for every `xmlns` / `xmlns:*` declaration on a start tag, with
no cap on how many there are, and `NsReader::read_event` reaches that path - not only
`read_resolved_event`.

None of our caps bound it. MAX_PART (512 MiB), MAX_TOTAL (1 GiB), MAX_ENTRIES,
MAX_CELLS, MAX_TEXT and MAX_DEPTH bound part size, entry count, cells, text and nesting,
never declarations per tag.

Measured with `gridcraft-cli info` on crafted .ods files, before this change:

     754 B, 0 declarations  ->    8.6 MB, exit 0
     24 KB, 1,000,000       ->   52.6 MB, exit 0
    187 KB, 8,000,000       ->  367.8 MB, exit 0
    770 KB, 33,000,000      ->    1.46 GB, exit 0

About 45 bytes of resident memory per declaration, roughly 1,900x the file size, and no
error at all. At MAX_PART a ~1 MB file reaches ~1.9 GB, and every part is held in memory
while the package is read. Both readers share the path, so a downloaded .ods or .xlsb
was enough. Nothing panicked, so `Session::execute` never saw it.

0.41.0 caps declarations per start tag at 256 (DEFAULT_MAX_DECLARATIONS_PER_ELEMENT,
raisable through NamespaceResolver::set_max_declarations_per_element) and returns
`NamespaceError::TooManyDeclarations` beyond it. The same fixtures after:

     754 B ->    8.6 MB, ok         24 KB ->   19 MB, error
    187 KB ->  101.8 MB, error     770 KB ->    397 MB, error

with "start tag declares more than 256 namespace bindings". The residual peak is the
part itself: MAX_PART still lets one part decompress to 256 MiB, the existing intended
bound.

API changes for 0.41 (0.x bumps may break; all four are in the xlsx crate):
- `resolve_element` and `resolve_attribute` moved off `NsReader` on to the resolver:
  `reader.resolver_mut().resolve_element(..)`. `attr` and `tag` in ods.rs take
  `&mut NsReader` for that.
- `Attribute::unescape_value` is deprecated in favour of `normalized_value`, which also
  applies the attribute-value normalization the XML specification requires.
- `BytesText::xml_content` now takes the document's XML version.

This also clears RUSTSEC-2026-0194, the quadratic duplicate-attribute check. xml.rs
already passed `with_checks(false)`, but ods.rs and xlsb.rs used the checked iterator, so
they were on the quadratic path as well.

Regression test: `ods_tests::ods_namespace_declaration_bomb_is_rejected` asserts that a
tag with thousands of declarations is an error and that a document with a handful still
reads. It fails on 0.38.4 (the tag is accepted) and passes on 0.41.0.

Verified: `cargo xtask ci`; the full xlsx suite (80 tests) plus the malformed-input
tests; and the four measurements above, before and after, with no panics either way.
2026-10-11 03:39:32 +03:00
webdevtodayjason a788c41faa #200: keep formula helpers inside the worksheet
Fixes #200
2026-10-10 18:29:25 -05:00
webdevtodayjason 83c7a3c2f5 Contributors: add Jason Brashear display name 2026-10-10 18:29:25 -05:00
Brandon Thomas 9f896c1249
Bump version to 0.5.0 for release (#227)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:10:53 -04:00
Dejan Kocić a0dc1abce1 fix: Save toolbar button and saveChanges dialog discard an in-progress cell edit
Clicking the Save icon in the toolbar, or the Save button in the "save
changes?" dialog, called file.save directly without first committing a
cell edit still in progress (e.g. double-click a cell, type a new value,
then click Save with the mouse instead of pressing Enter). The pending
text in EditState was never flushed into the document, so the saved
file still held the pre-edit value, silently.

The File > Save menu item already guards against this
(app.commit_edit(...) before running the command); this applies the
same guard to the toolbar icon and the saveChanges dialog's Save button.
2026-10-11 00:35:45 +02:00
Zarvinx 706fe7b10c macOS: use the padded app icon in the Dock 2026-10-11 01:20:03 +03:00
Zarvinx ac87c269ff Fix #213: delete selected objects from keyboard 2026-10-11 01:02:18 +03:00
MarekSpodBiedry 36c591883b Resize charts from the corner handle and the Size fields, keep typed chart and table names (#190)
The corner drag now grabs the chart where the press began and the selected object's corner shows a resize cursor. The Format Chart Area Size fields, chart and axis title fields and the Table Name field reloaded their value from the workbook every frame, so typed or dragged edits were thrown away; they now keep a draft until the field loses focus (Escape cancels, like sheet tab rename).
2026-10-10 23:52:49 +02:00
MarekSpodBiedry 5722dc5355 Do not draw the selection over the frozen pane when it lies only in the scrolling pane (#198) 2026-10-10 23:52:14 +02:00
MarekSpodBiedry 27be1753c2 Read the browser clock for NOW(), TODAY() and year-less dates on wasm (#202) 2026-10-10 23:43:32 +02:00
Zarvinx 271119d9e3 Build: satisfy Rust 1.95 clippy 2026-10-11 00:30:51 +03:00
Zarvinx 7a7e23cab7 Grid: reorder selected columns by header drag 2026-10-11 00:30:24 +03:00
Dejan Kocić d27dec0312 web: print straight to the system print dialog, not just export
The web build had no way to actually print — Ctrl+P (Cmd+P, declared on
file.print's shortcut) did nothing at all, and the ribbon's "Print…" menu
item ran file.print and silently discarded the result. file.print already
builds a PDF and returns it as base64 specifically "for the UI to hand to
the system print dialog" per its own doc comment — this wires that up.

- ui-egui: new Services::print hook (web: hand PDF bytes to the browser's
  print flow directly, no download); SheetApp::print_now runs file.print,
  decodes the base64 result, and calls the hook.
- ui-egui: Key::P now dispatches to print_now (was unbound); the ribbon's
  "Print…" menu item calls print_now instead of discarding file.print's
  result.
- gridcraft-web: implements the hook (Blob -> object URL -> hidden iframe
  -> iframe.contentWindow.print() on load).

Desktop builds are unaffected (no Services::print set there; print_now
falls back to the existing alert-on-error behaviour with no UI action on
success, same as before this change).
2026-10-10 22:50:36 +02:00
Mikkel Georgsen c9525ab5fb Cell references: an invalid reference is an error instead of the active cell
cell_param returned None for any cell argument it could not parse, so every
command that falls back to the selection (cell.set, view.freezePanes,
formulas.trace*, insert.* and others) silently used the active cell instead:
set_cell with cell "ZZZZ0" wrote A1 and reported success. It also dropped a
sheet prefix, so "Other!B2" was applied to the active sheet.

cell_param now returns Result<Option<CellRef>>. An absent or null value is still
None, so the selection fallback is unchanged; an unparsable or non-string value
is an error; a sheet prefix naming a sheet other than the one the command works
on is an error.

cell_param_on is for the commands that resolve their own sheet (cell.get, links,
comments, notes, checkboxes, chart placement, paste): they accept a prefix naming
that sheet. cell.set resolves its own target sheet and still writes to another
existing sheet.
2026-10-10 22:41:40 +02:00
Mikkel Georgsen f69861f7e3 Cell: cell.set without an input is an error
cell.set treated a missing input as an empty string, so a call that forgot the
input (or sent it under the wrong key) cleared the cell and reported success.
It now returns an error when neither `input` nor `value` is given. An explicit
empty string still clears the cell.
2026-10-10 22:41:40 +02:00
Brandon Thomas 90b3c41eec Fix-up: #83's array-export test expects #156's legacy-array read (cached values refilled on recalc)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:59:42 -04:00
GyuriX e8d935298f Linux: the display theme asks the desktop, not winit
winit reports no system theme on Linux — `ActiveEventLoop::system_theme()` is
`None` there on every desktop — so `System` in the appearance setting never saw
the desktop's choice and fell back to light. A new `desktop_theme` module asks
the XDG Desktop Portal's Settings interface for
`org.gnome.desktop.interface color-scheme` on a thread of its own, so no
D-Bus call is ever made from the frame loop, and the app follows the answer,
including a change made while it runs.

egui resolves `ThemePreference::System` from what winit reports and offers no
way to set that itself, so where a desktop answered, the app hands egui the
concrete theme instead. What is drawn is unchanged on macOS and Windows, where
winit answers and the same resolution happens a step earlier; the saved
preference is still `system`, so a file keeps meaning the same thing.

The colour scheme is asked for under its GNOME schema name first, which GNOME's
portal and XFCE's both answer, then under the older freedesktop one, which KDE
answers. A desktop that answers neither leaves the user's own choice alone.
2026-10-10 21:55:20 +02:00
Brandon Thomas c6392e6380 Fix-up: chartex frames use the (open, close) anchor tags after merging #161 onto the move-only anchor change
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:52:46 -04:00
Brandon Thomas 502c588690 Merge pull request #164 from LloydNicholson/macos-open-documents
macOS: open documents from Finder, the Dock and `open -a` (#69)

# Conflicts:
#	apps/gridcraft/src/main.rs
2026-10-10 15:50:57 -04:00
Brandon Thomas 88d0c772fb Merge pull request #161 from nihilist-narwhal/fix-chart-kinds-roundtrip
Charts: histogram, box & whisker, waterfall, funnel, treemap and sunburst keep their kind through XLSX
2026-10-10 15:50:48 -04:00
Brandon Thomas f23281db8c Merge pull request #144 from harrisonoconnorhover/codex/cross-sheet-cut
Preserve formula references when cutting cells across sheets

# Conflicts:
#	ROADMAP.md
#	crates/formula/src/adjust.rs
#	crates/formula/src/tests.rs
2026-10-10 15:48:08 -04:00
Brandon Thomas 49fbae245a Merge pull request #156 from nihilist-narwhal/fix-legacy-array-formulas
Legacy array formulas: fill the range they were entered in

# Conflicts:
#	crates/engine/src/cmd/edit.rs
#	crates/ui-egui/src/formula_bar.rs
2026-10-10 15:47:56 -04:00
Brandon Thomas de0c715662 Merge pull request #186 from MarekSpodBiedry/issue-126
Write comment and hyperlink maps with A1 keys so JSON save works (#126)

# Conflicts:
#	crates/engine/src/tests.rs
2026-10-10 15:47:35 -04:00
Brandon Thomas b07249de75 Merge pull request #160 from itrycodinganditsbad/fix-month-fill-clamp
fill: clamp month and year date series to the month's last day

# Conflicts:
#	crates/engine/src/tests.rs
2026-10-10 15:47:32 -04:00
Brandon Thomas c15a51c959 Fix-up: move the #179 table-style test module to the end of tables.rs after merging #149
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:47:19 -04:00
Brandon Thomas dd185ff12f Merge pull request #157 from nihilist-narwhal/fix-blocked-spill-dependents
Recalc: formulas over a blocked spill see #SPILL!; no spilling in tables
2026-10-10 15:47:12 -04:00
Brandon Thomas c3cfc92186 Merge pull request #155 from nihilist-narwhal/fix-range-blank-cells
Ranges keep their size past the used range; spilled blanks show 0
2026-10-10 15:47:12 -04:00
Brandon Thomas 9fea6235f7 Merge pull request #148 from nihilist-narwhal/fix-stale-recalc-values
Recalc: stale spills and values after edits (fixes #133, #139)
2026-10-10 15:47:12 -04:00
Brandon Thomas 84a414a705 Merge pull request #158 from nihilist-narwhal/fix-chart-kinds-combo-stock
Charts: combo and stock charts keep their kind in XLSX
2026-10-10 15:47:11 -04:00
Brandon Thomas 1e5edbd43c Merge pull request #154 from nihilist-narwhal/fix-repeated-headers
Repeated headers: number pivot fields and table columns in linear time
2026-10-10 15:47:11 -04:00
Brandon Thomas 5c456df4b7 Merge pull request #152 from nihilist-narwhal/fix-whole-sheet-selection
Whole-sheet selection: Fill and Remove Duplicates no longer run out of memory or time
2026-10-10 15:47:10 -04:00
Brandon Thomas 92fb7cd1b3 Merge pull request #149 from nihilist-narwhal/fix-table-header-edits
Tables: typing in the header row renames the column; Go To a table selects its data

# Conflicts:
#	crates/engine/src/tables.rs
2026-10-10 15:47:10 -04:00