Skip to content

Implement text-overflow - #893

Open
LinuxBoy-96 wants to merge 5 commits into
DioxusLabs:mainfrom
LinuxBoy-96:text-overflow
Open

LinuxBoy-96 wants to merge 5 commits into
DioxusLabs:mainfrom
LinuxBoy-96:text-overflow

Conversation

@LinuxBoy-96

@LinuxBoy-96 LinuxBoy-96 commented Sep 14, 2026

Copy link
Copy Markdown

Overflowing lines in a clipping inline root (overflow other than visible)
are cut at a glyph boundary and end with the text-overflow marker.
Design discussed in #<numéro de l'issue ellipsis>: paint-time truncation
(CSS UI §5.2 defines the ellipsis as a rendering effect), confirmed by an
independent implementation in the same thread. The Parley route remains the
long-term home; this is the in-Blitz implementation.

How

  • blitz-dom: TextLayout::text_overflow records the marker for the inline
    root when style asks for one (construct.rs::text_overflow_marker): None
    for clip, "…" for ellipsis, the given string otherwise. Stylo keeps a
    single value in second (with sides_are_logical), two values as
    (start, end): second is always the inline-end side. When the inline root
    is an anonymous block (Blitz wraps inline content inside scroll containers),
    the marker and the available width come from the parent element, which is
    the one that clips.
  • blitz-paint::text::stroke_text takes an optional TextOverflowClip
    (content-box width in device pixels + marker). For each line wider than
    the content box: glyphs ending past max_width - marker_advance are
    dropped, and the marker is drawn once right after the kept glyphs, with the
    first run's font, size, colour and synthesis (glyph lookup through
    skrifa; fonts without U+2026 fall back to ...).
  • Layout is untouched: scrollWidth, getBoundingClientRect, selection and
    hit testing keep the full text.
  • Decorations (underline, line-through) stop at the cut; the marker is drawn
    undecorated, as in Chrome.
  • Truncation is at glyph boundaries, so an unbreakable word that does not fit
    whole still fills the remaining space glyph by glyph before the marker.
  • The classic trigger is overflow (non-visible) + white-space: nowrap +
    text-overflow: ellipsis; wrapped text overflows only with unbreakable
    words, handled the same way.

Limitations

  • Only the inline-end side is handled (no RTL / start-side marker).
  • Inline boxes after the cut are still painted by the box painter (they are
    not glyph runs).

Tests

  • Manual page covering ellipsis, clip, string marker, underline, unbreakable
    word, font size, fitting text and wrapped text (in the PR thread).
  • WPT: css/css-overflow/text-overflow-*, css/css-ui/text-overflow-*.

WPT results

Subtests: 6 newly passing, 2 newly failing (net +4).

Full diff (8 changed tests)
+ FAIL => PASS  [1/1]  +1  css/css-overflow/line-clamp/line-clamp-auto-010.html
+ FAIL => PASS  [1/1]  +1  css/css-overflow/line-clamp/webkit-line-clamp-037.html
+ FAIL => PASS  [1/1]  +1  css/css-overflow/text-overflow-007.html
- PASS => FAIL  [0/1]  -1  css/css-overflow/text-overflow-008.html
+ FAIL => PASS  [1/1]  +1  css/css-overflow/text-overflow-014.html
+ FAIL => PASS  [1/1]  +1  css/css-overflow/text-overflow-029.html
+ FAIL => PASS  [1/1]  +1  css/css-overflow/text-overflow-ellipsis-001.html
- PASS => FAIL  [0/1]  -1  css/css-overflow/text-overflow-ellipsis-editable-div-with-caret.html

Generated by the WPT workflow.

@nicoburns nicoburns left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm onboard with implementing this in Blitz. However, can we make it run at mostly post-layout time (the marker computation, shaping, and positioning should run in inline.rs, just after the Parley layout has run, and then be stored on the TextLayout in an Option<Box<T>> field.

We will of course need a small amount of paint code to consume this data.

May also be worth consolidating most of the (non-paint) code into it's own module to make it easier to upstream into Parley in future if we decide we want to.

Comment thread packages/blitz-dom/src/layout/construct.rs Outdated
@LinuxBoy-96

Copy link
Copy Markdown
Author

Well it still work well on my end.

BTW here is the file I have for testing it.
ellipsis-test.html

@nicoburns

nicoburns commented Sep 14, 2026

Copy link
Copy Markdown
Member

Some AI review comments:

  1. Applies to flex/grid containers (WPT regression css-flexbox/text-overflow-on-flexbox-001). The "anonymous root → read the parent's style" fallback in construct.rs doesn't distinguish anonymous blocks wrapping mixed block/inline children (where the block container legitimately owns the ellipsis) from the anonymous wrappers Blitz creates around text items in flex/grid containers (text_item_needs_wrap). text-overflow only applies to block containers, so the fallback should require the parent's display.inside() to be Flow/FlowRoot.

  2. Ignores scroll offset (WPT regression css-overflow/text-overflow-021). cut_x/marker_x are in layout coordinates and fixed at layout time, so a scrolled overflow: scroll box still cuts at the original right edge and shows a marker even when the end of the content is in view (ellipsis-scrolling). Scrolling doesn't relayout, so the cut needs to be shifted by scroll_offset.x (and the marker skipped when line.advance <= scroll_x + width). The shaped marker and per-line advance can still be computed post-layout, but the cut position / marker_x probably need to be resolved (cheaply) at paint time.

  3. Stale text_overflow on style-only changes. TextLayout::text_overflow is captured in build_inline_layout_into (box construction). A change to only text-overflow / overflow produces RELAYOUT damage (compute_layout_damage), which re-runs inline.rs but not construction, so the stored side is stale. Simpler to drop the field and read primary_styles() directly in inline.rs where compute() is called.

  4. Marker never drawn if the cut lands inside an inline box (<img>, inline-block) rather than a glyph run: paint only emits the marker after a glyph run whose end reaches cut_x. Lines with no glyph runs are also skipped by compute. At minimum this should be listed under Limitations.

2 and 3 are the most important here (and apologies as 2 partially contradicts my earlier request to do this post-layout, but it's important that scrolling is kept cheap!)

@nicoburns

Copy link
Copy Markdown
Member

And a couple more:

  • The marker is drawn with run.normalized_coords() of the run reaching the cut, but with the font of the first run — mismatch for variable fonts / mixed fonts on one line.
  • line.metrics().advance includes trailing whitespace, so a line with trailing spaces (white-space: pre) can be flagged as overflowing; consider advance - trailing_whitespace.

@LinuxBoy-96

Copy link
Copy Markdown
Author

It work now I think :')

@nicoburns

Copy link
Copy Markdown
Member

Some more feedback from digging into why this isn't causing more passing tests. 1 is the big thing that I think we definitely ought to fix.

1. The marker must use the block container's style, not the first run's

css-overflow §5.2: "the ellipsis is styled and baseline-aligned according to the block". compute() shapes the marker with the font/size/coords of the line's first glyph run, and paint draws it with that run's colour and baseline. This is what fails text-overflow-002, -004, -013, -022, text-overflow.html and all ~25 text-overflow-string-* tests: they put a 30px Ahem <span> inside a 10px block, and the reference expects a small ellipsis / "123" / "Hi Hi" in the block's 10px font after the black box. Blitz shapes /123 in 30px Ahem instead, so the marker comes out as more black boxes. 013 (span with vertical-align: sub) and 022/012 (red span in green block) additionally show the baseline and colour also need to come from the block.

Fixing this would flip most of these tests to pass. Suggestion: rather than the skrifa charmap lookup, shape the marker with a tiny Parley layout built from the inline root's style (stylo_to_parley::style(root_styles) + layout_ctx.ranged_builder(...)). That also gives you proper font fallback for free, so the ... fallback hack goes away, and the marker's brush/baseline come from the root.

2. Overflow check ignores the line's start offset (text-indent)

compute() compares metrics.advance - trailing_whitespace with max_width, but the line may not start at 0. text-overflow-ellipsis-indent-001:

  • text-indent: 3ch + 1234 (4ch) in a 6ch box: content ends at 7ch → should be 1…, Blitz doesn't truncate (advance 4ch ≤ 6ch).
  • text-indent: -3ch + 123456789 (9ch): content ends at 6ch → fits, Blitz truncates it (advance 9ch > 6ch).

Should compare the line's end x (metrics.offset + advance - trailing_whitespace) against max_width, and resolve() should likewise measure from the line end rather than assuming it starts at 0.

3. Cut granularity: grapheme clusters, not glyphs

text-overflow-012: a Thai base + combining vowel ( + ) is cut between the two glyphs and only is left before the marker. The spec says to elide whole characters (grapheme clusters). Parley exposes clusters (run.clusters() / cluster.is_word_boundary() etc.), so cutting at cluster boundaries rather than positioned_glyphs() boundaries should be straightforward.

@LinuxBoy-96

Copy link
Copy Markdown
Author

@nicoburns

  1. Marker styled by the block container: shaped once per inline context via a
    small Parley layout from the block's stylo_to_parley style (font, size,
    coords, fallback — no more skrifa lookup or ... hack), painted in
    the block's colour on the line's baseline.
  2. Overflow measured at the line's end (offset + advance − trailing whitespace), so text-indent counts.
  3. Cuts on grapheme-cluster boundaries.

text-overflow-002: the marker now matches the reference, but the test still
fails on a ~6px vertical offset of the whole line that main shows too
(30px inline in a 10px block inside a scroll container) — independent of
this PR. text-overflow-021 passes. css/css-overflow text-overflow tests:
20 on main → 22 here, no regression.

@nicoburns nicoburns changed the title Implement text-overflow: ellipsis (and the string form) Implement text-overflow Sep 15, 2026

This branch has not been deployed

No deployments
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.

2 participants