Skip to content

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

Description

@sepehr-safari

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions