ladybird/Libraries/LibWeb/Compositor/CompositorContextState.cpp
Aliaksandr Kalenik c821c2e234 LibWeb: Extract compositor context state
CompositorThread kept per-context compositor state, scrollbar handling,
async scroll reconciliation, and cross-thread scheduling in one large
ThreadData implementation. That made it hard to tell which code was
plain state transition logic and which code owned locking and wakeups.

Move the per-context state transitions into CompositorContextState while
leaving ThreadData in charge of the command queue, mutexes, condition
variable, main-thread invocation, and cross-context present selection.
2026-05-19 14:09:25 +02:00

368 lines
17 KiB
C++

/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/Font/Font.h>
#include <LibGfx/PaintingSurface.h>
#include <LibGfx/SkiaUtils.h>
#include <LibWeb/Compositor/CompositorContextState.h>
#include <LibWeb/Painting/DisplayList.h>
#include <AK/Assertions.h>
#include <AK/StdLibExtras.h>
#include <core/SkCanvas.h>
#include <core/SkPaint.h>
#include <core/SkRRect.h>
namespace Web::Compositor {
static SkRect to_skia_rect(Gfx::IntRect const& rect)
{
return SkRect::MakeXYWH(rect.x(), rect.y(), rect.width(), rect.height());
}
static Gfx::Orientation orientation_for_scrollbar(ViewportScrollbar const& scrollbar)
{
return scrollbar.vertical ? Gfx::Orientation::Vertical : Gfx::Orientation::Horizontal;
}
struct ViewportScrollbarIdentity {
AsyncScrollNodeID scroll_node_id;
bool vertical { false };
};
static ViewportScrollbarIdentity viewport_scrollbar_identity(ViewportScrollbar const& scrollbar)
{
return { scrollbar.scroll_node_id, scrollbar.vertical };
}
static Optional<ViewportScrollbarIdentity> viewport_scrollbar_identity_at(ReadonlySpan<ViewportScrollbar> scrollbars, Optional<size_t> scrollbar_index)
{
if (!scrollbar_index.has_value() || *scrollbar_index >= scrollbars.size())
return {};
return viewport_scrollbar_identity(scrollbars[*scrollbar_index]);
}
static Optional<size_t> find_viewport_scrollbar_index(ReadonlySpan<ViewportScrollbar> scrollbars, ViewportScrollbarIdentity identity)
{
for (size_t i = 0; i < scrollbars.size(); ++i) {
if (scrollbars[i].scroll_node_id == identity.scroll_node_id && scrollbars[i].vertical == identity.vertical)
return i;
}
return {};
}
static void set_or_append_pending_scroll_offset(Vector<AsyncScrollOffset>& pending_scroll_offsets, AsyncScrollOffset const& scroll_offset)
{
for (auto& existing : pending_scroll_offsets) {
if (existing.stable_node_id == scroll_offset.stable_node_id) {
existing.compositor_scroll_offset = scroll_offset.compositor_scroll_offset;
existing.unadopted_scroll_delta.translate_by(scroll_offset.unadopted_scroll_delta);
return;
}
}
pending_scroll_offsets.append(scroll_offset);
}
static Gfx::IntRect scrollbar_gutter_rect(ViewportScrollbar const& scrollbar, bool expanded)
{
return expanded ? scrollbar.expanded_gutter_rect : scrollbar.gutter_rect;
}
static double scrollbar_scroll_size(ViewportScrollbar const& scrollbar, bool expanded)
{
return expanded ? scrollbar.expanded_scroll_size : scrollbar.scroll_size;
}
static Gfx::IntRect translated_thumb_rect(ViewportScrollbar const& scrollbar, Painting::ScrollStateSnapshot const& scroll_state_snapshot, bool expanded)
{
auto thumb_rect = expanded ? scrollbar.expanded_thumb_rect : scrollbar.thumb_rect;
auto scroll_size = scrollbar_scroll_size(scrollbar, expanded);
auto device_offset = scroll_state_snapshot.device_offset_for_index(scrollbar.scroll_frame_index);
if (scrollbar.vertical)
thumb_rect.translate_by(0, static_cast<int>(-device_offset.y() * scroll_size));
else
thumb_rect.translate_by(static_cast<int>(-device_offset.x() * scroll_size), 0);
return thumb_rect;
}
static Gfx::IntRect translated_thumb_rect(ViewportScrollbar const& scrollbar, Gfx::FloatPoint scroll_offset, bool expanded)
{
auto orientation = orientation_for_scrollbar(scrollbar);
auto thumb_rect = expanded ? scrollbar.expanded_thumb_rect : scrollbar.thumb_rect;
thumb_rect.translate_primary_offset_for_orientation(orientation, static_cast<int>(scroll_offset.primary_offset_for_orientation(orientation) * scrollbar_scroll_size(scrollbar, expanded)));
return thumb_rect;
}
static Gfx::IntRect scrollbar_hit_rect(ViewportScrollbar const& scrollbar, Gfx::FloatPoint scroll_offset)
{
static constexpr int scrollbar_hit_slop = 4;
auto rect = translated_thumb_rect(scrollbar, scroll_offset, false).united(translated_thumb_rect(scrollbar, scroll_offset, true));
auto expanded_gutter_rect = scrollbar_gutter_rect(scrollbar, true);
if (!expanded_gutter_rect.is_empty())
rect.unite(expanded_gutter_rect);
rect.inflate(scrollbar_hit_slop, scrollbar_hit_slop);
return rect;
}
static void paint_viewport_scrollbar(Gfx::PaintingSurface& surface, ViewportScrollbar const& scrollbar, Painting::ScrollStateSnapshot const& scroll_state_snapshot, bool expanded)
{
auto thumb_rect = translated_thumb_rect(scrollbar, scroll_state_snapshot, expanded);
auto& canvas = surface.canvas();
SkPaint gutter_fill_paint;
gutter_fill_paint.setColor(to_skia_color(scrollbar.track_color));
canvas.drawRect(to_skia_rect(scrollbar_gutter_rect(scrollbar, expanded)), gutter_fill_paint);
auto skia_thumb_rect = to_skia_rect(thumb_rect);
auto radius = skia_thumb_rect.width() / 2;
auto thumb_rrect = SkRRect::MakeRectXY(skia_thumb_rect, radius, radius);
SkPaint thumb_fill_paint;
thumb_fill_paint.setColor(to_skia_color(scrollbar.thumb_color));
canvas.drawRRect(thumb_rrect, thumb_fill_paint);
SkPaint stroke_paint;
stroke_paint.setStroke(true);
stroke_paint.setStrokeWidth(1);
stroke_paint.setAntiAlias(true);
stroke_paint.setColor(to_skia_color(scrollbar.thumb_color.lightened()));
canvas.drawRRect(thumb_rrect, stroke_paint);
}
static void paint_viewport_scrollbars(Gfx::PaintingSurface& surface, ReadonlySpan<ViewportScrollbar> scrollbars, Painting::ScrollStateSnapshot const& scroll_state_snapshot, Optional<size_t> hovered_scrollbar_index, Optional<size_t> captured_scrollbar_index)
{
for (size_t i = 0; i < scrollbars.size(); ++i)
paint_viewport_scrollbar(surface, scrollbars[i], scroll_state_snapshot, hovered_scrollbar_index == i || captured_scrollbar_index == i);
}
CompositorContextState::CompositorContextState(Optional<u64> page_id, CompositorThread::PagePresentationRegistration page_presentation_registration)
: presents_to_client(page_presentation_registration == CompositorThread::PagePresentationRegistration::Yes)
, page_id(page_id)
{
VERIFY(!presents_to_client || page_id.has_value());
}
CompositorContextState::~CompositorContextState() = default;
AsyncScrollOperationID CompositorContextState::next_async_scroll_operation_id()
{
return ++next_async_scroll_operation_id_value;
}
void CompositorContextState::record_completed_async_scroll_operation(Optional<AsyncScrollOperationID> operation_id)
{
if (operation_id.has_value())
completed_async_scroll_operation_ids.append(*operation_id);
}
bool CompositorContextState::has_pending_async_scroll_updates() const
{
return !pending_async_scroll_offsets.is_empty()
|| !completed_async_scroll_operation_ids.is_empty();
}
CompositorThread::PendingAsyncScrollUpdates CompositorContextState::take_pending_async_scroll_updates()
{
CompositorThread::PendingAsyncScrollUpdates updates;
AK::swap(updates.scroll_offsets, pending_async_scroll_offsets);
AK::swap(updates.completed_operation_ids, completed_async_scroll_operation_ids);
return updates;
}
void CompositorContextState::store_pending_async_scroll_offsets(Vector<AsyncScrollOffset> const& scroll_offsets, Optional<AsyncScrollOperationID> operation_id)
{
for (auto const& scroll_offset : scroll_offsets)
set_or_append_pending_scroll_offset(pending_async_scroll_offsets, scroll_offset);
record_completed_async_scroll_operation(operation_id);
}
Optional<Gfx::FloatPoint> CompositorContextState::reapply_pending_async_scroll_offsets(Vector<AsyncScrollOffset> const& pending_scroll_offsets)
{
Optional<Gfx::FloatPoint> viewport_scroll_offset;
for (auto const& pending_scroll_offset : pending_scroll_offsets) {
auto node_id = async_scroll_tree.scroll_node_id_for_stable_id(pending_scroll_offset.stable_node_id);
if (!node_id.has_value())
continue;
auto current_scroll_offset = async_scroll_tree.scroll_offset_for_node(*node_id, cached_scroll_state_snapshot);
if (!current_scroll_offset.has_value())
continue;
// Reapplying pending async offsets is a restoration step for a freshly received
// main-thread snapshot. The pending offset is already the compositor-visible
// position, so applying the unadopted delta here would compound it.
auto reconciled_scroll_offset = async_scroll_tree.set_scroll_offset(*node_id, pending_scroll_offset.compositor_scroll_offset, cached_scroll_state_snapshot);
if (reconciled_scroll_offset.has_value() && async_scroll_tree.scroll_node_is_viewport(*node_id))
viewport_scroll_offset = *reconciled_scroll_offset;
}
return viewport_scroll_offset;
}
void CompositorContextState::set_async_scrolling_state(AsyncScrollingState&& async_scrolling_state)
{
auto hovered_scrollbar_identity = viewport_scrollbar_identity_at(viewport_scrollbars, hovered_viewport_scrollbar_index);
auto captured_scrollbar_identity = viewport_scrollbar_identity_at(viewport_scrollbars, captured_viewport_scrollbar_index);
viewport_scrollbars = move(async_scrolling_state.viewport_scrollbars);
hovered_viewport_scrollbar_index = hovered_scrollbar_identity.has_value() ? find_viewport_scrollbar_index(viewport_scrollbars, *hovered_scrollbar_identity) : Optional<size_t> {};
captured_viewport_scrollbar_index = captured_scrollbar_identity.has_value() ? find_viewport_scrollbar_index(viewport_scrollbars, *captured_scrollbar_identity) : Optional<size_t> {};
async_scroll_tree.set_state(move(async_scrolling_state));
}
WheelHitTestResult CompositorContextState::hit_test_scroll_node_for_wheel(Gfx::FloatPoint position, Gfx::FloatPoint delta) const
{
return async_scroll_tree.hit_test_scroll_node_for_wheel(position, delta);
}
Optional<size_t> CompositorContextState::hit_test_viewport_scrollbar(Gfx::FloatPoint position) const
{
for (size_t i = 0; i < viewport_scrollbars.size(); ++i) {
auto const& scrollbar = viewport_scrollbars[i];
auto scroll_offset = async_scroll_tree.scroll_offset_for_node(scrollbar.scroll_node_id, cached_scroll_state_snapshot);
if (!scroll_offset.has_value())
continue;
if (scrollbar_hit_rect(scrollbar, *scroll_offset).to_type<float>().contains(position))
return i;
}
return {};
}
Optional<CompositorContextState::ViewportScrollbarDrag> CompositorContextState::begin_viewport_scrollbar_drag(Gfx::FloatPoint position)
{
for (size_t i = 0; i < viewport_scrollbars.size(); ++i) {
auto const& scrollbar = viewport_scrollbars[i];
auto scroll_offset = async_scroll_tree.scroll_offset_for_node(scrollbar.scroll_node_id, cached_scroll_state_snapshot);
if (!scroll_offset.has_value())
continue;
auto expanded = hovered_viewport_scrollbar_index == i || captured_viewport_scrollbar_index == i;
auto orientation = orientation_for_scrollbar(scrollbar);
auto thumb_rect = translated_thumb_rect(scrollbar, *scroll_offset, expanded);
auto primary_position = position.primary_offset_for_orientation(orientation);
float thumb_grab_position = 0;
if (thumb_rect.to_type<float>().contains(position)) {
thumb_grab_position = primary_position - static_cast<float>(thumb_rect.primary_offset_for_orientation(orientation));
} else if (scrollbar_hit_rect(scrollbar, *scroll_offset).to_type<float>().contains(position)) {
auto gutter_rect = scrollbar_gutter_rect(scrollbar, true);
auto thumb_size = static_cast<float>(thumb_rect.primary_size_for_orientation(orientation));
auto gutter_start = static_cast<float>(gutter_rect.primary_offset_for_orientation(orientation));
auto gutter_size = static_cast<float>(gutter_rect.primary_size_for_orientation(orientation));
auto offset_relative_to_gutter = primary_position - gutter_start;
thumb_grab_position = max(min(offset_relative_to_gutter, thumb_size / 2), offset_relative_to_gutter - gutter_size + thumb_size);
} else {
continue;
}
captured_viewport_scrollbar_index = i;
hovered_viewport_scrollbar_index = i;
viewport_scrollbar_thumb_grab_position = thumb_grab_position;
return ViewportScrollbarDrag { i, primary_position, thumb_grab_position };
}
return {};
}
Optional<CompositorContextState::ViewportScrollbarDrag> CompositorContextState::captured_viewport_scrollbar_drag(Gfx::FloatPoint position)
{
if (!captured_viewport_scrollbar_index.has_value())
return {};
auto scrollbar_index = *captured_viewport_scrollbar_index;
if (scrollbar_index >= viewport_scrollbars.size()) {
captured_viewport_scrollbar_index.clear();
return {};
}
auto const& scrollbar = viewport_scrollbars[scrollbar_index];
auto primary_position = position.primary_offset_for_orientation(orientation_for_scrollbar(scrollbar));
return ViewportScrollbarDrag { scrollbar_index, primary_position, viewport_scrollbar_thumb_grab_position };
}
Optional<CompositorContextState::ViewportScrollbarDrag> CompositorContextState::release_captured_viewport_scrollbar_drag(Gfx::FloatPoint position)
{
if (!captured_viewport_scrollbar_index.has_value())
return {};
auto scrollbar_index = *captured_viewport_scrollbar_index;
auto thumb_grab_position = viewport_scrollbar_thumb_grab_position;
if (scrollbar_index >= viewport_scrollbars.size()) {
captured_viewport_scrollbar_index.clear();
return {};
}
auto const& scrollbar = viewport_scrollbars[scrollbar_index];
auto primary_position = position.primary_offset_for_orientation(orientation_for_scrollbar(scrollbar));
captured_viewport_scrollbar_index.clear();
return ViewportScrollbarDrag { scrollbar_index, primary_position, thumb_grab_position };
}
bool CompositorContextState::set_hovered_viewport_scrollbar(Optional<size_t> scrollbar_index)
{
if (hovered_viewport_scrollbar_index == scrollbar_index)
return false;
hovered_viewport_scrollbar_index = scrollbar_index;
return true;
}
Optional<CompositorContextState::AppliedViewportScrollbarDrag> CompositorContextState::apply_viewport_scrollbar_drag(size_t scrollbar_index, float primary_position, float thumb_grab_position)
{
if (scrollbar_index >= viewport_scrollbars.size())
return {};
auto const& scrollbar = viewport_scrollbars[scrollbar_index];
auto expanded = hovered_viewport_scrollbar_index == scrollbar_index || captured_viewport_scrollbar_index == scrollbar_index;
auto scroll_size = scrollbar_scroll_size(scrollbar, expanded);
if (scroll_size == 0)
return {};
auto current_scroll_offset = async_scroll_tree.scroll_offset_for_node(scrollbar.scroll_node_id, cached_scroll_state_snapshot);
if (!current_scroll_offset.has_value())
return {};
auto orientation = orientation_for_scrollbar(scrollbar);
auto thumb_rect = expanded ? scrollbar.expanded_thumb_rect : scrollbar.thumb_rect;
auto min_thumb_position = static_cast<float>(thumb_rect.primary_offset_for_orientation(orientation));
auto max_thumb_position = min_thumb_position + scrollbar.max_scroll_offset * static_cast<float>(scroll_size);
auto target_thumb_position = AK::clamp(primary_position - thumb_grab_position, min_thumb_position, max_thumb_position);
auto target_scroll_offset = (target_thumb_position - min_thumb_position) / static_cast<float>(scroll_size);
Gfx::FloatPoint delta;
delta.set_primary_offset_for_orientation(orientation, target_scroll_offset - current_scroll_offset->primary_offset_for_orientation(orientation));
if (delta.x() == 0 && delta.y() == 0)
return {};
auto scroll_offsets = async_scroll_tree.apply_scroll_delta(scrollbar.scroll_node_id, delta, cached_scroll_state_snapshot);
if (scroll_offsets.is_empty())
return {};
async_scroll_tree.rebuild_wheel_hit_test_targets(cached_display_list, cached_scroll_state_snapshot);
auto viewport_scroll_offset = viewport_scroll_offset_from(scroll_offsets);
if (!viewport_scroll_offset.has_value())
return {};
return AppliedViewportScrollbarDrag { move(scroll_offsets), *viewport_scroll_offset };
}
CompositorContextState::ViewportScrollbarOverlayState CompositorContextState::viewport_scrollbar_overlay_state() const
{
return {
.scrollbars = viewport_scrollbars,
.hovered_scrollbar_index = hovered_viewport_scrollbar_index,
.captured_scrollbar_index = captured_viewport_scrollbar_index,
};
}
void CompositorContextState::paint_viewport_scrollbar_overlay(Gfx::PaintingSurface& surface, ViewportScrollbarOverlayState const& overlay_state, Painting::ScrollStateSnapshot const& scroll_state_snapshot)
{
paint_viewport_scrollbars(surface, overlay_state.scrollbars, scroll_state_snapshot, overlay_state.hovered_scrollbar_index, overlay_state.captured_scrollbar_index);
}
Optional<Gfx::FloatPoint> CompositorContextState::viewport_scroll_offset_from(Vector<AsyncScrollOffset> const& scroll_offsets)
{
for (auto const& scroll_offset : scroll_offsets) {
if (scroll_offset.stable_node_id.kind == AsyncScrollNodeKind::Viewport)
return scroll_offset.compositor_scroll_offset;
}
return {};
}
}