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.
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.
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.
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.
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.
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.
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.
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.
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.
Reuse existing paintables during relayout to reduce GC allocation
pressure. Each paintable subclass implements reset_for_relayout()
to clear state before reuse.
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.
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.