InlinePaintable was an ad-hoc paintable type required to support the
fragmentation of inline nodes across multiple lines. It existed because
there was no way to associate multiple paintables with a single layout
node. This resulted in a lot of duplicated code between PaintableBox and
InlinePaintable. For example, most of the CSS properties like
background, border, shadows, etc. and hit-testing are almost identical
for both of them. However, the code had to be duplicated to account for
the fact that InlinePaintable creates a box for each line. And we had
quite many places that operate on paintables with a code like:
```
if (box.is_paintable_box()) {
// do something
} else (box.is_inline_paintable()) {
// do exactly the same as for paintable box but using InlinePaintable
}
```
This change replaces the usage of `InlinePaintable` with
`PaintableWithLines` created for each line, which is now possible
because we support having multiple paintables per layout node. By doing
that, we remove lots of duplicated code and bring our implementation
closer to the spec.
CSS fragmentation implies 1:N relationship between layout nodes and
paintables. This change is a preparation for implementation of inline
fragmentation where InlinePaintable will be replaced with
PaintableWithLines corresponding to each line.
That's awkward, but getComputedStyle needs to return used track values
for gridTemplateColumns and gridTemplateRows properties. This change
implements it by saving style values with used values into layout state,
so it could be assigned to paintables during LayoutState::commit() and
later accessed by style_value_for_property().
I haven't seen it used in the wild, but WPT grid tests extensively use
it. For example this change helps to go from 0/10 to 8/10 on this test:
https://wpt.live/css/css-grid/layout-algorithm/grid-fit-content-percentage.html
Calls to `Document::set_needs_display()` and
`Paintable::set_needs_display()` now invalidate the display list by
default. This behavior can be changed by passing
`InvalidateDisplayList::No` to the function where invalidating the
display list is not necessary.
Sticky positioning is implemented by modifying the algorithm for
assigning and refreshing scroll frames. Now, elements with
"position: sticky" are assigned their own scroll frame, and their
position is refreshed independently from regular scroll boxes.
Refreshing the scroll offsets for sticky boxes does not require display
list invalidation.
A separate hash map is used for the scroll frames of sticky boxes. This
is necessary because a single paintable box can have two scroll frames
if it 1) has "position: sticky" and 2) contains scrollable overflow.
This allows the calculation of the cumulative scroll offset for a scroll
frame by adding its scroll offset to the parent’s scroll offset, rather
than traversing the containing block chain. While it doesn't greatly
simplify calculations for typical scroll frames, it serves as a
preparation for supporting "position: sticky".
Instead, it could be applied directly as a clip path in Skia painter.
As a side bonus, we get rid of some DeprecatedPath and
AntiAliasingPainter usage.
A new display list item type named PaintScrollBar is introduced. Having
a dedicated type for scroll bars allows the thumb position to be updated
without rebuilding a display list. This was not possible with
FillRectWithRoundedCorners that does not allow to tell whether it
belongs to scroll thumb.
Before this change AddClipRect was a "special" because it didn't respect
scroll frame offset and was meant to be recorded using viewport-relative
coordinates. The motivation behind this was to record a "final" clip
rectangle computed by intersecting all clip rectangles used by a clip
frame. The disadvantage of this approach is that it blocks us from
implementing an optimisation to reuse display list if the only change is
in the scroll offset, because any scroll offset change leads to
invalidating all AddClipRect items within a list.
This change aligns AddClipRect with the rest of display list items by
making it account for scroll frame offset. It required discontinuing
the recording of the intersection of all clip rectangles within a clip
frame and instead producing an AddClipRect for each of them.
A nice side effect is the removal of code that shifts clip rectangle by
`enclosing_scroll_offset()` in a bunch of places, because now it happens
automatically in `DisplayList::apply_scroll_offsets()`.
This change fixes layering violation by moving to_px() calls to happen
before display list recording. Also it should make display list
recording a bit faster by resolving background properties beforehand.
Overflow clipping is currently implemented as:
1. Create clip frame for each box with hidden overflow
2. Calculate clip rect for each clip frame by intersecting padding boxes
of all boxes with hidden overflow in containing block chain
3. Assign enclosing clip frame (closest clip frame in containing block
chain) to each PaintableBox
4. Apply clip rect of enclosing clip frame in Paintable::before_paint()
It breaks when any CSS transform other than simple translation is lying
between box with hidden overflow and a clipped box, because clip
rectangle will be applied when transform has already changed.
The fix is implemented by relying on the following rule:
"For elements whose layout is governed by the CSS box model, any value
other than none for the transform also causes the element to establish
a containing block for all descendants."
It means everything nested into a stacking context with CSS transform
can't escape its clip, so it's safe to apply its clip for all children.
Contrary to LibGfx, where corner clipping was implemented by sampling
and blitting pixels under corners into a temporary bitmap, Skia allows
us to simply apply a mask. As a result, we no longer need the
BlitCornerClipping command, which has become a no-op.
- SampleUnderCorners is renamed to AddRoundedRectClip
- The optimization that skipped unnecessary blit and sample commands has
been removed. However, this should not result in a performance
regression because Skia seems to perform mask rasterization lazily.
Having resolution of all properties for all paintable types in a single
function was hard to iterate on, so this change separates it into
smaller functions per paintable type.
The main intention of this change is to have a consistent look and
behavior across all scrollbars, including elements with
`overflow: scroll` and `overflow: auto`, iframes, and a page.
Before:
- Page's scrollbar is painted by Browser (Qt/AppKit) using the
corresponding UI framework style,
- Both WebContent and Browser know the scroll position offset.
- WebContent uses did_request_scroll_to() IPC call to send updates.
- Browser uses set_viewport_rect() to send updates.
After:
- Page's scrollbar is painted on WebContent side using the same style as
currently used for elements with `overflow: scroll` and
`overflow: auto`. A nice side effects: scrollbars are now painted for
iframes, and page's scrollbar respects scrollbar-width CSS property.
- Only WebContent knows scroll position offset.
- did_request_scroll_to() is no longer used.
- set_viewport_rect() is changed to set_viewport_size().
Currently, these only work when there are no CSS transforms (as the
stacking context painting is not set up to handle that case yet). This
is still enough to get most chat/comment markers working on GitHub
though :^)
Fixes bug when CSS transform is applied twice to clip rect:
- While calculating absolute clip rectangles in `refresh_clip_state()`
- While executing `PushStackingContext` painting command.
Duplicated transform is already removed for PaintableBox and this change
adds this for InlinePaintable.
Instead of TextPaintable fragments being an offset+length view into the
layout node, they are now a view into the paintable instead.
This removes an awkward time window where we'd have bogus state in text
fragments after layout invalidation but before relayout. It also makes
the code slightly nicer in general, since there's less mixing of layout
and painting concepts.
From https://drafts.csswg.org/css-backgrounds-4/#background-clip
"The background is painted within (clipped to) the intersection of the
border box and the geometry of the text in the element and its in-flow
and floated descendants"
This change implements it in the following way:
1. Traverse the descendants of the element, collecting the Gfx::Path of
glyphs into a vector.
2. The vector of collected paths is saved in the background painting
command.
3. The painting commands executor uses the list of glyphs to paint a
mask for background clipping.
Co-authored-by: Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
Introduces the rendering of scroll thumbs in vertical and horizontal
directions. Currently, the thumbs are purely graphical elements that
do not respond to mouse events. Nevertheless, this is beneficial as it
makes it easier to identify elements that should respond to scrolling
events.
Painting of scrollbars uncovers numerous bugs in the calculation of
scrollable overflow rectangles highlighting all the places where
elements are made scrollable whey they shouldn't be. Positively, this
issue might motivate us to pay more attention to this problem to
eliminate unnecessary scrollbars.
Currently, the scrollbar style is uniform across all platforms: a
semi-transparent gray rectangle with rounded corners.
Also here we add `scrollbar-width: none` to all existing scrolling
ref-tests, so they keep working with this change.
This change modifies hit_test() to no longer return the first paintable
encountered at a specified position. Instead, this function accepts a
callback that is invoked for each paintable located at a position, in
hit-testing order.
This modification will allow us to reuse this call for
`Document.elementsFromPoint()` in upcoming changes.
Refactor to resolve paint-only properties before painting, aiming to
stop using layout nodes during recording of painting commands.
Also adds a test, as we have not had any for outlines yet.
With this change, instead of applying only the border-radius clipping
from the closest containing block with hidden overflow, we now collect
all boxes within the containing block chain and apply the clipping from
all of them.
In this commit we have optimized the handling of scroll offsets and
clip rectangles to improve performance. Previously, the process
involved multiple full traversals of the paintable tree before each
repaint, which was highly inefficient, especially on pages with a
large number of paintables. The steps were:
1. Traverse the paintable tree to identify all boxes with scrollable or
clipped overflow.
2. Gather the accumulated scroll offset or clip rectangle for each box.
3. Perform another traversal to apply the corresponding scroll offset
and clip rectangle to each paintable.
To address this, we've adopted a new strategy that separates the
assignment of the scroll/clip frame from the refresh of accumulated
scroll offsets and clip rectangles, thus reducing the workload:
1. Post-relayout: Identify all boxes with overflow and link each
paintable to the state of its containing scroll/clip frame.
2. Pre-repaint: Update the clip rectangle and scroll offset only in the
previously identified boxes.
This adjustment ensures that the costly tree traversals are only
necessary after a relayout, substantially decreasing the amount of work
required before each repaint.
In cases where the stacking context painting requires a separate
bitmap, the destination position needs to be translated by the
scrolling offset to ensure it ends up in the correct position.
This change makes hit-testing more consistent in the handling of hidden
overflow by reusing the same clip-rectangles.
Also, it fixes bugs where the box is visible for hit-testing even
though it is clipped by the hidden overflow of the containing block.
Paintable boxes should not hold information stored in device pixels.
It should be converted from CSS pixels only by the time painting
command recording occurs.
The hit-testing position is now shifted by the scroll offsets before
performing any checks for containment. This is implemented by assigning
each PaintableBox/InlinePaintable an offset corresponding to the scroll
frame in which it is contained. The non-scroll-adjusted position is
still passed down when recursing to children because the assigned
offset accumulated for nested scroll frames.
With this change, hit testing works in the Inspector.
Fixes https://github.com/SerenityOS/serenity/issues/22068
With this change, clip rectangles for boxes with hidden overflow or the
clip property are no longer calculated during the recording of painting
commands. Instead, it has moved to the "pre-paint" phase, along with
the assignment of scrolling offsets, and works in the following way:
1. The paintable tree is traversed to collect all paintable boxes that
have hidden overflow or use the CSS clip property. For each of these
boxes, the "final" clip rectangle is calculated by intersecting clip
rectangles in the containing block chain for a box.
2. The paintable tree is traversed another time, and a clip rectangle
is assigned for each paintable box contained by a node with hidden
overflow or the clip property.
This way, clipping becomes much easier during the painting commands
recording phase, as it only concerns the use of already assigned clip
rectangles. The same approach is applied to handle scrolling offsets.
Also, clip rectangle calculation is now implemented more correctly, as
we no longer stop at the stacking context boundary while intersecting
clip rectangles in the containing block chain.
Fixes:
https://github.com/SerenityOS/serenity/issues/22932https://github.com/SerenityOS/serenity/issues/22883https://github.com/SerenityOS/serenity/issues/22679https://github.com/SerenityOS/serenity/issues/22534
Now, instead of resolving "transform" and "transform-origin" during the
construction of the stacking context tree, we do so during the layout
commit.
This is part of a refactoring effort to make the paintable tree
independent from the layout tree.
The paintable tree structure more closely matches the painting order
when fragments are owned by corresponding inline paintables. This
change does not affect the layout tree, as it is more convenient for
layout purposes to have all fragments owned by a block container in
one place.
Additionally, this improves performance significantly on pages with
many fragments, as we no longer have to walk the ancestor chain up
to the closest block container to determine if a fragment belongs
to an inline paintable.
This is a part of refactoring towards making the paintable tree
independent of the layout tree. Now, instead of transferring text
fragments from the layout tree to the paintable tree during the layout
commit phase, we allocate separate PaintableFragments that contain only
the information necessary for painting. Doing this also allows us to
get rid LineBoxes, as they are used only during layout.
With this change, a stacking context can be established by any
paintable, including inline paintables. The stacking context traversal
is updated to remove the assumption that the stacking context root is
paintable box.
Fragments contained by the inline node should be painted in the
foreground phase for this node, instead of being painted as a part of
the containing PaintableWithLines. This change implements that by
marking all fragments contained by inline nodes so they can be skipped
while painting the content of PaintableWithLines. This is an ugly way,
and instead, we should make InlinePaintables own all fragments
contained by them.
With this change, instead of applying scroll offsets during the
recording of the painting command list, we do the following:
1. Collect all boxes with scrollable overflow into a PaintContext,
each with an id and the total amount of scrolling offset accumulated
from ancestor scrollable boxes.
2. During the recording phase assign a corresponding scroll_frame_id to
each command that paints content within a scrollable box.
3. Before executing the recorded commands, translate each command that
has a scroll_frame_id by the accumulated scroll offset.
This approach has following advantages:
- Implementing nested scrollables becomes much simpler, as the
recording phase only requires the correct assignment of the nearest
scrollable's scroll_frame_id, while the accumulated offset from
ancestors is applied subsequently.
- The recording of painting commands is not tied to a specific offset
within scrollable boxes, which means in the future, it will be
possible to update the scrolling offset and repaint without the need
to re-record painting commands.