diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index d740c8b0635..da22d80a68b 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -147,6 +147,7 @@ set(SOURCES CSS/StyleValues/CSSLCHLike.cpp CSS/StyleValues/CSSLightDark.cpp CSS/StyleValues/CSSRGB.cpp + CSS/StyleValues/CursorStyleValue.cpp CSS/StyleValues/DisplayStyleValue.cpp CSS/StyleValues/EasingStyleValue.cpp CSS/StyleValues/EdgeStyleValue.cpp diff --git a/Libraries/LibWeb/CSS/CSSStyleValue.cpp b/Libraries/LibWeb/CSS/CSSStyleValue.cpp index bcb7f70cbaa..638a5a98651 100644 --- a/Libraries/LibWeb/CSS/CSSStyleValue.cpp +++ b/Libraries/LibWeb/CSS/CSSStyleValue.cpp @@ -1,6 +1,6 @@ /* * Copyright (c) 2018-2025, Andreas Kling - * Copyright (c) 2021-2024, Sam Atkins + * Copyright (c) 2021-2025, Sam Atkins * Copyright (c) 2021, Tobias Christiansen * Copyright (c) 2022-2023, MacDue * @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -146,6 +147,12 @@ CounterDefinitionsStyleValue const& CSSStyleValue::as_counter_definitions() cons return static_cast(*this); } +CursorStyleValue const& CSSStyleValue::as_cursor() const +{ + VERIFY(is_cursor()); + return static_cast(*this); +} + CustomIdentStyleValue const& CSSStyleValue::as_custom_ident() const { VERIFY(is_custom_ident()); diff --git a/Libraries/LibWeb/CSS/CSSStyleValue.h b/Libraries/LibWeb/CSS/CSSStyleValue.h index 1522ce8cd59..14f39ad61ac 100644 --- a/Libraries/LibWeb/CSS/CSSStyleValue.h +++ b/Libraries/LibWeb/CSS/CSSStyleValue.h @@ -1,7 +1,7 @@ /* * Copyright (c) 2018-2023, Andreas Kling * Copyright (c) 2021, Tobias Christiansen - * Copyright (c) 2021-2024, Sam Atkins + * Copyright (c) 2021-2025, Sam Atkins * Copyright (c) 2022-2023, MacDue * * SPDX-License-Identifier: BSD-2-Clause @@ -97,6 +97,7 @@ public: Content, Counter, CounterDefinitions, + Cursor, CustomIdent, Display, Easing, @@ -193,6 +194,10 @@ public: CounterDefinitionsStyleValue const& as_counter_definitions() const; CounterDefinitionsStyleValue& as_counter_definitions() { return const_cast(const_cast(*this).as_counter_definitions()); } + bool is_cursor() const { return type() == Type::Cursor; } + CursorStyleValue const& as_cursor() const; + CursorStyleValue& as_cursor() { return const_cast(const_cast(*this).as_cursor()); } + bool is_custom_ident() const { return type() == Type::CustomIdent; } CustomIdentStyleValue const& as_custom_ident() const; CustomIdentStyleValue& as_custom_ident() { return const_cast(const_cast(*this).as_custom_ident()); } diff --git a/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.cpp new file mode 100644 index 00000000000..d36619904cd --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2025, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "CursorStyleValue.h" +#include +#include +#include +#include +#include +#include + +namespace Web::CSS { + +String CursorStyleValue::to_string(SerializationMode mode) const +{ + StringBuilder builder; + + builder.append(m_properties.image->to_string(mode)); + + if (m_properties.x.has_value()) { + VERIFY(m_properties.y.has_value()); + builder.appendff(" {} {}", m_properties.x->to_string(), m_properties.y->to_string()); + } + + return builder.to_string_without_validation(); +} + +Optional CursorStyleValue::make_image_cursor(Layout::NodeWithStyle const& layout_node) const +{ + auto const& image = *this->image(); + if (!image.is_paintable()) { + const_cast(image).load_any_resources(const_cast(layout_node.document())); + return {}; + } + + auto const& document = layout_node.document(); + + CacheKey cache_key { + .length_resolution_context = Length::ResolutionContext::for_layout_node(layout_node), + .current_color = layout_node.computed_values().color(), + }; + + // Create a bitmap if needed. + // The cursor size for a given image never changes. It's based either on the image itself, or our default size, + // neither of which is affected by what layout node it's for. + if (!m_cached_bitmap.has_value()) { + // Determine the size of the cursor. + // "The default object size for cursor images is a UA-defined size that should be based on the size of a + // typical cursor on the UA’s operating system. + // The concrete object size is determined using the default sizing algorithm. If an operating system is + // incapable of rendering a cursor above a given size, cursors larger than that size must be shrunk to + // within the OS-supported size bounds, while maintaining the cursor image’s natural aspect ratio, if any." + // https://drafts.csswg.org/css-ui-3/#cursor + + // 32x32 is selected arbitrarily. + // FIXME: Ask the OS for the default size? + CSSPixelSize const default_cursor_size { 32, 32 }; + auto cursor_css_size = run_default_sizing_algorithm({}, {}, image.natural_width(), image.natural_height(), image.natural_aspect_ratio(), default_cursor_size); + // FIXME: How do we determine what cursor sizes the OS allows? + // We don't multiply by the pixel ratio, because we want to use the image's actual pixel size. + DevicePixelSize cursor_device_size { cursor_css_size.to_type().to_rounded() }; + + auto maybe_bitmap = Gfx::Bitmap::create_shareable(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, cursor_device_size.to_type()); + if (maybe_bitmap.is_error()) { + dbgln("Failed to create cursor bitmap: {}", maybe_bitmap.error()); + return {}; + } + auto bitmap = maybe_bitmap.release_value(); + m_cached_bitmap = bitmap->to_shareable_bitmap(); + } + + // Repaint the bitmap if necessary + if (m_cache_key != cache_key) { + m_cache_key = move(cache_key); + + // Clear whatever was in the bitmap before. + auto& bitmap = *m_cached_bitmap->bitmap(); + auto painter = Gfx::Painter::create(bitmap); + painter->clear_rect(bitmap.rect().to_type(), Color::Transparent); + + // Paint the cursor into a bitmap. + auto display_list = Painting::DisplayList::create(); + Painting::DisplayListRecorder display_list_recorder(display_list); + PaintContext paint_context { display_list_recorder, document.page().palette(), document.page().client().device_pixels_per_css_pixel() }; + + image.resolve_for_size(layout_node, CSSPixelSize { bitmap.size() }); + image.paint(paint_context, DevicePixelRect { bitmap.rect() }, ImageRendering::Auto); + + switch (document.page().client().display_list_player_type()) { + case DisplayListPlayerType::SkiaGPUIfAvailable: + case DisplayListPlayerType::SkiaCPU: { + auto painting_surface = Gfx::PaintingSurface::wrap_bitmap(bitmap); + Painting::DisplayListPlayerSkia display_list_player; + display_list_player.set_surface(painting_surface); + display_list_player.execute(*display_list); + break; + } + } + } + + // "If the values are unspecified, then the natural hotspot defined inside the image resource itself is used. + // If both the values are unspecific and the referenced cursor has no defined hotspot, the effect is as if a + // value of "0 0" were specified." + // FIXME: Make use of embedded hotspots. + Gfx::IntPoint hotspot = { 0, 0 }; + if (x().has_value() && y().has_value()) { + VERIFY(document.window()); + CalculationResolutionContext const calculation_resolution_context { + .length_resolution_context = m_cache_key->length_resolution_context + }; + auto resolved_x = x()->resolved(calculation_resolution_context); + auto resolved_y = y()->resolved(calculation_resolution_context); + if (resolved_x.has_value() && resolved_y.has_value()) { + hotspot = { resolved_x.release_value(), resolved_y.release_value() }; + } + } + + return Gfx::ImageCursor { + .bitmap = *m_cached_bitmap, + .hotspot = hotspot + }; +} + +} diff --git a/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.h new file mode 100644 index 00000000000..5754e3d34d5 --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace Web::CSS { + +class CursorStyleValue final : public StyleValueWithDefaultOperators { +public: + static ValueComparingNonnullRefPtr create(ValueComparingNonnullRefPtr image, Optional x, Optional y) + { + VERIFY(x.has_value() == y.has_value()); + return adopt_ref(*new (nothrow) CursorStyleValue(move(image), move(x), move(y))); + } + virtual ~CursorStyleValue() override = default; + + ValueComparingNonnullRefPtr image() const { return m_properties.image; } + Optional const& x() const { return m_properties.x; } + Optional const& y() const { return m_properties.y; } + + Optional make_image_cursor(Layout::NodeWithStyle const&) const; + + virtual String to_string(SerializationMode) const override; + + bool properties_equal(CursorStyleValue const& other) const { return m_properties == other.m_properties; } + +private: + CursorStyleValue(ValueComparingNonnullRefPtr image, + Optional x, + Optional y) + : StyleValueWithDefaultOperators(Type::Cursor) + , m_properties { .image = move(image), .x = move(x), .y = move(y) } + { + } + + struct Properties { + ValueComparingNonnullRefPtr image; + Optional x; + Optional y; + bool operator==(Properties const&) const = default; + } m_properties; + + // Data that can affect the bitmap rendering. + struct CacheKey { + Length::ResolutionContext length_resolution_context; + Gfx::Color current_color; + bool operator==(CacheKey const&) const = default; + }; + mutable Optional m_cache_key; + mutable Optional m_cached_bitmap; +}; + +} diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 10ca198be10..a84bc837f5e 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -168,6 +168,7 @@ class CSSStyleRule; class CSSStyleSheet; class CSSStyleValue; class CSSSupportsRule; +class CursorStyleValue; class CustomIdentStyleValue; class Display; class DisplayStyleValue;