Commit Graph

29 Commits

Author SHA1 Message Date
Jelle Raaijmakers
b38b7d0ae3 LibWeb: Draw text decoration lines with the right offset
For some text decorations (e.g. underline) we were using the line's top
edge as the Y-coordinate to draw the line at, which combined with the
line's thickness meant that it was positioned too high up.

Correct this by calculating the line's center Y position.
2026-03-26 12:15:36 +00:00
Jelle Raaijmakers
cd7210cc8e LibWeb: Clean up resolve_text_fragment_properties()
No functional changes.
2026-03-26 12:15:36 +00:00
Jelle Raaijmakers
3d2571b46e LibWeb: Implement text-decoration-skip-ink painting
Use Skia's SkTextBlob::getIntercepts() to find where glyph outlines
cross the underline/overline band, then split the decoration line into
segments with gaps around those intersections.
2026-03-26 12:15:36 +00:00
Luke Wilde
df32da5e86 LibWeb: Make every HTMLElement potentially form-associated
This can be the case for form-associated custom elements, where any
HTML element can be form-associated.
2026-03-25 13:18:15 +00:00
Zaggy1024
d94293cc25 LibWeb: Use an abstract class for scrollbar and resize handle input
This unifies the implementations of the element resize and the scroll
mouse inputs, so the actual code involved in handling the events can
be simplified, and it only should require one object per event.

The cursor override is now part of this ChromeWidget class as well, so
that the hit test's cursor doesn't need to be passed around as much.
2026-03-17 04:01:29 -05:00
Aliaksandr Kalenik
b217c5a496 LibWeb: Add PaintableBox::transform_point_to_local_for_descendants()
Similar to transform_point_to_local(), but uses the descendants' visual
context which accounts for the element's own scroll offset. Used during
fragment hit testing in PaintableWithLines.
2026-03-11 02:31:30 +01:00
Aliaksandr Kalenik
21876bd011 LibWeb: Add PaintableBox::transform_point_to_local()
Extract the repeated pattern of transforming a screen position to local
coordinates via the accumulated visual context into a helper method.
This returns an Optional, unlike the existing
transform_to_local_coordinates() which falls back to the original
position. The old method is now implemented in terms of the new one.
2026-03-11 02:31:30 +01:00
Aliaksandr Kalenik
d49809cba3 LibWeb: Remove paint-only properties resolution phase
With per-paintable display list command caching now in place, the
separate paint-only properties resolution phase is no longer needed.
Resolution now happens inline during painting and its cost is amortized
since it only runs on cache miss.

Move all property resolution to point of consumption:
- is_visible() and visible_for_hit_testing() compute on the fly
- Filter resolution moved to assign_accumulated_visual_contexts()
- Border radii, outlines computed on access
- Box shadows, backdrop filter resolved inline during painting
- Background resolution moved into paint_background()
- Mask resolution moved to StackingContext::paint()
- Text fragment and SVG stroke properties resolved during painting
2026-03-04 19:35:45 +01:00
Aliaksandr Kalenik
0356f1b510 LibWeb: Assign visual context before paint() in paint_node()
Previously, PaintableWithLines switched the visual context partway
through paint() — using the element's own context for background/border
phases, then switching to the descendants' context mid-paint for
foreground content. Move this decision into paint_node() so the correct
context is set before paint() is called.

This is preparation for per-paintable display list command caching:
captured commands must be recorded under the correct visual context
from the start, with no mid-paint switches.
2026-03-04 19:35:45 +01:00
Jelle Raaijmakers
499ec26f97 LibWeb: Remove CSS pixel offsets from ScrollState
This was arguably put in a worse place by #8162; we mostly need the
device pixel offsets from the scroll state so keep track of those and
convert back to CSS pixels when necessary (i.e. scrollbar data).
2026-02-27 08:29:29 +01:00
Jelle Raaijmakers
90a211bf47 LibWeb: Use device-pixel coordinates in display list and AVC
Stop converting between CSS and device pixels as part of rendering - the
display list should be as simple as possible, so convert to DevicePixels
once when constructing the display list.
2026-02-26 07:43:00 +01:00
Timothy Flynn
a112eb4881 LibWeb: Ensure empty contenteditable boxes contain the hit test position
There are actually a couple of bugs here:

1. As of commit ebda8fcf11, editing hosts
   are now excluded from Node::is_editable. Since this special hit test
   handling is specifically for contenteditable nodes, we would not
   enter this branch for these nodes.

2. We were not checking if the contenteditable node actually contained
   the hit testing position. So if a page had multiple empty editable
   nodes, we would just return whichever was hit test first.

These bugs were exposed by 7c9b3c08fa.
This commit resulted in the text cursor hit test node being set as the
document focus node. If we returned the wrong result, we would not set
the correct node.

This was seen on discord, where clicking the message box would result in
the search box being focused.
2026-02-21 01:02:06 +00:00
Psychpsyo
30c1f2f938 LibWeb: Do not re-target hits to fragments with user-select:none
This makes text selection more usable around them, as demonstrated in
the added test case.
2026-02-16 08:39:28 +01:00
Jelle Raaijmakers
d3a2e4bbbb LibWeb: Draw caret for empty contenteditable elements
We don't have a text node in these, so no fragments either. Maintaining
a special case for this situation seems much simpler than reworking
`contenteditable`s to always have a fragment.
2026-02-11 11:17:27 +01:00
Jelle Raaijmakers
3bc4374344 LibWeb: Draw caret for empty <input>s and <textarea>s
Our change to generate spans in PaintableWithLines from fragments also
broke drawing the caret for empty <input>s and <textarea>s, since spans
was empty in that case.

Fix this by moving caret drawing to PaintableWithLines, and only
invoking it if we have a cursor position set.
2026-02-11 11:17:27 +01:00
Jelle Raaijmakers
b6deacf6b0 LibWeb: Support hit testing fully above fragments
This allows us to consider fragments for hit testing even though the
cursor moved fully above them. This will allow us to select all the
contents in a <textarea> when you drag the mouse all the way to the top,
beyond the <textarea>'s top edge.
2026-02-11 11:04:53 +01:00
Jelle Raaijmakers
2558d80288 LibWeb: Apply AVC for descendants when hit testing fragments
We were not applying the descendants' transformation when hit testing on
fragments, which meant selection offsets on scrolled containers were
off.
2026-02-11 11:04:53 +01:00
Jelle Raaijmakers
b9f5411967 LibWeb: Remove redundant condition from PaintableWithLines::hit_test()
`is_visible` is always true at this point.
2026-02-11 11:04:53 +01:00
Tim Ledbetter
07614a2e55 LibWeb: Don't crash when painting cursor in a vertical writing mode
This change removes an assertion, which is not true when a cursor is
painted in a vertical writing mode.
2026-02-11 00:29:50 +01:00
Jelle Raaijmakers
856091fb56 LibWeb: Implement support for text-decoration in ::selection 2026-02-06 10:47:50 +00:00
Jelle Raaijmakers
48aa199f09 LibWeb: Implement support for text-shadow in ::selection 2026-02-06 10:47:50 +00:00
Jelle Raaijmakers
98b4feb406 LibWeb: Apply CSS ::selection styling to text and form controls
Previously, text selection always used the system highlight color. This
implements support for the ::selection pseudo-element's background-color
and color properties.

For form controls like <input> and <textarea>, the selection style is
looked up on the shadow host element, since the actual text lives inside
their shadow DOM.

The text painting logic has been refactored to split fragments into
styled spans (before selection, selected, after selection) so that each
portion can be rendered with its appropriate colors, taking care not to
allocate in 99%+ of fragment rendering cases.
2026-02-06 10:47:50 +00:00
Jelle Raaijmakers
a048355e66 LibWeb: Prevent overlapping selection rects
We were always extending selection rects by 1, which caused some
unnecessary overlap between adjacent selections. This also happened
vertically between lines, because we were ceil()ing instead of rounding.
This makes the selection rects look much nicer.
2026-02-06 10:47:50 +00:00
Andreas Kling
c550301a04 LibWeb: Allow hit testing visible children of hidden elements
Previously, hit testing would return early for elements with
visibility: hidden, which prevented their visible children from being
hit. Now we traverse children even for hidden elements, allowing visible
descendants to be hit while still preventing the hidden elements
themselves from being hit.

The key changes:
- PaintableBox::hit_test() and PaintableWithLines::hit_test() no longer
  return early for hidden elements, but still skip chrome hit testing
  and the final hit result for them
- hit_test_fragments() now checks is_visible() on each fragment's
  paintable to skip hidden text

This matches the CSS specification where visibility is inherited but
children can override it with visibility: visible.
2026-01-25 10:55:30 +01:00
Andreas Kling
d91c646788 LibWeb: Fix hit testing for text inside stacking context roots
When an element creates a stacking context (e.g. via position: relative
with z-index), its text fragments were not being hit tested. This was
because PaintableBox::hit_test() returns early when it has a stacking
context, and StackingContext::hit_test() only iterated child paintables,
not the stacking context root's own fragments.

Fix this by extracting fragment hit testing into a new method
hit_test_fragments() on PaintableWithLines, and calling it from
StackingContext::hit_test() when the stacking context root is a
PaintableWithLines.
2026-01-25 10:55:30 +01:00
Aliaksandr Kalenik
d4feeb1cad LibWeb: Preserve paintable tree across relayouts
Reuse existing paintables during relayout to reduce GC allocation
pressure. Each paintable subclass implements reset_for_relayout()
to clear state before reuse.
2026-01-21 10:00:17 +01:00
Aliaksandr Kalenik
009ddd4823 LibWeb: Integrate AccumulatedVisualContext with display list
Integrate AccumulatedVisualContext with display list recording and
playback. This is the main commit of the refactoring that delivers the
architectural improvements enabled by AccumulatedVisualContext.

Recording changes:

Each display list command now stores a single
RefPtr<AccumulatedVisualContext> instead of separate scroll_frame_id
and ClipFrame. The recorder simply captures the current accumulated
context when appending commands.

The before_paint()/after_paint() hooks that pushed/popped scroll frame
IDs are replaced by directly setting accumulated_visual_context on the
recorder before painting each element.

Playback changes:

The display list player now uses LCA (Lowest Common Ancestor) based
traversal to switch between visual contexts efficiently. When
transitioning from context A to context B:

1. Find the LCA of A and B in the context tree
2. Pop (restore) states back to the LCA depth
3. Push (save + apply) states from LCA down to B

This approach minimizes redundant save/restore operations. For example,
when rendering siblings that share a common scroll container, the
player keeps that scroll state applied and only switches the divergent
parts of their context chains.

Key deletions:

- Remove translate_by() from all 45 display list commands - commands
  are now immutable
- Remove transform/perspective fields from PushStackingContext -
  transforms are tracked via AccumulatedVisualContext
- Remove push_scroll_frame_id()/pop_scroll_frame_id() from
  DisplayListRecorder
- Remove before_paint()/after_paint() hooks from Paintable
- Merge ApplyOpacity, ApplyCompositeAndBlendingOperator, ApplyFilter
  into single ApplyEffects command

Stacking context painting changes:

The StackingContext::paint() method is significantly simplified.
Instead of building a PushStackingContextParams struct with transform
matrices and pushing/popping stacking contexts, it now:

1. Sets the accumulated visual context (which already contains
   transforms)
2. Applies effects (opacity, blend mode, filters) if needed
3. Applies clip path if needed
4. Paints the content
5. Restores state

The visual state management that was interleaved throughout the
painting code is now handled uniformly by the context tree.
2026-01-15 19:50:53 +01:00
Jonathan Gamble
8f1cb4cbb0 LibWeb: Implement resizing for eligible elements and update scrollbars
Add ElementResizeAction to Page (maybe there's a better place). It's
just a mousemove delegate that updates styles on the target element.

Add ChromeMetrics for zoom-invariant chrome like scrollbar thumb
thickness, resize gripper size, paddings, etc. It's not user-stylable
but separates basic concerns in a way that a visually gifted
designer unlike myself can adjust to taste.

These values are pre-divided by zoom factor so that PaintableBox can
continue using device_pixels_per_css_pixel calls as normal.

The adjusted metrics are computed on demand from Page multiple times
per paint cycle, which is not ideal but avoids lifetime management and
atomics. Maybe someone with more surety about the painting flow control
can improve this, but it won't be a huge win. If profiling shows
this slowing paints, then Ladybird is in good shape.

Update PaintableBox to draw the resize gripper and deconflict
the scrollbars. Set apropriate cursors for scrollbars and gripper in
mousemove. We override EventHandler's cursor handling because nothing
should ever come between a man and his resize gripper.

Chrome metrics use the CSSPixels class. This is good because it's
broadly compatible but bad because they're actually different units
when zoom is not 1.0. If that's a problem, we could make a new type
or just use double.
2026-01-12 11:00:14 +00:00
Jonathan Gamble
555681bdb5 LibWeb: Split PaintableWithLines from PaintableBox
No functional changes. I just hope to improve code navigation.
2026-01-12 11:00:14 +00:00