Add Cluster::from_point_exact method for hit-testing spans#447
Merged
Conversation
Signed-off-by: Nico Burns <nico@nicoburns.com>
5 tasks
dfrg
approved these changes
Oct 31, 2025
dfrg
left a comment
Contributor
There was a problem hiding this comment.
Thanks for the detailed explanation. LGTM
github-merge-queue Bot
pushed a commit
that referenced
this pull request
Apr 17, 2026
## Related PRs - Depends on #420 - Depends on #447 - Fixes #99 - Fixes #283 ## Introduction This PR is aimed at enabling Blitz to implement [CSS floats](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Floats) on top of Parley. However, the changes made are not float-specific, and will also enable other advanced layouts such as those that have "exluded regions" or want to lay out text into a complex shape. ## Parley's existing architecture - While most users call the simple `Layout::break_all_lines(max_advance)` to perform line breaking, Parley does already have a lower-level `Layout::break_lines()` function which returns a `BreakLines` struct, on which you can call `BreakLines::next()`. Each call to `BreakLines::next()` lays out a single line and then returns. This PR extends the functionality of this method so that in addition returning when a line-break occurs, it will also return when: - It encounters a floated box - The configured `max_line_height` is exceeded - Parley's `Line` struct has `min_coord` and `max_coord` fields. ## Changes Made ### Per-line bounding box `max_advance` When using the `BreakLines` struct directly, the` max_advance` (max width) can now be configured on a per-line basis. This makes it possible to create layouts with lines broken to different lengths. Additionally, it is valid to adjust the `max_advance` midway through line-breaking a line (it is advised to ensure that the existing content of the line still fits, although nothing terrible will happen if you don't (the contents will just overflow)). ### Automatically determined (per-line) alignment width Related to the per-line max-advances, the width used for alignment is now per-line and determined automatically and is equal to the `max_advance` for that line (at the time that the line is "committed"). This means that each line is aligned within it's own width bound, separately from the other liens. This is really the only way to sensibly align lines of differing widths, but making alignment width automatic had previously been discussed as being desirable anyway. ### Added `max_line_height` Each line now has a `max_line_height`. If at any point the line exceeds this height, control flow will be yielded from `BreakLines::next()`, allowing the user to find a new, larger space to lay out into. By default, `max_line_height` is set to `f32::MAX` and thus has no effect. ### Customisable per-line x- and y-offset Parley previously assumed that every line started at x=0 and that the start of the next line would be immediately below the previous line. Parley now allows an x and y offset to be manually configured for each line. This does not affect line breaking, but is used when reporting the positions of glyphs and for selection. The x offset is in addition to and stored separately from the offset generated by alignment. ### Added `InlineBoxKind` enum A new `kind: InlineBoxKind` field has been added the `InlineBox` struct, where InlineBoxKind is defined as: ```rust enum InlineBoxKind { InFlow, OutOfFlow, CustomOutOfFlow, } ``` - `InlineBoxKind::InFlow` represents the existing kind of inline box that is is laid out in-flow with text like a `display: inline-block` box in CSS. - `InlineBoxKind::OutOfFlow` is assigned a position during layout in exactly the same way as an `InFlow` box. However it does not take up space or affect the position of other items (glyphs or boxes) in the layout. This corresponds to a `position: absolute` box in CSS. Blitz was previously representing this as a zero-sized box, but I have taken the opportunity here to represent it explicitly. - `InlineBoxKind::CustomOutOfFlow` is the box kind for a floated box. Parley does not attempt to lay these out at all. When it encountered a box of this kind it yields control flow back to the caller who then responsible for positioning the box, adjusting the line's position/size, and then resuming layout (details below). ## Tasks - [x] Allow boxes to marked as in-flow or out-of-flow - [x] Allow line-breaking to yield control flow when encountering a box - [x] Allow users of parley to the x/y position and the max-width of each line - [x] Update selection to account for line offsets - [x] Update alignment to be per-line --------- Signed-off-by: Nico Burns <nico@nicoburns.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The existing
Cluster::from_pointmethod is tuned for text-selection / caret positioning where in the case that the cursor is not positioned exactly above a cluster, you usually want to position the caret (or selection anchor/focus) relative to the nearest cluster.For example if you click the empty space at the end of a left-aligned line of text in an editable textbox, then you would expect the caret to be positioned at the end of the line of text.
However, this logic doesn't work very well when hit-testing clusters for "hover" or "click" functionality (e.g. click a link within a paragraph of text). In that case you usually simply want to return
Nonein the case that the cursor is not directly over a cluster.Changes made
Cluster::from_pointinto a new private functionCluster::from_point_implthat takes a boolean parameter which determines whether or not the match should be exact (good for hit-testing click/hover) or not (the existing functionality - good for text selection).Cluster::from_pointsimply call intoCluster::from_point_implwithexact: falseCluster::from_point_exactthat calls intoCluster::from_point_implwithexact: trueThis PR is non-breaking
Videos
Before:
Screen.Recording.2025-10-30.at.20.40.46.mp4
After:
Screen.Recording.2025-10-30.at.20.39.02.mp4