Skip to content

[DO NOT MERGE] Hang trailing whitespace (conditionally for last line in paragraph) - #3

Draft
conor-93 wants to merge 6 commits into
mainfrom
conor/upstream/withHangingWhitespaceFixes
Draft

[DO NOT MERGE] Hang trailing whitespace (conditionally for last line in paragraph)#3
conor-93 wants to merge 6 commits into
mainfrom
conor/upstream/withHangingWhitespaceFixes

Conversation

@conor-93

@conor-93 conor-93 commented May 14, 2026

Copy link
Copy Markdown
Owner

Latest branch (rebased off parley v11.0) here: linebender/parley@main...conor-93:parley:conor/upstream/withHangingWhitespaceFixes20260625 (removes extra cluster_idx bump)

Implements CSS Text 3 §4.1.2 trailing whitespace hanging behaviour:

  • Mid-paragraph (soft-wrapped) lines: trailing preserved whitespace is unconditionally hung (excluded from the line's measured width so alignment and justification ignore it (it overflows into the end margin).
  • Paragraph-final lines (forced break via \n or end-of-text): trailing whitespace is conditionally hung; it is included in alignment calculations, but clamped so visible content never gets pushed past the start edge on overflow (which would result in another soft-wrapped line if this happened in a mid-paragraph line, hence why this has to be handled differently for the final line).
  • Line breaking: trailing whitespace immediately before a forced break no longer triggers a soft wrap, even if it overflows the line width. The spaces accumulate on the current line for conditional hanging during alignment.
  • Justification: trailing hung spaces are excluded from the justification space distribution, so only inter-word gaps in visible content are expanded.

These behaviours are forced. This should not be merged as-is, but it could be used as inspiration for implementing configurable CSS white-space-collapse behaviours (issue: linebender#619), specifically:

  • Unconditional hanging on soft-wrapped lines
    • Required by WhiteSpaceCollapse::Collapse, WhiteSpaceCollapse::Preserve, WhiteSpaceCollapse::PreserveBreaks, WhiteSpaceCollapse::PreserveSpaces
  • Conditional hanging on paragraph-final (i.e. with BreakReason::Explicit or BreakReason::None) lines
    • IIUC, this is required by WhiteSpaceCollapse::Preserve and WhiteSpaceCollapse::PreserveSpaces
  • Trailing whitespace being excluded from justification distribution
    • Required by all because hung whitespace should never be considered in justification algorithm

@conor-93 conor-93 changed the title [NOT FOR MERGE] Hang trailing whitespace (conditionally for last line in paragraph) [DO NOT MERGE] Hang trailing whitespace (conditionally for last line in paragraph) May 14, 2026

@djmcnab-canva djmcnab-canva left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks pretty good. I definitely understand how this resolves.

One question: Did you evaluate using linebender#485 instead? In my reading, that looks like it solves the same issues with very similar approaches, but also handles the hanging clusters issue.

Comment thread parley/src/layout/alignment.rs Outdated
Comment on lines +107 to +111
// Hanging whitespace is an alignment/positioning concern, orthogonal to text
// directionality. The RTL handling here doesn't interact with bidi reordering:
// it simply shifts the line's origin so that hung trailing whitespace (which
// sits at the *start* edge in visual order for RTL) overflows into the start
// margin rather than displacing visible content.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This smells like a Claude artifact to me. I'd lean towards reverting this section.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I actually included this intentionally while I was working through the solution, as I was trying to figure out the implication the change would have on text directionality. Revisiting it now, it seems a bit overbearing, and I prefer the old comment.

Comment thread parley/src/layout/line_break.rs Outdated
Comment on lines +664 to +665
// TODO: can this be hoisted out of the conditional?
self.state.cluster_idx += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm slightly surprised that this actually gets all the remaining space clusters in the line; my impression was previously that the reason we were seeing the wrapping behaviour was because this is +1. That is, do we not need to consume clusters until we reach the last cluster on the line? We might need to do more digging

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

You're right, and though this was technically out of scope of this PR, I got this working (which correctly hangs leading whitespace, which is a separate issue we've been facing) by taking some inspiration from linebender#485 (see my main comment).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It isn't clear to me why this snapshot has been regenerated. Do you have that clear?

I can't see any actual changes, so I'm not overly worried.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Sub-pixel shifting in the horizontal position of "amet" on line two; all else looks the same.

conor-93 added 4 commits May 19, 2026 14:57
…just a single cluster (+ test demonstrating this bug)
…at a time without ever committing a new line until the contiguous whitespace section ends
@conor-93

Copy link
Copy Markdown
Owner Author

One question: Did you evaluate using linebender#485 instead? In my reading, that looks like it solves the same issues with very similar approaches, but also handles the hanging clusters issue.

Yes, early on when researching this task; it's targeting a different issue: breaking with extra, trailing whitespace would let the trailing whitespace form its own lines, which isn't an issue I'm able to re-create in our application.

The PR does achieve some of what we need, though; the main issue is that it doesn't implement conditional vs non-conditional whitespace hanging (i.e. hanging whitespace on all but the final line before a paragraph break, at which point whitespace is preserved up until the visible portion of a line collides with the margin, then it is hung). However, looking at this again with fresh eyes inspired me to simplify the whitespace cluster handling logic in line_break.rs, basically by consuming whitespace clusters without committing a new line, which means we don't need to look ahead (like with the previous approach I had) which added significant complexity to the whitespace handling in line_break. I think this is a lot better now.

@nicoburns

Copy link
Copy Markdown

This looks like the right basic approach.

A note that we should probably follow css-text-4 https://www.w3.org/TR/css-text-4/#white-space-phase-2 rather than css-text-3. The actual logic hasn't changed, but the spec has been rewritten in terms of the decomposed white-space-collapse style which makes it easier to follow (and closer to how we probably want to represent the styles in Parley).

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.

3 participants