Let accumulated visual context updates keep the previous tree version
when rebuilt with the same shape. Display lists reference visual
context tree versions, so keep compositor-only updates on the old
version unless the tree structure changes.
Add coverage for version reuse and incompatible tree shapes.
Some paintables can carry sticky positioning without receiving sticky
inset data from layout. They cannot produce sticky constraints for async
scrolling.
Only create sticky scroll frames when sticky inset data exists. Add a
crash test for a sticky ruby paintable.
The inline and replaced paint phase walks every descendant even for
block-only contexts. Track whether a stacking context has inline or
replaced descendants while the stacking-context tree is built, then
skip the phase when it cannot produce display-list commands. This adds
one bool to StackingContext.
Visual viewport scroll and pinch zoom used to invalidate the full
accumulated visual context tree and display list, even though those
changes only modify the root visual viewport transform. That forced a
full display list rerecord before sending updated compositor state.
Patch the reserved visual viewport AVC node in place instead.
VisualViewport marks the tree for compositor update without
invalidating the display list, and Navigable sends the replacement tree
through the new IPC path before updating scroll state.
Problem: Select All draws the selection highlight across
user-select:none content — breaking compat with Chrome and Firefox,
which leave such content unhighlighted.
Cause: ViewportPaintable::recompute_selection_states walks the Range and
assigns SelectionState::Start|::Full|::End to each layout node within.
The walk filters out is_inert() nodes — but not user-select: none nodes.
Fix: Extend the existing inert-only guards into a helper that also
rejects nodes whose used value of user-select is ‘none’. Such nodes stay
at SelectionState::None from the initial reset — so the selection
highlight skips them.
Fixes https://github.com/LadybirdBrowser/ladybird/issues/9695
ViewportPaintable owned both the current visual context tree and the
details for constructing it, keeping CSS transform, clip, and effect
derivation mixed into the viewport paint lifecycle.
Move the construction helpers into AccumulatedVisualContext.cpp and
leave ViewportPaintable responsible for owning the resulting snapshot.
The visual viewport context index is now local builder state instead of
viewport object state.
Display lists owned the accumulated visual context tree through a
ref-counted pointer. That tied visual-context state to display-list
lifetime and made compositor updates treat the two as one unit, even
though AVC trees need to become independently updateable compositor
state.
Make accumulated visual context trees plain versioned values, have each
display list store the compatible tree version, and pass the matching
tree alongside display-list updates and replay calls. Replay verifies
that the provided tree matches the display list before executing it.
This prepares the compositor for receiving AVC tree updates separately
from display-list updates: it now accepts the tree as a separate update
parameter, stores it next to the display list, and uses that stored tree
for replay and async-scroll hit testing. Nested display-list resources
carry their own tree snapshots for the same version check.
Async scrolling display-list items were still recorded by helper
functions in LibWeb/Compositor even though they are produced while
walking the paint tree. That kept paint-time knowledge about boxes,
viewport state, and wheel-event regions in the compositor-facing code.
Move viewport setup and final metadata emission to ViewportPaintable,
and move per-box scroll-node, hit-test, scrollbar, wheel-region, and
sticky-area item recording to PaintableBox. AsyncScrollingState now
keeps the compositor-side conversion and lookup logic for consuming the
recorded metadata.
Parameter values are absolutized at style computation time (or are
confirmed to be resolvable in the case of reification or DOMMatrix) so
there is no reason this function should fail.
Previously, dissociating a range from a selectionleft stale selection
state on paintables and never requested a repaint, so the old highlight
remained visible until something else dirtied the viewport. We now
reset the per-paintable selection state and trigger a repaint on
dissociation.
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.
Instead of baking the current scroll offset into the background
positioning area at record time, use the `ScrollCompensation` visual
context node to negate ancestor scroll frames dynamically at replay
time. This keeps the background fixed relative to the viewport even
when the display list is cached and replayed at different scroll
positions, and works correctly with arbitrarily nested scroll
containers.
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.
Replace per-frame heap-allocated RefCounted ScrollFrame objects with a
single contiguous Vector<ScrollFrame> inside ScrollState. All frames for
a viewport are now stored in one allocation, using type-safe
ScrollFrameIndex instead of RefPtr pointers.
This reduces allocation churn, improves cache locality, and moves
parent-chain traversal (cumulative offset, nearest scrolling ancestor)
into ScrollState — similar to how visual context nodes were recently
consolidated into AccumulatedVisualContextTree.
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
Add unsafe_layout_node(), unsafe_paintable(), and unsafe_paintable_box()
accessors that skip layout-staleness verification. These are for use in
contexts where accessing layout/paintable data is legitimate despite
layout not being up to date: tree construction, style recalculation,
painting, animation interpolation, DOM mutation, and invalidation
propagation.
Also add wrapper APIs on Node to centralize common patterns:
- set_needs_display() wraps if (unsafe_paintable()) ...set_needs_display
- set_needs_paint_only_properties_update() wraps similar
- set_needs_layout_update() wraps if (unsafe_layout_node()) ...
And add Document::layout_is_up_to_date() which checks whether layout
tree update flags are all clear.
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.
Move the inline dom_node() method to Viewport.cpp so the header no
longer needs the full Document definition. Add explicit includes to
files that relied on the transitive dependency.
Previously, absolutely positioned elements jumped directly to their
containing block's accumulated visual context, skipping effects
(opacity, mix-blend-mode, isolation) from intermediate ancestors. Per
CSS spec, these properties create stacking contexts that abspos elements
cannot escape — they only escape scroll containers and overflow clips.
The scrollport size was previously fetched at refresh time from the
scroll ancestor paintable. Since it doesn't change between layouts,
precompute it alongside the other sticky constraints.
Previously `refresh_scroll_state()` reached back to PaintableBox for
sticky insets. Storing them on ScrollFrame makes it self-contained
for sticky offset computation.
Previously, sticky elements were excluded from propagating their
scroll frame to descendants' accumulated visual context. This meant
that when a sticky element also had scrollable overflow, the scroll
offset was never visually applied to its children during painting.
Compute the perspective matrix on the fly during visual context
assignment instead of caching it in PaintableBox. This reduces
PaintableBox's size and keeps the logic closer to where it's used.
Instead of computing and caching the transform matrix and transform
origin on PaintableBox during resolve_paint_properties(), compute them
inline during assign_accumulated_visual_contexts() where they are
actually consumed. This makes PaintableBox smaller by not wasting space
for properties that are only consumed during AccumulatedVisualContext
tree construction.
Precompute the geometric data needed for sticky positioning during
scroll frame assignment. This avoids walking the paintable tree to query
containing blocks and ancestor geometry during scroll state refresh,
which runs on every scroll event.
Move the visual viewport (pinch-to-zoom) transform from a reserved slot
in DisplayList to the AccumulatedVisualContext tree as a root transform
node. Fixed position elements now correctly inherit from this context.
This requires rebuilding the context tree and display list on each zoom
change, but this overhead will be eliminated by future partial context
tree rebuilds.
Previously, both mask and clip-path were rendered to separate mutable
Gfx::Bitmap objects which forced CPU rasterization. They were then
combined using a CPU pixel-by-pixel operation before being returned
as an ImmutableBitmap.
Instead of including mask in the final bitmap as already rasterized
images, we now use display lists which opens opportunity to utilize
GPU if available.
Bitmap::apply_mask() and ApplyMaskBitmap display list command are no
longer used and have been removed.
Effects (opacity, blend mode, filters) must be applied in the parent's
coordinate space, before the element's transform. Previously this was
handled by manually switching to the parent's visual context when
applying effects at paint time.
By adding EffectsData to AccumulatedVisualContext and positioning it
before TransformData in the chain, effects are now naturally applied in
the correct order during display list replay, eliminating the special
case in StackingContext::paint().
For SVG filters that can generate content from empty elements (feFlood,
feImage, feTurbulence), a transparent FillRect command is emitted to
trigger the filter through the same AVC pipeline.
Reuse existing paintables during relayout to reduce GC allocation
pressure. Each paintable subclass implements reset_for_relayout()
to clear state before reuse.
For position:relative/static elements, use visual parent's state
directly instead of containing block's state + intermediate walk.
This reuses existing context nodes, avoids duplicate allocations,
and eliminates the intermediate ancestor vector construction.
Previously, clip-path was applied only during painting in
StackingContext::paint(), which meant hit testing did not respect
clip-path boundaries. Clicks outside the visible clipped region but
inside the element's bounding box would incorrectly register as hits.
By moving clip-path into AccumulatedVisualContext, it becomes part of
the same system that handles transforms, clips, and scroll offsets for
both painting and hit testing, ensuring consistent behavior.
Remove the now-obsolete ClipFrame infrastructure:
- Delete ClipFrame.h and ClipFrame.cpp
- Remove assign_clip_frames() from ViewportPaintable
- Remove enclosing_clip_frame and own_clip_frame from PaintableBox
- Remove m_clip_state HashMap from ViewportPaintable
Clip handling is now fully managed through AccumulatedVisualContext
nodes with ClipData.
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.
Cache the scroll state snapshot in ViewportPaintable when
refresh_scroll_state() is called.
The upcoming AccumulatedVisualContext integration requires access to
the scroll state snapshot during hit testing to transform screen
coordinates through scroll frames. Without caching, each hit test would
allocate a new snapshot (a Vector<Entry>), causing many temporary
allocations during mouse movement. Caching the snapshot eliminates this
overhead.
Introduce AccumulatedVisualContext, a tree structure that tracks the
cumulative visual state (scroll offsets, clip regions, transforms,
perspective) for each paintable box.
Motivation:
Before this change, visual state was fragmented across multiple
mechanisms:
- ClipFrame: Tracked clip rectangles, each storing its own
enclosing_scroll_frame_id to handle scroll offset adjustments
- scroll_frame_id: Passed separately to each display list command
- PushStackingContext: Stored transform matrices directly in the command
- Every display list command implemented translate_by() (45 methods
total) to allow scroll offset adjustment during playback
This fragmentation led to:
- Complex, error-prone coordinate transformation logic scattered
throughout the codebase
- Commands being mutated during playback to apply scroll offsets
- Duplicate logic between painting and hit testing for coordinate
transformations
Solution:
AccumulatedVisualContext builds a tree where each node represents a
single visual operation:
- ScrollData: A scroll frame with its ID
- ClipData: A clip rectangle with optional border radii
- TransformData: A 4x4 transform matrix with its origin
- PerspectiveData: A perspective projection matrix
Each PaintableBox stores a reference to its accumulated context node.
The tree structure naturally captures the parent-child relationships,
so traversing from any node to the root gives the complete chain of
visual transformations.
Benefits this enables (in subsequent commits):
- Display list commands become immutable - no more translate_by()
- Single RefPtr<AccumulatedVisualContext> replaces separate
scroll_frame_id and ClipFrame on commands
- LCA-based tree traversal during playback for efficient save/restore
- transform_point_for_hit_test() provides coordinate transformation for
hit testing using the same structure
This adds visit_edges(Cell::Visitor&) methods to various helper structs
that contain GC pointers, and makes sure they are called from owning
GC-heap-allocated objects as needed.
These were found by our Clang plugin after expanding its capabilities.
The added rules will be enforced by CI going forward.
Clip frames for overflow were applied based on whether the box in
question had a non-identity matrix transformation associated with it.
That however is not correct, since specifying a no-op transform like
`scale(1)` still needs to apply clip overflow rectangles. So instead we
need to check whether the element associated with the box in question
has any CSS transforms.
This appears to have been a regression from
9bbc1cd618 and effectively reverts that
commit, but keeps its effect by unifying on the check for CSS transforms
instead.
This fixes some background boxes being rendered for the invisible items
of the carousels on https://computerbase.de/.
Fixes a bug where we would clip `box-shadow` when `overflow: hidden`
was set, which is not supposed to happen since `overflow` only affects
clipping of an element's content.