Skip to content

Paragraph scroll, Clear and Fill - #78

Merged
ralyodio merged 1 commit into
mainfrom
feat/text-primitives
Sep 8, 2026
Merged

Paragraph scroll, Clear and Fill#78
ralyodio merged 1 commit into
mainfrom
feat/text-primitives

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Three of the smaller gaps in #64, taken together because they are all about the surface a widget sits on rather than the widget.

Paragraph scroll

drawText wrapped and then drew from the first line. Long wrapped text could be displayed but not scrolled: you had to pre-slice the string, which is wrong the moment the width changes and the line count changes with it.

ui.text(prose, { wrap: true, scroll: offset });

scroll and scrollX are applied after wrapping, which is the only place either can be correct — the caller does not know how many lines their text became.

Clear and Fill

clear resets a region so an overlay can own it. modal already did this privately; without it in the open, anything else that floats is drawn into whatever it lands on and shows the widget underneath through the gaps between its words. fill floods a region with one repeated symbol.

Two details that needed deciding

A horizontal scroll can land in the middle of a double-width character. It cannot draw half of one, so what is left of that character is a space — which is what a terminal shows when a wide cell is clipped. That is dropColumns, new in every port's unicode layer, and it walks graphemes because a byte slice would cut inside one and corrupt it. Styled text needed the same thing without losing the styles of the runs that survive, so dropSpanColumns cuts inside whichever run straddles the offset and keeps its style for the remainder.

A fill with a wide symbol steps over both cells rather than writing one glyph per column: each glyph owns a continuation cell, and writing the next on top of it leaves a row of half-characters. Where the region does not divide evenly the last glyph is dropped rather than clipped, because half a wide character is not a fill, it is damage.

Scope

Six ports and seven new fixtures, all additive — nothing existing moved. This is one slice of #64; the calendar and the bounds-aware canvas are still open there, which is why this closes nothing.

Verified

  • 310 TypeScript tests under both bun test and node --test, typecheck clean
  • 76 widget scenes matching the reference in Rust, Go, Python, Zig and C++ (was 69)
  • 11/11 ctest, every widget gallery, next build for the site

🤖 Generated with Claude Code

https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy

Three of the smaller gaps in #64, taken together because they are all about
the surface a widget sits on rather than the widget.

`drawText` wrapped and then drew from the first line. Long wrapped text could
be displayed but not scrolled: you had to pre-slice the string, which is wrong
the moment the width changes and the line count changes with it. `scroll` and
`scrollX` are applied after wrapping, which is the only place either can be
correct -- the caller does not know how many lines their text became.

`clear` resets a region so an overlay can own it. `modal` already did this
privately; without it in the open, anything else that floats is drawn *into*
whatever it lands on and shows the widget underneath through the gaps between
its words. `fill` floods a region with one repeated symbol.

Two details that needed deciding rather than assuming.

A horizontal scroll can land in the middle of a double-width character. It
cannot draw half of one, so what is left of that character is a space -- which
is what a terminal shows when a wide cell is clipped. That is `dropColumns`,
new in every port's unicode layer, and it walks graphemes because a byte slice
would cut inside one and corrupt it. Styled text needed the same thing without
losing the styles of the runs that survive, so `dropSpanColumns` cuts inside
whichever run straddles the offset and keeps its style for the remainder.

A fill with a wide symbol steps over both cells rather than writing one glyph
per column: each glyph owns a continuation cell, and writing the next on top
of it leaves a row of half-characters. Where the region does not divide evenly
the last glyph is dropped rather than clipped, because half a wide character
is not a fill, it is damage.

Six ports and seven new fixtures, all additive -- nothing existing moved.

Verified: 310 TS tests under bun and node; 76 widget scenes matching the
reference in Rust, Go, Python, Zig and C++; 11/11 ctest; every gallery; the
site builds.

Part of #64

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy
@ralyodio
ralyodio merged commit 3e3d799 into main Sep 8, 2026
18 checks passed
@ralyodio
ralyodio deleted the feat/text-primitives branch September 8, 2026 23:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant