Commit Graph
2878 Commits
Author SHA1 Message Date
Hamza 18f2dc2bde A spent cell budget stops reading tables, rather than keeping one each
Keeping a 1 x 1 table for every further `<Table>` was unbounded in the one
direction the budget exists to close. `<Table></Table>` is fifteen bytes, so
the 512 MiB a part may decompress to holds some 36 million of them, and a
`Cell` is 1,360 bytes: tens of gigabytes, with every table inside both caps.
The test I wrote said as much without my noticing -- its invariant was
`reserved <= MAX + tables`, and `tables` had no bound.

`budgeted_size` now returns `None` once the budget is spent and the table is
not read at all, so the cells one import reserves are at most the budget,
exactly. The invariant is now `reserved <= MAX_IMPORT_TABLE_CELLS`, with no
term that grows with the file.

The warnings needed the same treatment, for the same reason: a file of 400,000
tables each claiming one row and two columns got a line about every one of
them. At most 20 are named, then one line saying the rest were the same, and
the file is told once that its tables ran out of cells between them.
2026-10-29 16:43:49 +05:00
Hamza 1995fb5feb IDML import: the tables in one file are capped between them
Review pointed out that capping each table leaves a file free to hold
many capped tables: three at the per-table limit claim 750,000 cells,
about 1 GB of Cell, with every table inside its own limit. The package
reader's limits count input bytes, which say nothing about how many
cells a row and column count claim.

budgeted_size clamps each table against what the import has left, and
the cells are reserved before Table::new so a cell's own story cannot
spend the budget the table around it is counting on. The budget is
400,000 cells: a Cell carries a whole story and is 1,360 bytes, so that
is about the 512 MiB one part of the file may already decompress to,
and the same count the spreadsheet importer collects at most.

A table is never read as nothing. One row and one column are kept, so
the table stays in the document and still says where its text went, and
a file pays one cell for a table it really does contain -- which its own
size already bounds. The warning says which limit cut the table down.

The arithmetic is tested rather than the allocation: 400,000 cells is
544 MB and three such tables are not a thing to ask CI to allocate. The
sequence test runs 32 claimed sizes, including usize::MAX, and holds the
invariant across all of them.
2026-10-28 16:31:34 +05:00
Hamza b49535624c Tables: a shape a table cannot hold is clamped, not allocated (#1583)
`Table::new` allocates `rows * cols` cells up front, and the counts reach it
from IDML and DOCX, where they are whatever the file claims. Measured here:
`size_of::<Cell>()` is 1,360 bytes, so the reported IDML case -- one `<Table>`
with 5,000 `<Row/>` and 5,000 `<Column/>`, about 1 KB zipped -- asks for 25M
cells, some 34 GB. That allocation does not fail politely: it aborts the
process through `handle_alloc_error`, which no `catch_unwind` can catch, so
every open document's unsaved work goes with it.

A table is clamped to InDesign's own limits, 10,000 rows and 1,000 columns,
and to 250,000 cells whatever its shape -- the row and column limits alone
would still allow 13.6 GB. Columns are clamped first and rows take what is
left, because a table is far more often long than wide.

The IDML importer walks the clamped slices and reports the difference.
Indexing its rows and columns by the file's own counts would be out of
bounds, which is how a 34 GB abort would otherwise have become a panic.

Rebased onto the post-split main: the constants and `clamped_size` live in
`doc`'s `table` module root, `Table::new` in `table/grid.rs`, and the IDML
table reader in `import/table.rs`.
2026-10-28 16:26:13 +05:00
aiongg 1abeda4459 Merge pull request #1802 from dotCooCoo/fix/column-rules-span-the-text
Text frames: column rules run from the top line's ascent to the bottom line's descent
2026-10-27 13:45:19 -04:00
aiongg 5d8739b3d8 Merge pull request #1795 from dotCooCoo/feat/story-editor-depth-ruler
Story Editor: a depth ruler beside the text, and View › Story Editor › Show/Hide Depth Ruler, as in InDesign
2026-10-27 13:44:46 -04:00
aiongg d36e5fb63f Merge pull request #1927 from Silvertree2010/perf/export-speed
PDF export: ask the composition cache once per story and page
2026-10-27 13:41:05 -04:00
NetterSilverandClaude Opus 5.5 1db1635a88 Ask the composition cache once per story and page in PDF export
The PDF exporter called Cache::get twice for every text frame it drew.
Each call rebuilds the cache signature of the whole story, which looks up
every frame of the story with Document::find (a linear walk over all
spreads), and the anchored-frame placements walk them again. For a story
threaded through many frames this is O(frames^2 x items) per export.

The document doesn't change during an export, so the exporter now keeps
the composition it got for each (story, page) and reuses it. The cache
still decides what to compose; the output is byte-identical.

Add examples/pdf_export.rs to designcraft-cli: File > Export PDF through
a headless session on the `designcraft-cli perf` stress document (200
pages, 1M characters in 40 threaded stories, 500 placed images). The
stress document generator moves unchanged from src/perf.rs to a small
library module (src/stress.rs) that the binary and the example share.

Ryzen 16 threads, Linux, 12 runs each, interleaved:
  main     median 3485 ms, min 3427 ms
  patched  median 2016 ms, min 1656 ms  (-42 % median, -52 % min)
The exported PDF is byte-identical (12,075,744 bytes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:47:25 +01:00
aiongg d77fdf2d4c fix(ui): dropdown menus grow back after a search with no matches (#1917)
* fix(ui): font menus grow back after a search with no matches

Every combo box is craft_ui::ComboBox (craft-libs pinned at 62bb0e1d), whose
menu follows its contents' height while open (egui#5225); clippy.toml bans
egui::ComboBox.

The Font menu splits into a header (search field, filters) and a list
(font_menu_header, font_menu_list). Font fields show it in a
craft_ui::combo::Menu and the style dialogs' Font Family combo boxes in
ComboBox::show_ui_with_header, so only the rows scroll and the search field
stays in place. The blank and Parent's Font rows sit in the header, above
the search.

* chore(deps): craft-libs 60d60d48 (dropdown fixes)

* chore(deps): craft-libs 4d7b82f7 (dropdown fixes)

* chore(deps): craft-libs 92d392cf (#387 merged)

* chore(xtask): pdf-deps expects craft-libs 92d392cf
2026-10-27 10:27:32 -04:00
dotCooCoo 9ef2e64f9f Merge main into the column rule extent fix 2026-10-27 07:16:50 -07:00
aiongg 50650f74d7 Iconic panel flyouts stay open while the canvas is in use (#1902)
Auto-Collapse Iconic Panels is off by default, so a flyout closes only from its icon or
another icon. The setting is saved under a new key, so preferences saved with the old
default start with it off, and Preferences > Interface > Panels shows it.
2026-10-27 01:54:50 -04:00
dotCooCoo fcea4173be Hide stale Story Editor depth marks while text changes
Draw the depth ruler and overset line after composition reflects the edited
text. Add a paint-output regression for inserting a paragraph break.
2026-10-26 21:42:22 -07:00
dotCooCoo 17247c929e Merge main into feat/story-editor-depth-ruler
Move the depth ruler state, translations, and menu command into the split UI modules.
2026-10-26 21:02:19 -07:00
aiongg 42993ae680 test(engine): read only test cfgs and any pub(…) mod as test modules in the file guard
Split files share test helpers through pub(super)/pub(crate) test modules. The guard read only `mod` and `pub mod` as a test module, so it scanned their fixtures as production code. `attr.contains("test")` also matched `#[cfg(not(test))]`, so such a module was skipped; it is scanned now.
2026-10-26 23:06:07 +00:00
aiongg bbb495111e refactor(epub): split lib.rs into modules 2026-10-26 23:05:57 +00:00
aiongg 27e21ba19a refactor(render): split lib.rs into modules 2026-10-26 23:05:57 +00:00
aiongg 08f633ae48 refactor(fonts): split fontdb.rs into fontdb/ 2026-10-26 23:05:57 +00:00
aiongg f553e17406 refactor(doc): move the CharAttrs and ParaAttrs sets to attrs/char_attrs.rs and attrs/para_attrs.rs 2026-10-26 21:00:23 +00:00
aiongg f15fe7aef0 refactor(ui-egui): split i18n.rs into i18n/table.rs 2026-10-26 21:00:23 +00:00
aiongg 2bca3285bf refactor(ui-egui): split app.rs into app/ 2026-10-26 21:00:23 +00:00
aiongg 02f595fe92 refactor(ui-egui): split lib.rs into app, state, services, clock and key_names 2026-10-26 21:00:23 +00:00
aiongg 2dd87273a2 refactor(doc): split attrs.rs into attrs/ 2026-10-26 21:00:23 +00:00
aiongg bc345384f6 refactor(doc): split table.rs into table/ 2026-10-26 21:00:23 +00:00
aiongg a24e9dffd1 refactor(doc): split item.rs into item/ 2026-10-26 21:00:23 +00:00
aiongg f70c3efa3c refactor(ui-egui): split chrome.rs into chrome/ 2026-10-26 21:00:23 +00:00
aiongg 8f601148b6 refactor(engine): split cmd/layout.rs into cmd/layout/ 2026-10-26 21:00:23 +00:00
aiongg f33de8f4aa refactor(pdf): split export.rs into export/ 2026-10-26 21:00:23 +00:00
aiongg bec8800b62 refactor(idml): split export.rs into export/ 2026-10-26 21:00:23 +00:00
aiongg 2c0882c6e0 refactor(idml): split import.rs into import/ 2026-10-26 21:00:23 +00:00
aiongg a1ec465207 refactor(engine): split cmd/style.rs into cmd/style/ 2026-10-26 21:00:23 +00:00
aiongg 7afc1af676 refactor(engine): split cmd/text.rs into cmd/text/ 2026-10-26 21:00:23 +00:00
aiongg 2f7e956e8e refactor(ui-egui): split panels/properties.rs into panels/properties/ 2026-10-26 21:00:23 +00:00
aiongg d93acf7120 refactor(engine): split cmd/export.rs into cmd/export/ 2026-10-26 21:00:23 +00:00
aiongg b2f1ad1867 refactor(engine): split cmd/file.rs into cmd/file/ 2026-10-26 21:00:23 +00:00
aiongg 18e03a3035 refactor(engine): split cmd/table.rs into cmd/table/ 2026-10-26 21:00:23 +00:00
aiongg 96295e3f58 refactor(ui-egui): split menus.rs into menus/ 2026-10-26 21:00:23 +00:00
aiongg 9d5dbb4b8c refactor(ui-egui): split canvas.rs into canvas/ 2026-10-26 19:02:05 +00:00
aiongg 5f53d41387 docs(engine): name the file guard test's new path 2026-10-26 19:02:05 +00:00
aiongg bec8b28cbc refactor(ui-egui): move panel_docking_tests.rs to panel_docking/tests.rs 2026-10-26 19:02:05 +00:00
aiongg 09688b34d6 refactor(fonts): move the fontdb tests_*.rs files to fontdb/tests/ 2026-10-26 19:02:05 +00:00
dotCooCoo af46697a88 Merge branch 'main' into feat/story-editor-depth-ruler
# Conflicts:
#	ROADMAP.md
2026-10-26 11:57:12 -07:00
aiongg b0dc1578b5 refactor(engine): move the loose tests_*.rs files to tests/ 2026-10-26 18:18:14 +00:00
aiongg 42071abc5c refactor(ui-egui): move the font_menu_ui_tests module of panels.rs to panels/font_menu_ui_tests.rs 2026-10-26 18:18:14 +00:00
aiongg 2e52c9fcd0 refactor(engine): move the tests module of cmd/place_text.rs to cmd/place_text/tests.rs 2026-10-26 18:18:14 +00:00
aiongg 63dff1e6b5 refactor(ui-egui): move the tests module of prefs.rs to prefs/tests.rs 2026-10-26 18:18:14 +00:00
aiongg 256426ae99 refactor(doc): move the tests module of attrs.rs to attrs/tests.rs 2026-10-26 18:18:14 +00:00
aiongg d4bd716ebf refactor(engine): move the text_clipboard_tests module of cmd/edit.rs to cmd/edit/text_clipboard_tests.rs 2026-10-26 18:18:14 +00:00
aiongg 8d36acd1c0 refactor(engine): move the tests module of cmd/load_styles.rs to cmd/load_styles/tests.rs 2026-10-26 18:18:14 +00:00
aiongg 8ba7fd46b9 refactor(engine): move the tests module of cmd/variables.rs to cmd/variables/tests.rs 2026-10-26 18:18:14 +00:00
aiongg 16b3594f31 refactor(ui-egui): move the tests module of i18n.rs to i18n/tests.rs 2026-10-26 18:18:14 +00:00
aiongg 8d4a32667f refactor(ui-egui): move the tests module of panels/pages.rs to panels/pages/tests.rs 2026-10-26 18:18:14 +00:00