Commit Graph
351 Commits
Author SHA1 Message Date
FredApps 8f8842e3ab Merge pull request #5396 from KenM76/fix/ocr-edit-under-scan
fix(edit): an edited OCR phrase under its scan image is drawn on top (#3973)
2026-11-04 19:42:25 +01:00
ljkx 023af5bdb2 M7: Arrange keeps a top-level inline image a unit of its own (#3146) (#5823)
* M7: Arrange keeps a top-level inline image a unit of its own (#3146)

The parser emits a whole inline image (BI ... ID ... EI) as one BI op, but
content_units scanned on from it for an EI op that never comes, so the unit ran
to the end of the stream. SendBackward on an image after a top-level inline
image then acted like SendToBack, and BringForward/BringToFront on an image in
the swallowed region could do nothing. A BI op is now a unit of its own, like Do
and sh.

* M7: pin Arrange around inline images at every position (#3146)

Check the exact content units and the paint order after each move, not
string positions: several top-level inline images (at the start, side by
side and at the end) are one unit each, BringForward and SendBackward step
past one inline image at a time, BringToFront on the front image is refused
without touching the page, and SendToBack and BringToFront go past inline
images that start or end the stream. A guard checks that an inline image
inside q/Q or marked content stays in that unit. Document content_units.

* M7: Arrange tests check each image's transform through q/Q (#3146)
2026-11-03 00:32:50 -05:00
NetterSilverandClaude Haiku 5.5 872a775010 Expand header/footer tokens preceded by extra '<' characters (#5766)
Found while testing crates/edit/src/marks.rs:247-262 (expand)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-03 00:32:44 -05:00
KenM76andClaude Opus 5.5 56578a6006 M3: test what moves an OCR edit on top: clipped, two images, form (#3973)
Review asked for text under and above a scan (already pinned), an image
clipped away entirely, two images, and a form holding the image. The
clipped case fails when the hit test uses the image's box instead of
its visible box; the negative cases fail when any later XObject counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 13:42:16 -05:00
KenM76andClaude Opus 5.5 d69d5cc832 Merge origin/main into fix/ocr-edit-under-scan
Conflicts: images.rs (main folded image_transform_scope into
image_scopes; kept only painted_over) and text/interp.rs (gs arm now
reads params via dict_in_place, then records the soft-mask anchor).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 13:36:38 -05:00
KenM76andClaude Opus 5.5 e93e8d50ba fix(edit): keep a soft mask anchored when the edit is lifted over the scan (#3973)
A soft mask is placed by the CTM in force when its `gs` runs (ISO 32000-2
11.6.5.2). The paragraph lifted over the scan re-selects the graphics state
after its own `cm`, so a mask selected before a translation landed
translated with it and could hide the new text. A re-emitted state that
carries a dictionary /SMask now records its anchor CTM and is selected
there: `cm` to the anchor, `gs`, `cm` back.

The test edits the same paragraph with and without a mask and requires the
same ink; it fails with the re-anchoring disabled (the mask lands 100 pt
right and the text vanishes). Both rewrite paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 13:15:43 -05:00
FredApps 4ffe5908c8 Merge origin/main into feat/jbig2-run-allowance
# Conflicts:
#	vendor/README.md
2026-11-02 14:15:33 +01:00
d3384340a6 M7: make page-mark replacement transactional (#5657) (#5704)
* M7: make page-mark replacement transactional (#5657)

* ROADMAP: drop the header line (the log entry stays)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 23:49:18 -05:00
FredApps cc986fa289 Merge origin/main into feat/jbig2-run-allowance 2026-11-01 21:06:29 +01:00
FredAppsandCodex GPT-6.1 75e012de9a fix(edit): compose inside-scope group translations in order
Fold a leading transform inside the image scope as cm followed by the local transform; an enclosing transform uses the opposite multiplication order. This preserves the complete requested image and crop displacement under an outer scale. The exact crop-arrange-group-move fixture previously moved only five points for a ten-point request; saved geometry and rasters now match an independent direct Transform control across both axes and signed offsets. Full scoped Linux native, wasm and repository gates pass.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 17:41:13 +01:00
FredAppsandCodex GPT-6.1 e677c009b3 fix(edit): restore inherited text state when arranging Forms
Track the text-state operators with saved drawing state, restoring inherited spacing, scaling, leading, rendering mode, rise and known font before relocated Form artwork. Emit restoration inside BT/ET, and leave a font selected by a replayed ExtGState authoritative over an older direct Tf. The exact inherited 12 Tc fixture failed its saved-PDF raster comparison before this fix; zero-spacing and destination-state controls now preserve rendering. Scoped Linux native, wasm, clippy and formatting checks pass.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 17:41:13 +01:00
FredAppsandCodex GPT-6.1 8669aa48aa fix(edit): preserve graphics state chronology and mask identity
Replay repeated ExtGState names in last-application order. Compare each complete soft-mask value and its establishment matrix instead of only its group reference, and retain cached shared definitions across saved states. Saved-PDF raster regressions cover Alpha versus Luminosity over the same group, repeated aliases at different matrices, and ordinary sequence/single-mask controls. Definition checks also cover backdrop and transfer parameters. Actual baseline and reverted-comparator tests fail; scoped Linux native, wasm, clippy and formatting checks pass.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 17:41:12 +01:00
Grzegorz MartensandClaude Opus 5.5 1b61121c66 M2.6: a page's forms and pictures read their resources in place
Each `Do` copied the page's `/XObject` dictionary, and each form its `/Resources`, `/Properties`,
`/Font` and `/ExtGState`, or the resources it was drawn with if it had none. So Office export of one
page took time in the square of what it drew: 31 s for 10,000 pictures, about 70 ms of it decoding, 30
s for 10,000 forms with no `/Resources`, and 16 minutes for one form with 100,000 keys in them drawn
50,000 times, in a test build.

`FormCall` keeps the form and `resource_dict` reads a dictionary where it is, with an object of its
own held. 10,000 pictures take about 0.15 s now, 10,000 forms 79 ms, and that form drawn 5,000 times 40 ms. The test holds to 10,000 pictures, not the 2,000 it could afford.

A `gs` reads a direct graphics state in place too, in the text walk and in `page_background`, and a
value of the wrong type isn't copied to find that out. A form that sets a font still reads the font each time it is drawn, as the fonts read
are kept only for that draw: 3.5 ms a draw for one of 100,000 widths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-01 14:53:48 +01:00
FredAppsandCodex GPT-6.1 9a1334e04d fix(edit): preserve image scopes and content ownership
Move editor crop scopes with grouped images while retaining fixed author clips and folding repeated transforms. Arrange keeps relocated original artwork in its own stream beside an added item, so later edits or deletion cannot discard it; unusual shared owned streams are refused. Refuse nested marked-content extraction when its tags cannot be preserved. Synthetic regressions and the headless group-move, undo/redo, save/reopen, arrange, delete and render lifecycle fail before these fixes and pass afterward.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 14:47:46 +01:00
FredAppsandCodex GPT-6.1 3a0ea63c33 fix(edit): share saved drawing operands across graphics scopes
Keep dash, color and extended printing operand payloads shared across q snapshots. Copy-on-write color updates preserve saved values without multiplying large operand arrays by nesting depth. A pointer-sharing regression failed before the change; scoped native tests, clippy and wasm check pass on GGEO-AI02.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 14:47:46 +01:00
FredAppsandCodex GPT-6.1 43ad4bf418 feat(edit): expose image crop ownership to frontends
Report active PdfCraft crop markers through PageImage::has_editor_crop and page_images.has_editor_crop so frontends can distinguish removable editor crops from author clipping paths.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 14:47:46 +01:00
FredAppsandCodex GPT-6.1 f62c1cfafe refactor(text): share source font names and selection defaults
Reuse the existing subset-tag parser through the engine facade and share the fallback formatting state.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 12:54:52 +01:00
FredAppsandCodex GPT-6.1 f607d26d14 fix(edit): preserve printing state when arranging images
Refuse relocation into transfer, overprint, black-generation, undercolor-removal or halftone state that cannot be restored. Preserve implicit device color spaces before sc/scn and restore rendering intent from both ri and ExtGState RI, including its initial default. Three synthetic regressions fail against the previous implementation; all 411 edit tests and scoped clippy pass on GGEO-AI02.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 12:41:19 +01:00
FredAppsandCodex GPT-6.1 3c394bb860 fix(edit): remove only editor-owned image crops
Identify crop paths with a marked-content point and preserve ownership through arrange reconstruction. Find the same isolated or outermost image scope as Crop, preserving authored and shared clips. Both regressions fail with the fix reverted; all 410 edit tests, scoped clippy, fmt, layers and assets pass on GGEO-AI02.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 12:37:24 +01:00
FredAppsandCodex GPT-6.1 1a2f7b2a34 fix(edit): refuse arrange across crossed scope boundaries
Require both graphics and marked-content boundaries to balance inside source, destination and removed spans. Regression fails before the guard; all 408 edit tests, scoped clippy, fmt, layers and assets pass on GGEO-AI02.

Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
2026-11-01 12:33:51 +01:00
FredAppsandClaude Opus 5.5 eb0d5e64c7 refactor(edit): fold duplicated image scope and arrange helpers
PR #2155 review item A5 (part 2), plus the unreachable Ok(()) exits in
arrange_image from B11. No behaviour change for well-formed content:
- image_exclusive_scope and image_transform_scope were one scan; they are
  now image_scopes, and transform/crop take ImageScopes::transform_scope.
  The shared scan resets an open path at Q, as the exclusive one did; the
  two only differed on content that leaves a path open across Q.
- arrange_image and remove_image_crop took page and index and listed the
  page's images again; change_image now passes the page, its streams and
  the image, and neither is public any more.
- content_units' four copied scan loops are one sequence_end.
- DrawingState keeps its line-style, intent and flatness operators in a
  table; the w/J/j/M/d guards in compute_inherited_drawing_ops tested ops
  extract_scope_state never collects.
- clip_path_to_ops: ImageState never builds a quadratic segment and only
  stores finite points, so the QuadTo arm and finite checks were dead; it
  now reports a segment it can't write instead of dropping the clip.
- target_destination_unit and determine_arrange_spans are shorter; the
  Ok(()) exits for lookups that cannot fail are now errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:33:50 +01:00
FredAppsandClaude Opus 5.5 71b39aec5b refactor(edit): drop test-only image API and dead graphics-state code
PR #2155 review item A5 (part 1). No behaviour change for callers:
- crop_image, and the arrange_image/crop_image/remove_image_crop/MaskIdentity
  re-exports, existed only for tests; tests now go through change_image.
- PageImage.smask was read only by tests; page_images no longer tracks
  ExtGState for it. The nested-mask test keeps its rendered-pixel check.
- MaskIdentity's Display, ExtractedScopeState.effective_cm, the
  ExtendedGStateTracker overflow_depth (an error ends the scan, so it was
  never read) and the EffectiveGState one-line getters were unused.
- merge_dict always gets the ExtGState name, so the hash fallback for an
  unnamed inline soft mask was unreachable outside a test.
- cid_to_gid_map_odd_length_is_trimmed_to_even exercised no crate code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:33:50 +01:00
Combiler01 fdc0e6404a fix: address review feedback on arrange, image flips, rotate knob, and format panel
- [P1] Arrange: track and replay inherited non-ExtGState drawing state (colors, line widths, etc.) across q/Q scopes for stencil and form artwork
- [P2] Flips: route ImageEdit::Flip through coordinate-aware transform logic to swap axes when rotated at odd quarter turns (90°/270°)
- [P2] Rotation knob: measure clockwise angle delta from drag start relative to the rotated image center to prevent reapplying existing angle
- [P2] Format panel: connect superscript/subscript controls to line editor and content draft selections, and update parity evidence
2026-10-31 23:18:58 +05:30
Combiler01 d2d28c7728 test(edit): restore regression test rewritten_paragraph_is_drawn_after_backgrounds_between_its_rows (#2377) 2026-10-31 19:40:44 +05:30
Combiler01 e781d31187 fix(ui, test): resolve font-size inspect query and command-key paste tracking 2026-10-31 17:09:50 +05:30
Combiler01 79eed72c08 Merge upstream/main into main preserving custom features
- Resolve all 13 merge conflicts between upstream/main and main
- Preserve full image editing suite: OBB, crop, proportional resize, arrange, and context menu actions
- Preserve text editing formatting tools: bullet/numbered lists, subscript/superscript, and line spacing
- Cleanly integrate upstream's canvas modularization, font updates, and styling changes
- Refactor arrange_image and run_edit_page_image helpers to satisfy clippy cognitive complexity limits
- All engine, edit, and UI integration test suites passing cleanly
2026-10-31 15:50:31 +05:30
Combiler01 87c1e858e3 Coderavbit Fixes 2026-10-31 14:23:19 +05:30
KenM76andClaude Opus 5.5 e473629b9e Merge origin/main into fix/ocr-edit-under-scan
Conflict in crates/edit/src/text_regions/styled.rs: kept this branch's
`row_step` beside main's `WrappedStyledRows`, and main's marker-aware
`splice_with_marker` splice with the branch's `at` (already computed
before the reach check). main's `wrap` now yields (line, soft, gap), so
the reach's widest line in rewrite.rs reads the line from the tuple.

Gates: fmt, clippy -p pdfcraft-edit -p pdfcraft-engine --all-targets
-D warnings, tests edit 362 / engine 388 / 18, xtask parity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-30 16:20:01 -04:00
FredApps d7f3e67141 fix: bound backdrop edits and withhold unsafe integration behavior 2026-10-30 14:45:50 +01:00
FredApps 673178b70c fix(edit): require intact streams when identifying page backdrops 2026-10-30 13:21:04 +01:00
FredApps 25f3d12960 style: format checked soft-wrap budget assignment 2026-10-30 12:54:56 +01:00
FredApps ee71348bc8 fix: bound soft-wrap metadata and preserve unknown backdrop content 2026-10-30 12:47:37 +01:00
FredApps bdf56209ce style: format combined paragraph regression 2026-10-30 11:08:55 +01:00
FredApps 7d3d4830e5 Integrate remaining six PdfCraft parity PRs with conflict resolutions
Includes #4325 #1952 #4180 #2529 #4320 #4805, preserving their original histories.
Keep paragraph alignment and hard/soft wrap metadata together; retain CCITT/downsampling/RGB ownership guards with bounded JBIG2 export and exact-copy symbol encoding.
Combined interaction regressions added. Validation pending.
2026-10-30 11:02:07 +01:00
FredApps b21af3b969 Integrate 21 clean-merging PdfCraft parity PRs
Preserve each original PR head and its authorship as a merge parent.
Includes #4304 #4335 #4384 #4404 #4179 #5300 #4719 #4541 #2815 #4620 #3789 #3779 #2960 #5208 #4596 #3636 #4494 #4013 #5134 #2133 #2635.
Conflicting candidates skipped. Combined CI pending; see docs/integration-20261030.md.
2026-10-30 10:35:42 +01:00
KenM76andClaude Opus 5.5 ffa08fa00d fix(edit): reach one line above the paragraph's top (#3973)
A larger range on the first line of a multi-line paragraph rises from the
first line's baseline, not from the old box's bottom, so the reach now
extends one pitch above the moved box's top. Name the 1.2 em line floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-30 03:37:21 -04:00
KenM76andClaude Opus 5.5 8e5f423d6d fix(edit): reach as high as a larger range's glyphs
With only a range enlarged, row_step is the source leading, so the
styled rewrite's reach rose one source line above its old box while a
120-point range draws far higher, and an image there was missed. The
pitch is now at least 1.2 times the largest size.

The landing test gains a 120-point range on a 12-point OCR line under an
image above the old box; it fails without the floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-30 03:31:03 -04:00
KenM76andClaude Opus 5.5 8fd151ff79 fix(edit): reach the styled rewrite's rows at the step they are written
The styled path writes rows `line_spacing` (1.5 by default) times the
size apart when a size or spacing is set, but its Reach assumed 1.2, so
an image over only the lower rows of a larger rewrite was missed. The
pitch now comes from the row writer's own step (row_step, shared by
both), at the largest size among the paragraph's lines and ranges.

The landing test gains a six-row rewrite at twice the size over an image
covering only its last row; it fails with the 1.2 step.
A case with only line_spacing set covers the other branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-30 02:53:45 -04:00
KenM76andClaude Opus 5.5 705d862328 fix(edit): check revealed OCR text where the rewrite lands
The on-top trigger compared later paint with the paragraph's old box. A
rewrite that is moved (BlockStyle::offset), rewrapped wider or grown to
more lines can land under an image the old box is clear of, and stayed
underneath it. Both rewrite paths now test a Reach: the old box (where
the cover goes) plus the moved box grown to the wrap width either way
and down to the drawn line count (the widest measured line when wider), and
up by one line pitch for a larger size; turned text grows by the lines'
diagonal; any non-finite number makes it unbounded. Over-reaching is
harmless: the cover still fills only the old box. The parity notes say
so.

Tests: an_ocr_rewrite_goes_on_top_of_paint_over_where_it_lands (move,
width and line growth, both paths) and a_rewrites_reach_covers_its_move_
width_and_lines. Seen failing with each path reverted to the old box and
with growth zeroed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-30 02:12:01 -04:00
Dylan M. Taylor 3517d05b47 Update prompt shows the app icon and accent color instead of a letter (#5388)
UPDATE_APP (native builds; craft-update is a native-only dependency) carries our 128 px icon and accent (#b70228, its red, from the icon (#5245)). The updater, the self-update
helper and craft-update's startup prompt all get them through crate::UPDATE_APP; the
prompt draws the icon, the accent and "by ArtCraft" (craft-update's default developer).

craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
Clippy fixes for Rust 1.99 in untouched code (same logic):
- crates/edit/src/images.rs
- crates/forms/src/appearance.rs
2026-10-30 01:27:58 -04:00
KenM76andClaude Opus 5.5 3c11186dbd fix(edit): move revealed OCR text on top only when an XObject covers it
An unrelated image later on the page no longer moves the paragraph and
its cover on top, where the cover could hide visible content inside the
paragraph's box. An XObject now counts only when it is drawn at or after
the paragraph and its visible box overlaps the paragraph's
(`images::painted_over`, from `page_images`). An inline image or a
shading after the paragraph still counts anywhere, and so does any XObject
when the page's images cannot be listed.

The trigger table now covers an image elsewhere, an image drawn first,
visible text, a filled path, a shading and an inline image. It fails with
the old any-`Do` trigger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-30 01:22:41 -04:00
KenM76andClaude Opus 5.5 5a2f9273a3 test(edit): pin when revealed OCR text moves on top
A table test for `revealed_under_paint`: only invisible text with an
XObject, inline image or shading after its place moves; a filled path,
visible text or nothing after it does not. Seen to fail with the check
reduced to `invisible`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-30 00:46:27 -04:00
KenM76andClaude Opus 5.5 270a11e699 fix(edit): refuse before changing anything; cover undo, reopen, re-edit and the split case
An OCR paragraph that may go on top of the page is checked (`check_on_top`)
before either rewrite path changes the document, so a page whose marked
content nests too deep is refused with nothing changed. `lift_on_top` no
longer checks.

Tests: undo restores the hidden layer and the scan, redo, save and reopen,
and a second edit of the moved text all show the edit; a phrase split from
visible text in the same text object shows its edit; a refused rewrite on a
too-deep page leaves the saved bytes unchanged. Each new test was seen to
fail with its part of the fix reverted.

Parity: the note says when the edit is drawn on top and lists what is
still missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-30 00:43:25 -04:00
KenM76andClaude Opus 5.5 cc9d95e1fb fix(edit): an edited OCR phrase under its scan image is drawn on top (#3973)
Some OCR tools write the invisible text layer first and the opaque page
image after it. Edit text rewrote such a phrase in the text's place, so
the cover and the new visible text were painted under the scan: the edit
was saved (text extraction finds it) but nothing showed.

When the rewritten paragraph was invisible and an XObject, inline image
or shading is painted after its place, the cover and the new text now go
on top of the page in their own q/Q (place_tagged, tagged Edited), drawn
from the initial graphics state with the paragraph's CTM. Only the state
the following text expects stays in place. Both rewrite paths (single
style and styled rows) do this. An OCR layer drawn over its scan, the
common case, is rewritten in place as before, so reading order is kept.

The test builds a scan whose OCR word comes before the image and checks
the cover and new text show, for one line and for a typed line break.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-10-30 00:29:23 -04:00
RiceDee 5a86f35210 fix(edit, ui): alignments follow the right rows; Format by keyboard keeps the box
Four holes CodeRabbit found on 648dd428:

- alignment_frame framed a one-row paragraph when any alignment entry
  was set, including one past the draft's rows, and then moved rows
  with no alignment of their own to the column's edge. It now takes the
  draft and frames only when every draft row is aligned, by its own
  alignment or the paragraph's.
- remap could not tell which of two same paragraphs an edit took
  ("A\nA" to "A"), and kept the deleted one's alignment. remap_at takes
  the caret where the edit ended, and the typing path (follow_typing,
  both editors) passes it from the box's state. The caret only chooses
  between readings that fit both texts.
- Both editors judged a focus loss by where a click landed, so focusing
  a Format text control by keyboard or a screen reader (no pointer)
  closed the box before the alignment was chosen. They now also judge
  by where the keyboard went (focus_pos): focus outside the document
  view, or on the added-text box's own buttons, keeps the box open as a
  click there does.

Each change has a test seen failing before it.
2026-10-29 21:30:21 +11:00
ihsan mokhlisse 55a933800b M7: keep a typed space run through a saved width change (#2517)
The wrap's space count has to survive an incremental save and a full rewrite, and a width-only text_edit has to reflow with that count. A recorded editor session narrows the box and widens it again.
2026-10-29 10:42:20 +01:00
ihsan mokhlisse bb7186d87c M7: restore the exact gap a wrap consumed, not a guessed space (#2517)
wrap() already dropped the whole gap at a wrap point (#4199); rejoining it always guessed a single space back, so two or more typed spaces silently became one, and a Chinese/Japanese word split (which has no space of its own) could gain a spurious one. wrap() now records how many separator characters it consumed at each wrap (0 for a CJK split); the /PCSoft marker carries it as /N (a file saved before this, or by another writer, reads as 1); reflow_text and the mixed-style row editor insert exactly that many spaces back instead of assuming one.
2026-10-29 10:13:01 +01:00
ihsan mokhlisse dbeb9fa963 M7: move a paragraph instead of rewriting it to trigger a stale-number regroup
wrap()'s own test for mixed exotic whitespace fed through it directly, bypassing the caller's CR/CRLF normalization, needed its expected value updated for #4199's keep-every-space design, landed on main after this branch's base.
2026-10-29 10:11:19 +01:00
ihsan mokhlisse b282ec26be M7: check a shared text object through the tools (#2517)
Editing either end of one text object, then saving and editing again, has to leave the other paragraph in place. The library tests did not drive text_edit and doc_save.
2026-10-29 10:11:19 +01:00
ihsan mokhlisse 59513aa887 M7: keep a preceding paragraph out of a shared text object's marker (#2517)
A partial rewrite of the last paragraph opened the marker at the shared BT, so the paragraph before it was inside the region.
2026-10-29 10:11:19 +01:00