Skip to content

Add Cluster::from_point_exact method for hit-testing spans#447

Merged
nicoburns merged 1 commit into
linebender:mainfrom
nicoburns:exact-cluster-from-point
Oct 31, 2025
Merged

Add Cluster::from_point_exact method for hit-testing spans#447
nicoburns merged 1 commit into
linebender:mainfrom
nicoburns:exact-cluster-from-point

Conversation

@nicoburns

Copy link
Copy Markdown
Collaborator

Motivation

The existing Cluster::from_point method 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 None in the case that the cursor is not directly over a cluster.

Changes made

  • Move the implementation of Cluster::from_point into a new private function Cluster::from_point_impl that 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).
  • Make Cluster::from_point simply call into Cluster::from_point_impl with exact: false
  • Add a new public function Cluster::from_point_exact that calls into Cluster::from_point_impl with exact: true

This 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

Signed-off-by: Nico Burns <nico@nicoburns.com>

@dfrg dfrg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the detailed explanation. LGTM

@nicoburns nicoburns added this pull request to the merge queue Oct 31, 2025
Merged via the queue into linebender:main with commit aa2bfa6 Oct 31, 2025
23 checks passed
@nicoburns nicoburns deleted the exact-cluster-from-point branch October 31, 2025 10:21
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>
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