Selecting text without custom ::selection styling changed the
foreground color of the selected content. This was especially visible
for links, where the text changed color but the underline did not.
The default selection style supplied both a selection background and a
foreground color from the palette or HighlightText system color. That
made ordinary selections behave as if the page had explicitly styled
::selection color.
Only provide a default selection background, so selected content keeps
its own foreground color unless CSS overrides it. Remove the now-unused
SelectionText palette role.
The border-radius overlap algorithm scales by L_i / S_i for each side
whose adjacent radii exceed the side length. If layout produced a
negative border box dimension, a side with a zero radius sum could be
mistaken for an overflowing side and passed to CSSPixelFraction as a
zero denominator.
Use non-negative side lengths for the normalization step and only divide
by nonzero radius sums. Add reduced crash coverage for an output element
whose negative letter spacing produces this geometry during painting.
The accumulated visual context tree used index 0 as a null sentinel, so
visual viewport transforms were only represented by adding a normal
transform node when the transform was non-identity. That made callers
treat index 0 as a special no-context value and kept the tree shape
dependent on the visual viewport state.
Reserve index 0 as the visual viewport transform node instead. AVC
traversal, display-list replay, hit testing, debug dumping, and root
paint state now treat that node as a real root. Rebaseline the affected
display-list and async-scrolling text expectations so the explicit root
node appears in AVC dumps.
Build a hit-test display list while recording paint output. Use it as
source of truth for point hit testing instead of recursively walking the
paintable tree in reverse paint order.
The retained list records target paintables, visual context indices,
border radii, caret rects, and line metadata needed by hit testing. It
also keeps a spatial index so point queries inspect nearby items before
checking containment in paint order.
Refresh scroll state before hit testing so visual context transforms use
current scroll offsets. Add text tests for rounded hit regions and
selection across non-text content.
Use the page palette for default selection backgrounds instead of a
hardcoded blue color. Tint opaque selection backgrounds before painting.
Selected images stay visible through the overlay.
Use SelectionText for default selected text and HighlightText. Authored
::selection colors still take precedence. On macOS, use a light
translucent color matching other engines.
Update selection and system-color baselines for the themed colors. Add
macOS-specific screenshot baselines for tests whose expectations depend
on system colors.
Paint grid overlays with the same visual context as the normal inspector
overlay, so scroll frames and transforms affect highlighted grids the
same way they affect page content.
Also support optional line numbers, named areas, infinite lines, and
track size labels.
We introduce a paint_with_inspector_overlay_context() helper to share
the significant amount of code to paint overlays with the correct
context.
We now apply first letter styles by splitting text with a first-letter
style applied into 2 `TextSliceNode` objects. The
`DOM::Text` layout node always points at the non first-letter slice
and the first-letter slice is reachable via
`TextSliceNode::first_letter_slice()`.
First letter splitting works by `TreeBuilder` walking a block
container's inline descendants to find the first typographic letter
unit per the pattern given in css-pseudo level 4, which is then
wrapped in an anonymous inline box styled with the `::first-letter`
computed properties.
Consumers that map between DOM offsets and layout geometry
are updated to visit all slices of a `DOM::Text` through
`TextOffsetMapping`.
Wheel deltas were truncated to int at the platform input boundary,
which dropped the sub-pixel tail of trackpad momentum scrolls. Each
NSEvent's scrollingDeltaY arrived as a CGFloat, got cast to int, and
flowed through IPC, EventHandler, and PaintableBox::scroll_by as int,
losing fractional information that never came back.
Widen Web::MouseEvent::wheel_delta_{x,y} to double and propagate
through Page, EventHandler, Paintable, PaintableBox, and the AppKit,
Qt, and GTK input paths.
Add a new visual context node type that negates a scroll frame's offset
during display list replay. This is the inverse of ScrollData: where
ScrollData translates by the scroll offset, ScrollCompensation
translates by the negated offset.
The Paintable tree and its supplemental painting data structures were
GC allocated because that was the easiest way to manage it and avoid
leaks introduced by ref cycles. This included the Paintable subclasses
themselves plus StackingContext, ChromeWidget, Scrollbar, ResizeHandle,
and scroll-frame state.
We are now trying to reduce GC allocation churn on layout and painting
updates, so keeping this short-lived rendering tree outside the JS heap
is a better fit. Move Paintable to RefCountedTreeNode, make painting
helpers ref-counted or weakly reference Paintables, and update the
layout and event-handler call sites to use RefPtr/WeakPtr ownership.
The visual context tree is expected to be non-null at all call sites.
Change the return type from raw pointer to reference with VERIFY(),
making the contract explicit and removing unnecessary null checks from
callers.
Implement support for the 'round' radii in 'clip-path: inset()'
by resolving and normalizing corner radii and generating a path
with elliptical arcs.
Add a screenshot test.
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.
Replace per-node heap-allocated AtomicRefCounted
AccumulatedVisualContext objects with a single contiguous Vector inside
AccumulatedVisualContextTree. All nodes for a frame are now stored in
one allocation, using type-safe VisualContextIndex instead of RefPtr
pointers.
This reduces allocation churn, improves cache locality, and opens the
door for future snapshotting of visual context state — similar to how
scroll offsets are snapshotted today.
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
Cache the display list commands produced by each PaintableBox's paint()
on a per-phase basis. On subsequent display list rebuilds, if a
paintable's cache is still valid, replay the recorded commands directly
— skipping paint() and all the property resolution it entails.
Besides saving time on property resolution, this also enables Skia to
reuse path tessellation results across frames — e.g. border paths are
preserved in the cache and don't need to be re-tessellated on every
repaint.
Rename Document::set_needs_display() to set_needs_repaint() and make it
private. External callers must now go through Node/Paintable which
route the request to the document internally.
Fix one existing misuse in AnimationEffect that was calling
document-level set_needs_display() instead of routing through the
target element's paintable.
This is preparation for per-paintable display list command caching:
repaint requests must go through specific paintables so their cached
command lists can be invalidated.
Paintable::display() was chasing through layout_node().display() which
goes through computed_values().display() on every call. Since display
never changes after the Paintable is constructed, cache it as a member
set in the constructor.
This was 1.6% of CPU time while playing a YouTube video.
Cache the result of the visibility+opacity check as a bit field
(m_visible) computed in resolve_paint_properties(), which already runs
before each paint when paint properties need updating.
This makes Paintable::is_visible() a simple inline bit field read
instead of chasing through layout_node->computed_values() every time.
This was 3.3% of CPU time while playing a YouTube video.
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.
When painting inspector overlays for a highlighted node, we want to
ignore any clipping that may be going on due to explicit CSS properties,
stacking contexts or similar. That makes sure our overlay is not
obscured by the clipping.
This fixes a regression from 009ddd4823.
When highlighting something that does not correspond to a
`PaintableBox`, we still need to apply `AccumulatedVisualContext`s. In
that case, search for the closest `PaintableBox` ancestor and apply its
`AccumulatedVisualContext`
This bug is most easily observed by highlighting a text node on a page
that has a scrollbar and is scrolled down at least a bit.
This fixes a regression from 009ddd4823.
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.
...instead of reaching into DOM tree during hit-testing in order to
figure out if an element is inert. This is a part of the effert to make
possible running hit-testing solely based on data contained by the
paintable tree.
Before this change, we always updated paint-only properties for every
single paintable after layout or style changes.
This could get very expensive in large documents, so this patch makes
it something we can do partially based on "repaint" invalidations.
This cuts down time spent in paint-only property update when scrolling
https://imdb.com/ from 19% to 5%.
The overlay shown for the node hovered in the inspector is painted as
part of the normal tree traversal of all paintables. This works well in
most cases, but falls short in specific scenarios:
* If the hovered node or one of its ancestors establishes a stacking
context and there is another element that establishes a stacking
context close by or overlapping it, the overlay and especially the
tooltip can become partially hidden behind the second element. Ditto
for elements that act as if they established a stacking context.
* If the hovered node or one of its ancestors involves clipping, the
clip is applied to the overlay and espicially the tooltip. This can
cause them to be partially invisible.
* Similarly, if the hovered node or one of its ancestors has a defined
mask, the mask is applied to the overlay, often making it mostly
invisible.
* No overlays are shown for SVG nodes because they are painted
differently from HTML documents.
Some of these problems may be fixable with the current system. But some
seem like they fundamentally cannot work fully when the overlays are
painted as part of the regular tree traversal.
Instead we pull out painting the overlay as a separate pass executed
after the tree traversal. This way we ensure that the overlays are
always painted last and therefore on top of everything else. This also
makes sure that the overlays are unaffected by clips and masks. And
since overlay painting is independent from painting the actual elements,
it just works as well.
However we need to be careful, because we still need to apply some of
the steps of the tree traversal to get the correct result. Namely we
need to apply scroll offsets and transforms. To do so, we collect all
ancestors of the hovered node and apply those as if we were in the
normal tree traversal.
We used to only walk the paintable root tree from the layout root and
detach paintables from there. In some cases, this could leave paintables
behind, so we added another loop that iterates over all layout nodes and
detaches their paintables, if any remained.
Instead of traversing two trees like this, just traverse the layout tree
once and detach the inclusive descendant's paintables, similar to how we
deal with the DOM tree immediately after that.
Fixes rendering of elements with large border-radius values by scaling
radii proportionally when they exceed element dimensions per CSS spec.
Co-authored-by: Samyat Gautam <thesamyatgautam@gmail.com>
This reverts commit 7dc8062283.
This did not propagate correctly to paintables whose style was inherited
from an ancestor, causing rendering artifacts on https://linear.app/
Before committing a new layout (and thus building a new paint tree)
we now go through both the old paint tree and the layout tree and detach
them from each other.
This is a little extra work, but it ensures that there are no lingering
references across the trees, which we were apparently accumulating in
some cases on Discord, causing GC leaks.
For every invocation of `::before_paint()` and `::after_paint()`, we
would reach into the node's computed values to determine its visibility.
Let's just do this once during construction of the paintable instead,
since this was showing up in profiles.
The `cursor` property accepts a list of possible cursors, which behave
as a fallback: We use whichever cursor is the first available one. This
is a little complicated because initially, any remote images have not
loaded, so we need to use the fallback standard cursor, and then switch
to another when it loads.
So, ComputedValues stores a Vector of cursors, and then in EventHandler
we scan down that list until we find a cursor that's ready for use.
The spec defines cursors as being `<url>`, but allows for `<image>`
instead. That includes functions like `linear-gradient()`.
This commit implements image cursors in the Qt UI, but not AppKit.