#461

A lone CR in a textarea is laid out as one line but drawn as two on macOS, so rows overprint

Opensepehr-safariopened 1mo ago

Pasting text whose paragraphs are separated by a lone \r into a textarea draws the words after each CR one row lower, on top of the next wrapped row. The text itself is intact; only the drawing is wrong. U+2028 and vertical tab (0x0B) do the same. LF and CRLF draw correctly.

Seen with CLI 0.10.1 on macOS; the code below is as of 6b05318 on main.

Reproduction. A fixed-width textarea (mine is 484pt), four paragraphs of prose separated by a single \r, each long enough to wrap two or three times. Paste it (native automate widget-key <view> cmd+v reproduces it too). In the host render the first words of each paragraph after the first are drawn over that paragraph's first wrapped row.

What I found in the source

  • Layout breaks a line only at LF (src/primitives/canvas/text_layout.zig:761, :793, :840) and wraps only at space or tab (isTextBreakByte, :1088). So end.\rNext is one unbreakable word on one row.
  • The macOS host draws a row through NSLayoutManager (src/platform/macos/appkit_host.m:2093-2106), which treats \r as a paragraph break, so the part after the CR is drawn a line lower.
  • Width measurement on macOS uses sizeWithAttributes: (appkit_host.m:2539), which returns the width of the widest line for a string containing \r, so measureTextAdvance (src/primitives/canvas/text_metrics.zig:98) gives every character after a CR an advance of 0. The reference renderer (native automate screenshot) accordingly draws those characters collapsed into one cell.
  • The paste keeps \r as pasted.

U+2029, U+0085 and FF should behave the same (NSLayoutManager breaks at them and the layout does not); I have only read that, not pasted it.

Possible fixes. Treat a lone CR (and the other separators) as a hard break wherever LF is one, or normalize them to LF when text enters a textarea. Either way, the text handed to the host for one row should not contain a character the host breaks at.

A related gap. A TEA app cannot tell a textarea where its caret is: Widget has text_selection, but ui.el's ElementOptions has no field for it and widgetFromOptions never sets it. So when an app's model text differs from the editor's copy (here, because the app normalized a paste), the editor takes the app's text, puts the caret at the end, and its undo history for the field restarts. Letting ElementOptions carry a text_selection through to the widget would let an app keep the caret where the edit left it.

In Plaza I work around it by turning those separators into LF before an edit reaches the model, leaving CRLF alone, and moving the model's caret to the end when a paste had to change (zig-nostr/plaza#388).

Activity

0

No activity yet.