feat(editor): wrap text at a dragged width or the canvas edge - #59
feat(editor): wrap text at a dragged width or the canvas edge#59jondkinney wants to merge 8 commits into
Conversation
4dff61b to
838b198
Compare
|
Pushed a round of fixes from testing the draft side of this more heavily:
One deliberate behavior change to flag: the committing click now only commits. It used to also open the next editor at that point, which made clicking off a way to accidentally start new text. A second click opens the next draft. The click-away check has been updated to the two-click sequence, so if you prefer the old contract that hunk is the one to look at. |
838b198 to
726c2fd
Compare
|
Rebased over the text box work. The two compose rather than overlap: the box gives the entry room for lines and decides when Enter commits, and this branch handles horizontal overflow, wrapping long text at the canvas edge or at a width dragged from the committed layer's handle. Both feature's checks pass together in the suite. |
be4f067 to
7e56a65
Compare
Long text ran straight off the capture, taking its handle with it, so it could not be read or dragged back. Text layers carry a `textWidth` (0 = wrap at the canvas edge). `annotationTextLines()` splits on hard newlines first, then word-wraps each paragraph to `annotationTextWrapWidth()`, breaking mid-word only when a single word cannot fit. Bounds and painting both lay out through it, so the editor preview and the export agree. The inline editor wraps as you type: the widget takes the wrap width and reports its own laid-out height, since wrapped lines are not the newline count. The bottom-right handle now sets that wrap width rather than scaling the font. Its cursor has always been SizeHorCursor, and the wheel already sets a selected layer's size, so size had two controls and width had none. Text that wrapped at the canvas edge freezes that shape on commit, as tight as its widest line, so moving it later never reflows the paragraph you just placed. The handle can still re-wrap it. Smoke (exit 125): wrapping at a dragged width, hard newlines surviving alongside it, an over-long word breaking rather than overflowing, bounds following the wrapped shape, and the canvas edge wrapping when the layer has no width of its own. The painted glyphs lay out through annotationTextLines() like the bounds do; the pill and export path take the canvas width; the draft pill hugs the laid-out lines instead of inheriting the widget's scroll slack; and a click that commits a text does only that, so the next text takes its own click. The click-away check follows that contract with a second click, and the wrap check asserts rendered pixels, not just the layout.
The draft pill borrowed the widget's rect, but the widget keeps typing slack (a 48px floor plus room for the next glyph) and starts at the glyph top rather than a pad above it. So the pill ran wide of short text, the caret sat at the left of an empty pill instead of inside a small one, and the whole background shifted up and in on commit. Rebuild the rect from the draft text with the committed pill's own formula so the two coincide, and gate it with a check that compares the cream pixels before and after the committing click.
The inline editor forbade soft wrapping, so a draft typed toward the edge ran past it in one long line and only wrapped on commit, with the background snapping into the wrapped shape. Let the widget wrap the way the committed text will (word boundaries, anywhere within a word too long to fit) and give its viewport the same room the commit wrap gets, so the wrapped draft pill and the committed pill coincide. The pill now measures the laid-out lines rather than newline-split advances. A layer whose frozen width sticks out past the canvas edge kept its wrap handle out there too, sometimes off the screen, with no way to drag the text back in. The handle now waits at the canvas edge instead.
Multiline text can run below the canvas bottom even when its width fits inside the right edge, which left the wrap handle underneath the canvas with the same unreachable-handle problem sideways overrun had. Clamp the handle inside every edge, and check that text loaded with its bottom clipped still offers a grabbable handle that resizes it inward.
3dceeff to
e0b6d8e
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Wrapped-text persistence, whitespace handling, zoom scaling, and pill resize calculations must be corrected before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds canvas-aware, resizable text wrapping throughout annotation editing and rendering.
Changes:
- Adds automatic and dragged-width text wrapping.
- Integrates wrapping with canvas expansion and text resizing.
- Adds smoke tests and documentation.
File summaries
| File | Summary |
|---|---|
tests/editor-smoke.cpp |
Adds wrapping and expanded-canvas smoke coverage. |
src/editor.hpp |
Tracks draft wrap width. |
src/editor.cpp |
Integrates wrapping into editing and resizing; wrap-width scaling and pill padding calculations need correction. |
src/capture.hpp |
Defines text-width state and layout APIs; JSON persistence is missing. |
src/capture.cpp |
Implements wrapped layout and rendering; whitespace preservation needs correction. |
README.md |
Documents text-wrapping behavior. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| for (const QString &word : paragraph.split(' ')) { | ||
| const QString candidate = line.isEmpty() ? word : line + ' ' + word; |
| /// Wrap width for text layers in image px; 0 leaves the layer unbounded. | ||
| qreal textWidth = 0.0; |
| // edge rather than running off it, where its handle is unreachable. | ||
| const int desiredWidth = | ||
| textEditWrapWidth_ > 0.0 | ||
| ? std::max(48, qRound(textEditWrapWidth_) + sidePadding * 2) |
| const QRectF originalBounds = annotationBounds(originalAnnotation_); | ||
| const qreal ratio = | ||
| originalBounds.width() > 0 | ||
| ? std::abs(point.x() - originalBounds.left()) / | ||
| originalBounds.width() | ||
| : 1.0; | ||
| annotation.size = | ||
| std::clamp(originalAnnotation_.size * ratio, 1.0, 24.0); | ||
| annotation.start.setY( | ||
| originalBounds.top() + | ||
| QFontMetricsF( | ||
| annotationTextFont(annotation.size, annotation.textFont)) | ||
| .ascent()); | ||
| annotation.textWidth = std::max<qreal>( | ||
| kMinimumTextWrapWidth, point.x() - originalBounds.left()); |
There was a problem hiding this comment.
🟡 Changes recommended
Two moderate text-resizing and draft-layout inconsistencies remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Balanced
| const int availableWidth = | ||
| std::max(48, qRound(editImageRect().right() - textEditor_->x())); | ||
| const int lineCount = std::max(1, static_cast<int>(lines.size())); | ||
| std::max(48, qRound(editImageRect().right()) - textEditor_->x() + | ||
| sidePadding); |
| annotation.textWidth = std::max<qreal>( | ||
| kMinimumTextWrapWidth, | ||
| point.x() - originalBounds.left() - 2.0 * padding); |
There was a problem hiding this comment.
🟡 Changes recommended
Two moderate text-layout and resize defects must be resolved before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Balanced
| const int desiredWidth = textEditWrapWidth_ > 0.0 | ||
| ? std::max(1, qRound(textEditWrapWidth_ * editScale())) + sidePadding * 2 | ||
| : std::max(48, widestLine + sidePadding * 2); |
| annotation.textWidth = std::max<qreal>( | ||
| kMinimumTextWrapWidth, | ||
| originalAnnotation_.textWidth > 0.0 | ||
| ? originalAnnotation_.textWidth + point.x() - dragStart_.x() | ||
| : point.x() - originalBounds.left() - 2.0 * padding); |
Long text now wraps at the current canvas boundary while it is being typed, keeping the inline editor and rendered output readable. Hard newlines and word wrapping compose: paragraphs split on explicit newlines first, ordinary words wrap at spaces, and a single over-long word breaks only when necessary.
This update also makes wrapping compose with the expandable canvas already on
main:textWidth = 0is unbounded, so text that did not need wrapping can be dragged past an edge and grow the canvasThe operation log remains the source of truth; preview, bounds, export, move, resize, undo, and redo all use the same text layout behavior.
Test plan
make check