Skip to content

Add inline-axis edges (inline_start / inline_end) to style spans - #83

Open
nicoburns wants to merge 6 commits into
mainfrom
devin/1790968000-inline-span-edges
Open

nicoburns wants to merge 6 commits into
mainfrom
devin/1790968000-inline-span-edges

Conversation

@nicoburns

@nicoburns nicoburns commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

LLM Contributions: This PR, including this description, was written by Devin (an LLM agent) at the request of @nicoburns. The description would need rewriting by a human before going upstream, per the Linebender LLM policy.

Stacked on linebender#873 (LineMetrics::collapsible_hanging_advance, rebased on upstream main), which is the first commit here and is used to leave collapsible hanging whitespace out of span fragments.

Lets a TreeBuilder style span reserve space before and after its content, which is what CSS inline-axis margin + border + padding on a non-atomic inline box needs. The span stays non-atomic: its text is shaped, wrapped and reordered as before.

API

  • TextStyle::inline_start / inline_end and StyleProperty::InlineStart / InlineEnd. Not inherited: push_style_modification_span resets them unless the modification list sets them, and they are ignored on the root style. Only the tree builder supports them.
  • Line::span_fragments() yields a SpanFragment (style_index, x, advance, baseline, ascent, descent, has_start_edge, has_end_edge, is_rtl) for every push_style_span span with content or an edge on the line, parents before children. Spans without edges get fragments too, so a consumer can use one path for backgrounds and hit rects.
  • Layout::span_edges_mut() to resize edges before (re)breaking lines, analogous to inline_boxes_mut().

Behaviour (matches what Chrome does for box-decoration-break: slice)

  • An edge is a LayoutItemKind::SpanStart / SpanEnd item with a fixed advance. Items are only created for spans with a non-zero edge or with no content, so other spans cost nothing in shaping or line breaking.
  • A span boundary is not a soft wrap opportunity. The start edge stays on the line of the content after it (a break before that content is taken before the edge), and the end edge on the line of the content before it (if it does not fit, the previous break opportunity is taken). A span with no content stays with the content before it.
  • A wrapped span has its start edge only on its first line and its end edge only on its last.
  • A non-zero edge is a shaping boundary and makes an otherwise empty line count as having content. Whitespace before a non-zero edge does not hang.
  • Edges are included in min/max content widths.
  • Collapsible whitespace stays on the side of the boundary it was pushed on.
  • Bidi: after the items of a line are reordered, each span's edges are moved to the visual ends of its content on that line, in the paragraph direction (start edge on the left in an LTR paragraph), inner spans first. Text order is never changed by an edge.
  • When reordering splits a span into visually discontiguous pieces, it gets one fragment per piece; the edges belong to the outermost pieces.
  • Fragments exclude collapsible whitespace hanging at the end of a line, but include preserved whitespace.

Not done

  • Spans have no direction of their own: edges are placed in the paragraph direction, and SpanFragment::is_rtl is the paragraph's direction. In CSS the sides depend on the span's own direction.
  • box-decoration-break: clone.
  • An empty span without edges still gets (zero-advance) edge items so that it has a fragment, which makes it a shaping boundary.
  • span_edges_mut() only covers spans built with a non-zero edge or without content.

Tests are in parley_tests/tests/styles/span_edges.rs; they compare against reference layouts rather than snapshots.

Changelog

Added

  • TextStyle::inline_start / inline_end (and the matching StyleProperty variants): advances before and after a TreeBuilder style span's content, for CSS inline margin/border/padding. Line::span_fragments returns the per-line extent of each span, and Layout::span_edges_mut allows the edges to be resized after building.
  • Breaking change: TextStyle has two new fields.

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/38d20c11138046c5a04fb2aa400e7033
Open in Devin Desktop: https://dioxus.staging.devinenterprise.com/desktop/session/38d20c11138046c5a04fb2aa400e7033?variant=devin-insiders

@staging-devin-ai-integration

Copy link
Copy Markdown

I'll fix CI failures and address comments from users with write access that start with 'Devin'.

  • Disable automatic comment, CI, and merge conflict monitoring

Report the collapsible white space at the end of a line separately from
the rest of the hanging white space (preserved white space and spaces
that never collapse, like the ideographic space).
Style spans can now have an `inline_start` and `inline_end` advance
(the inline-axis margin, border and padding of a CSS inline box). They
are laid out as `SpanStart`/`SpanEnd` items that are not soft wrap
opportunities and stay on the line of the content next to them.
`Line::span_fragments` reports the per-line extent of every span box.
- Edges are moved to the visual ends of their span box after bidi reordering,
  in the direction of the paragraph (CSS 2 § 8.6), and a span box split by
  reordering yields a fragment for each visual piece. `SpanFragment::is_rtl`
  is now the direction of the paragraph.
- Only collapsible hanging whitespace is excluded from span fragments:
  preserved whitespace is part of the fragment.
- An empty span stays on the line of the content preceding it.
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1790968000-inline-span-edges branch from a1708fe to e6c2e55 Compare October 3, 2026 12: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