Commit Graph
2825 Commits
Author SHA1 Message Date
aionggandHamza 6f7fc7134a IDML: dragged column guides import, validated, and round-trip (#1096) (#1171)
* IDML: keep column guides that were dragged off centre (#1096)

InDesign records dragged column guides in `MarginPreference ColumnsPositions`,
as pairs of offsets from the margin box's left edge. The importer discarded them
and divided the columns evenly, so a page whose gutter the designer had moved
came back with it in the middle: on a 450 pt margin box, `0 150 170 450` was
read as columns at 215 and 235.

`Columns::positions` already existed, documented as "Custom column positions
(after dragging column guides)", but nothing set it and nothing read it. Now
`Page::column_rects` returns the custom rects when they are set — that one
method is what the canvas guides, snapping, threading and the first frame of a
placed story all go through, so they follow the dragged gutter — the importer
fills it, and the exporter writes the page's real positions instead of
recomputing an even split, so the round trip keeps them.

`layout.marginsAndColumns` clears them when a count or gutter is set, which is
what InDesign's Margins and Columns does. Without that the stale positions would
outrank the new value and the dialog would look broken.

Verified against InDesign-authored IDML rather than assumed, which settled two
things. The attribute is written whether or not anything was dragged — a real
page's values are exactly its even split — so an even set is dropped rather than
marking every imported page custom and freezing its columns; the tolerance for
that comparison only absorbs float noise, about 1e-14 pt on that page. And
`<Guide>` is a `<Page>` child, not a `<Spread>` one, which is where this
importer already looks: the reporter's missing ruler guide was 099fbd2, landed
the day the ticket was filed, not a second bug.

That commit left `lib.rs` still listing guides as ignored on import; corrected.

* IDML: custom column positions are validated and round-trip (#1096)

Columns::positions are used only when they are 2 x count finite edges,
ascending from 0 to the margin box width (within 0.01 pt); anything else
is the even split, in Page::column_rects, on IDML import (bounded parse,
checked against the page's margin box) and on IDML export.
Columns::with_gutter_moved moves one gutter's pair of guides between its
neighbours for the coming column-guide command.

ROADMAP: drop four progress-log lines the #1121 merge duplicated and move
its line to the top.

* IDML: a page without its own margins checks the default's column positions against its width (#1096)

* ROADMAP: the progress line lands in one commit after the guide PRs

---------

Co-authored-by: Hamza <mail.hamzanaveed@gmail.com>
2026-10-22 01:59:10 -04:00
aionggandSBaksa 3be2736d0a Guides: select ruler guides and set their position (#1282) (#1445)
* Guides: a dragged guide shows where it is (#1282)

While a guide is dragged out of a ruler or moved, a label beside the
pointer reads X: or Y: and the guide's place, in the ruler units and
measured the way the rulers measure (from the zero point, and from the
page origin when rulers restart per page).

* Guides: keep the drag readout inside the canvas

Near the canvas's right or bottom edge the readout flips to the other
side of the pointer, and it is kept inside the canvas in every case
(readout_at).

* Guides: the drag readout measures the spread under the pointer

The readout measured from the spread in the middle of the view (where the
rulers measure), but a guide is dropped on the spread under the pointer,
so with spreads above one another the number shown could differ from the
guide's place. It now measures that spread (ruler_pieces_of).

* Guides: no readout for an existing guide over another spread

An existing guide moves only on its own spread (guide_drag), so dropped
over another it stays where it was: the readout no longer shows a place
there it won't go.

* #1282: guide selection commands (guide.select, guide.selectAll, guide.deselect, guide.moveBy)

* #1282: select ruler guides on the canvas, X/Y in the Control bar and Properties

* #1282: guides take clicks over objects, a new guide is selected, group drag preview

* ROADMAP: the progress line lands in one commit after the guide PRs

---------

Co-authored-by: SBaksa <sabaksa11@gmail.com>
2026-10-22 01:47:49 -04:00
aiongg 2660395db5 macOS: a Language submenu in the app menu (#1541) 2026-10-22 01:31:36 -04:00
aiongg 8a6f9725b0 IDML: test a ruler guide in a column gutter without facing pages (#1096) (#1170) 2026-10-22 01:25:12 -04:00
aiongg 496b1899a2 macOS menu bar: menus keep working after a language change; key equivalents follow the shortcuts (#1540)
* macOS menu bar: keep working after a rebuild; key equivalents follow the shortcuts in effect

* macOS menu bar: the shortcut test sets its key with window.setShortcut
2026-10-22 01:18:41 -04:00
Zuujis 438d7b6d79 Dedupe the 400-year-cycle UTC conversion and the EPUB package writer (#1537)
* Dedupe the 400-year-cycle UTC conversion and the EPUB package writer

SonarCloud flagged new-code duplication on main (17%): five copies of the
"move the instant into chrono's range by whole 400-year cycles" conversion
and two copies of the EPUB OPF/container/zip writer.

- designcraft-doc gains a `time` module: `date_parts(days)` and
  `utc_parts(secs, nanos)`. App logging, EPUB `iso8601`, PDF `civil`, PPTX
  `civil` and text-variable `civil` are now thin callers; each keeps its own
  format string and year clamp. The app reaches it through
  `designcraft_engine::doc`, so its direct chrono dependency is dropped.
- EPUB: `package_opf` + `write_package` shared by fixed-layout and
  reflowable export (same entry order and compression).
- Boundary tests on the helper: pre-1970, year 9999 -> 10000, i64 extremes,
  nanos past a second.

Output is unchanged: existing date tests pass, and fixed/reflowable EPUBs
(with/without cover, author, pre-1970 and i64::MAX dates) are byte-identical
to origin/main.

* time: drop the unreachable i128 clamp and match arms in utc_parts

Whole 400-year cycles times 400 stay under 2.6e16 for any i64 day count,
so the year can't overflow i64 and the i128 + saturate was dead code
(carried over from the copies this replaced). The date and time lookups
can't fail either; each keeps one unwrap_or_default instead of a match
with a fake 1970 fallback. Output and tests unchanged.
2026-10-21 21:46:38 -04:00
aiongg 2aa5da5950 Paragraph rules: Rule Above's offset is measured from the baseline (#1535)
* Paragraph rules: Rule Above's offset is measured from the first line's baseline

* ROADMAP: Rule Above offset progress line
2026-10-21 20:40:26 -04:00
Dylan M. Taylor 3b985e3813 Use chrono instead of hand-rolled UTC conversion (#1479)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the hand-rolled civil / civil_from_days / days_from_civil copies (the
146_097 / 719_468 magic numbers) across the logging timestamp, text-variable
dates, EPUB dcterms:modified, PDF metadata dates and PPTX core.xml dates with
chrono, added as a workspace dependency (default-features = false, std only, so
wasm is unaffected).

- apps/designcraft logging `timestamp` (private civil_from_days deleted).
- doc text variables: `civil` and `days_from_civil` use chrono::NaiveDate;
  `format_date` and `parse_iso_date` keep their own format/parse logic and
  still call these helpers.
- epub `iso8601`, pdf `civil`, pptx `civil`: UTC datetime via
  chrono::DateTime::from_timestamp.

Output is byte-identical and pinned by the existing date, ISO-date, EPUB,
image, EPS and logging tests. The OOXML/EPUB dcterms timestamps keep their
exact strings. Each converted helper keeps a proleptic-Gregorian fallback for
dates outside chrono's NaiveDate range (and for the serial/overflow dates
parse_iso_date can hand days_from_civil), so no input changes or panics.
AGENTS.md now says time goes through chrono.

Follow-up to storytold/cadcraft#1785

* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed

logging.rs timestamp(): chrono's From<SystemTime> unwraps, so a clock past
year 262143 panicked inside the logger. Use the fallible DateTime::from_timestamp
and read such a clock, or a pre-epoch clock, as the epoch like the old saturating
code did. Years past 9999 are written with {:04} like before, because %Y adds a
'+'. Tests cover both cases.

epub iso8601(): write the year with {:04} instead of %Y, matching the old
{year:04} output, so years past 9999 are not prefixed with '+'. Test added.

AGENTS.md: the "Time goes through chrono" rule now notes the narrow fallback
for dates outside chrono::NaiveDate's range, which crates/doc/src/vars.rs
retains (Howard Hinnant's algorithm).

* Don't build an unrepresentable SystemTime in the far-future test

UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.

* Route out-of-range dates through chrono's 400-year cycle, not hand-rolled copies

doc/vars.rs kept hand-rolled Hinnant `civil_fallback`/`days_from_civil_fallback`
copies for dates outside chrono's ±262143-year range, and AGENTS.md carved a
"narrow fallback" exception for them, which left calendar arithmetic outside
chrono after all.

Replace both copies with the Gregorian 400-year cycle (146_097 days): chrono
converts the day/year within one cycle and whole cycles are added back via i128
(so i64::MIN/MAX can't overflow). `civil` is reached by the system clock through
`format_date(now(), ..)`, so it must stay exact; `days_from_civil` only ever
sees a 4-digit parsed year via `parse_iso_date`, but it uses the same cycle form
for consistency. Both reproduce the pre-chrono proleptic Gregorian result, so
outputs are unchanged (verified against the old formula over wide ranges and at
the i64 extremes).

New test pins dates past chrono's range on both sides of the epoch, through the
public `format_date` and round-tripping `days_from_civil`/`civil`, with values
from the old formula. Removed the AGENTS.md "narrow fallback" exception.

epub/pdf/pptx `civil`/`iso8601` are left as-is: the PR already dropped their
hand-rolled copies for pure chrono, and their remaining epoch (`unwrap_or_default`)
fallback only triggers for a metadata/clock timestamp past year 262143 -- the
live-clock/file-mtime case FredApps accepted, with outputs that clamp the year
anyway (pptx 0..9999, PDF DateTime). No hand-rolled copy remains there to replace.

Tested: cargo test -p designcraft-doc; fmt --check; clippy --all-targets -D warnings.

* Carry an overflowing day into the next month again in parse_iso_date

parse_iso_date accepts any day 1..=31, and the old days_from_civil
carried a day past the end of its month into the next one (2019-02-30 is
2019-03-02). from_ymd_opt rejects such a day, and unwrap_or(0) turned it
into 1970-01-01. chrono now places the 1st of the month and the day is
added on, which restores the old normalization. Tested through days_from_civil
and parse_iso_date.

* time: preserve old out-of-range date conversion (no epoch fallback)

`DateTime::from_timestamp(...).unwrap_or_default()` silently reset any
instant past chrono's year range to 1970-01-01, changing output the old
hand-rolled Howard-Hinnant code converted exactly (e.g. 10_000_000_000_000 s
was 318857-05-20 17:46:40, not the epoch). Restore the old conversion domain
with a cycle-based helper that routes the calendar through chrono (the
calendar repeats every 400 years = 146_097 days, so month/day/time come from
chrono and only the year is carried, keeping every i64 exact), then apply each
format's existing year clamp afterwards.

- apps/designcraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
  pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/epub/src/lib.rs: `iso8601` via `utc_parts`.
- crates/pdf/src/export.rs: `civil` cycle-based; `date_time` keeps its
  0..=9999 year clamp.
- crates/pptx/src/package.rs: `civil` cycle-based, keeps its 0..=9999 year
  clamp on the year only (month/day/time keep the real instant).

Pinned old outputs in tests (through the public helpers):
  logging u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch)
  epub 10_000_000_000_000 -> 318857-05-20T17:46:40Z
       -10_000_000_000_000 -> -314918-08-13T06:13:20Z
       i64::MAX -> 292277026596-12-04T15:30:07Z
       i64::MIN -> -292277022657-01-27T08:29:52Z
  pdf  civil(10_000_000_000_000) -> (318857,5,20,17,46,40)
  pptx civil(10_000_000_000_000) -> (9999,5,20,17,46,40) (year clamped)

* Don't build a year-318857 SystemTime unchecked in the logging test

Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.

* Name the 400-year range extension as the one exception to the time rule

The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.

* Say the library computes month and day; time-of-day is plain arithmetic

Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
2026-10-21 19:36:27 -04:00
Dylan M. Taylor dec78db02c security: enforce HTTPS on curl downloads (SonarCloud S6506) (#1519)
Add --proto '=https' to curl invocations so redirects cannot downgrade to
plaintext HTTP. Resolves SonarCloud S6506 (shell/docker/githubactions).
2026-10-21 19:36:16 -04:00
PiMinusMaxandClaude Opus 5.5 aa1a0a73f3 Gradient panel: drag the gradient onto the Swatches panel to keep it
A gradient made in the Gradient panel stayed unnamed: there was no way
to drag it into the Swatches panel for reuse. The panel's thumbnail now
drags the selection's gradient, and the Swatches list takes the drop as
a new gradient swatch (swatch.new, "New Gradient Swatch"). A gradient
that already is a swatch is left as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:28:23 +02:00
Dylan M. Taylor e2a2502fba ci: add sonar-project.properties for CI-based analysis
Marks the project ineligible for SonarCloud Automatic Analysis, resolving the
CI-vs-automatic-analysis conflict so the SonarCloud workflow succeeds.
2026-10-21 16:00:32 -04:00
Dylan M. Taylor c2dbcff51f ci: add SonarCloud scan via shared reusable workflow
Calls storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main
with secrets: inherit. Adds SonarCloud badge to README where missing and
removes the redundant sonar-project.properties (config now passed by the
reusable workflow).
2026-10-21 15:46:54 -04:00
Brandon ThomasandClaude Opus 5.5 4d62a501c1 Release 0.11.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.11.0
2026-10-21 13:57:30 -04:00
Dylan M. Taylor b67232a6c8 ci: add SonarCloud scan workflow and config 2026-10-21 13:57:06 -04:00
Dylan M. Taylor 9826154db3 ci: add SonarCloud scan workflow and config 2026-10-21 13:56:26 -04:00
Dylan M. Taylor 3ce3a24f08 Merge pull request #1110 from dylanmtaylor/ci/shared-nix-workflow
ci: switch to shared nix workflow from craft-common-actions
2026-10-21 03:40:09 -04:00
skylar taylor-barrick 9f7504931d Merge pull request #1352 from FredApps/ci-xtask-ci-workflow
CI: run cargo xtask ci on pull requests and pushes to main
2026-10-20 22:40:42 -06:00
skylar taylor-barrick 63a68b3d49 Merge current main into CI workflow branch 2026-10-20 21:33:52 -06:00
skylar taylor-barrick 417ef40c4c Pin winit to storytold/winit (same commit) (#1113)
The fork moved from skylartaylor/winit to the storytold org. The pinned
revision 3d51077f is unchanged; only the repository URL differs.
2026-10-20 20:26:43 -06:00
skylar taylor-barrick 1420d8c79b refactor(pdf): share placed-PDF rendering (PDFR) (#876) 2026-10-20 19:52:28 -06:00
NetterSilverandClaude Haiku 5.5 e1e9923471 Keep ':', '<', '>' and '\' in Tagged Text style names
Found while testing crates/textimport/src/tagged.rs:35 (also :52)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 18:02:07 +00:00
NetterSilverandClaude Haiku 5.5 6641e7c29f Drop out-of-range RTF \uN values instead of wrapping them
Found while testing crates/textimport/src/rtf.rs:237

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 17:44:49 +00:00
Hamza ba4b6d4089 Say which backend was chosen without claiming it was the first
Two corrections to the message, both reported by CodeRabbit on #1371.

switched_from being None only means this start did not switch from a
backend that never showed a frame. A backend that failed on an earlier
start is remembered in the record, and choose() then picks the first
candidate that is not in that list -- so calling it the "first choice"
misstates how it was arrived at. The line now says either "nothing has
failed before" or which backends had, so the log says what happened.

The comment claimed that reaching this line means WGPU_BACKEND is
unset. wgpu-types returns an empty mask for an empty or unrecognised
value and begin() only returns early for a non-empty one, so such a
value reaches here with the variable set. The comment says that instead.
2026-10-20 20:59:04 +05:00
Brandon ThomasandClaude Opus 5.5 8dff7ecdb0 Skip CodeRabbit reviews on Dependabot PRs (#1386)
Adds .coderabbit.yaml so CodeRabbit no longer reviews or comments on PRs opened by dependabot[bot].

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:39:20 -04:00
Dylan M. Taylor 72f8186550 docs: add SonarCloud and CodeRabbit badges to README (#1266)
* docs: add SonarCloud and CodeRabbit badges to README

* docs: put SonarCloud and CodeRabbit badges on their own line
2026-10-20 11:33:05 -04:00
NetterSilverandClaude Haiku 5.5 c35dd44f82 Remember the Preview checkbox in Paragraph/Character Style Options
Fixes #1340

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 15:17:27 +00:00
NetterSilverandClaude Haiku 5.5 d271298362 Refuse PDF page ranges in the Place PDF dialog instead of using page 1
Fixes #1337

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 14:34:47 +00:00
NetterSilverandClaude Haiku 5.5 7aba0e15af Saturate table alternating-fill and index paragraph counts
Found while testing crates/engine/src/cmd/table.rs:1396-1403

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 14:10:59 +00:00
Hamza e572a5316f Graphics: the start-up log says which backend was chosen and how to change it
`Fallback::begin` returns before the fallback runs when `WGPU_BACKEND`
is set, logging that wgpu picks the backend. So the line below it is
reached only when the variable is *not* set -- and it said
"(WGPU_BACKEND overrides)", which reads in a log as though the user's
environment had chosen the backend when nothing had.

    before: graphics: DirectX 12 (WGPU_BACKEND overrides)
    after:  graphics: DirectX 12 (first choice; set WGPU_BACKEND to choose another)

The report quotes that line as evidence of which backend a start used,
which is exactly the wrong conclusion to be led to.

Part of #1343.
2026-10-20 18:26:44 +05:00
NetterSilverandClaude Haiku 5.5 0e7e5c3f04 Clamp polygon side count before narrowing to u32
Found while testing crates/engine/src/cmd/object.rs:710-711 (also :975)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 15:07:40 +02:00
NetterSilverandClaude Haiku 5.5 9887272efb Fix "-0p0" for tiny negative pica/cicero values
Found while testing crates/geom/src/units.rs:276

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 12:39:05 +00:00
FredAppsandClaude Opus 5.5 1fec005d19 CI: run cargo xtask ci on pull requests and pushes to main
Only path-filtered workflows run on pull requests today, so most PRs land
without a test run. Port of the wordcraft#800 workflow: Linux, standard
GitHub-hosted runner, read-only token, no secrets, craft-fonts pinned to
the release.yml commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:57:30 +02:00
Brandon ThomasandClaude Opus 5.5 15a2f3c8c8 Release 0.10.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.10.0
2026-10-19 22:18:33 -04:00
Brandon Thomas 443ea4640c Merge pull request #1163 from MaridWSH/fonts/reload-changed-fonts
Fonts: Type › Reload Fonts reads fonts changed on disk again (#328, #547)
2026-10-19 02:53:01 -04:00
Brandon Thomas affce22f4f Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	docs/gaps.md
2026-10-19 02:52:49 -04:00
Brandon Thomas af7bd3cc6e Merge pull request #1136 from harrisonoconnorhover/codex/delete-parent-pages
fix: allow deleting parent pages (#956)
2026-10-19 02:52:39 -04:00
Brandon Thomas f78bf721e8 Merge storytold/main into codex/delete-parent-pages 2026-10-19 02:52:27 -04:00
Brandon Thomas bad330b218 Merge pull request #1242 from 4waymedia/fix/340-package-relative-links
Package: link the packaged document and IDML relative to the package (fixes #340)
2026-10-19 02:52:24 -04:00
Brandon Thomas 96957ef53f Merge storytold/main into fix/340-package-relative-links 2026-10-19 02:52:13 -04:00
Brandon Thomas dceac88f2c Merge pull request #1271 from Hamza0101/screen-mode-bleed-slug
View: Bleed and Slug screen modes show their area; draw the slug guide
2026-10-19 02:52:10 -04:00
Brandon Thomas 0857671f48 Merge storytold/main into screen-mode-bleed-slug 2026-10-19 02:51:59 -04:00
Brandon Thomas 14603a56e1 Merge pull request #1299 from KnottyDyes/squad/stroke-type-validation
Object: reject malformed stroke types
2026-10-19 02:51:41 -04:00
Brandon Thomas 0e4d1cef7e Merge storytold/main into fonts/reload-changed-fonts 2026-10-19 02:51:28 -04:00
Brandon Thomas fcb83fd12d Merge storytold/main into codex/delete-parent-pages 2026-10-19 02:51:25 -04:00
Brandon Thomas 0ac836d6cb Merge storytold/main into fix/340-package-relative-links 2026-10-19 02:51:22 -04:00
Brandon Thomas 5e94f78c2e Merge storytold/main into screen-mode-bleed-slug 2026-10-19 02:51:20 -04:00
Brandon Thomas 157035aa9e Merge storytold/main into squad/stroke-type-validation 2026-10-19 02:51:17 -04:00
Brandon Thomas 73d68e4ef4 Merge pull request #1149 from y-az/fix/arabic-contextual-alternates
Shaping: Arabic keeps contextual alternates
2026-10-19 02:50:41 -04:00
Brandon Thomas 8e7c9504d5 Merge pull request #1148 from y-az/fix/page-number-digits
Breaker: keep a page number's digits together
2026-10-19 02:50:38 -04:00
Brandon Thomas 992a171b78 Merge pull request #1117 from Silvertree2010/fix/h1992-table-moverow-table-dropcells-ca
Refuse table row moves across the header/footer boundary
2026-10-19 02:50:34 -04:00