LibWeb+WebContent+UI: Support image cursors

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.
This commit is contained in:
Sam Atkins 2025-02-20 12:17:29 +00:00 committed by Andreas Kling
commit bfd7ac1204
Notes: github-actions[bot] 2025-02-28 12:51:27 +00:00
20 changed files with 297 additions and 170 deletions

View file

@ -11,7 +11,6 @@
#include <LibGC/CellAllocator.h> #include <LibGC/CellAllocator.h>
#include <LibWeb/CSS/Clip.h> #include <LibWeb/CSS/Clip.h>
#include <LibWeb/CSS/ComputedProperties.h> #include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/StyleValues/AngleStyleValue.h>
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h> #include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
#include <LibWeb/CSS/StyleValues/ColorSchemeStyleValue.h> #include <LibWeb/CSS/StyleValues/ColorSchemeStyleValue.h>
#include <LibWeb/CSS/StyleValues/ContentStyleValue.h> #include <LibWeb/CSS/StyleValues/ContentStyleValue.h>
@ -32,7 +31,6 @@
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h> #include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/CSS/StyleValues/PositionStyleValue.h> #include <LibWeb/CSS/StyleValues/PositionStyleValue.h>
#include <LibWeb/CSS/StyleValues/RectStyleValue.h> #include <LibWeb/CSS/StyleValues/RectStyleValue.h>
#include <LibWeb/CSS/StyleValues/ScrollbarGutterStyleValue.h>
#include <LibWeb/CSS/StyleValues/ShadowStyleValue.h> #include <LibWeb/CSS/StyleValues/ShadowStyleValue.h>
#include <LibWeb/CSS/StyleValues/StringStyleValue.h> #include <LibWeb/CSS/StyleValues/StringStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h> #include <LibWeb/CSS/StyleValues/StyleValueList.h>
@ -954,13 +952,30 @@ ContentVisibility ComputedProperties::content_visibility() const
return keyword_to_content_visibility(value.to_keyword()).release_value(); return keyword_to_content_visibility(value.to_keyword()).release_value();
} }
Cursor ComputedProperties::cursor() const Vector<CursorData> ComputedProperties::cursor() const
{ {
// Return the first available cursor.
auto const& value = property(PropertyID::Cursor); auto const& value = property(PropertyID::Cursor);
// FIXME: We don't currently support custom cursors. Vector<CursorData> cursors;
if (value.is_url()) if (value.is_value_list()) {
return Cursor::Auto; for (auto const& item : value.as_value_list().values()) {
return keyword_to_cursor(value.to_keyword()).release_value(); if (item->is_cursor()) {
cursors.append({ item->as_cursor() });
continue;
}
if (auto keyword = keyword_to_cursor(item->to_keyword()); keyword.has_value())
cursors.append(keyword.release_value());
}
} else if (value.is_keyword()) {
if (auto keyword = keyword_to_cursor(value.to_keyword()); keyword.has_value())
cursors.append(keyword.release_value());
}
if (cursors.is_empty())
cursors.append(Cursor::Auto);
return cursors;
} }
Visibility ComputedProperties::visibility() const Visibility ComputedProperties::visibility() const

View file

@ -94,7 +94,7 @@ public:
}; };
ContentDataAndQuoteNestingLevel content(DOM::Element&, u32 initial_quote_nesting_level) const; ContentDataAndQuoteNestingLevel content(DOM::Element&, u32 initial_quote_nesting_level) const;
ContentVisibility content_visibility() const; ContentVisibility content_visibility() const;
Cursor cursor() const; Vector<CursorData> cursor() const;
Variant<LengthOrCalculated, NumberOrCalculated> tab_size() const; Variant<LengthOrCalculated, NumberOrCalculated> tab_size() const;
WhiteSpace white_space() const; WhiteSpace white_space() const;
WordBreak word_break() const; WordBreak word_break() const;

View file

@ -1,5 +1,6 @@
/* /*
* Copyright (c) 2020-2023, Andreas Kling <andreas@ladybird.org> * Copyright (c) 2020-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2023-2025, Sam Atkins <sam@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -28,7 +29,7 @@
#include <LibWeb/CSS/Size.h> #include <LibWeb/CSS/Size.h>
#include <LibWeb/CSS/StyleValues/AbstractImageStyleValue.h> #include <LibWeb/CSS/StyleValues/AbstractImageStyleValue.h>
#include <LibWeb/CSS/StyleValues/BasicShapeStyleValue.h> #include <LibWeb/CSS/StyleValues/BasicShapeStyleValue.h>
#include <LibWeb/CSS/StyleValues/PositionStyleValue.h> #include <LibWeb/CSS/StyleValues/CursorStyleValue.h>
#include <LibWeb/CSS/StyleValues/ShadowStyleValue.h> #include <LibWeb/CSS/StyleValues/ShadowStyleValue.h>
#include <LibWeb/CSS/Transformation.h> #include <LibWeb/CSS/Transformation.h>
@ -79,6 +80,8 @@ struct Containment {
bool is_empty() const { return !(size_containment || inline_size_containment || layout_containment || style_containment || paint_containment); } bool is_empty() const { return !(size_containment || inline_size_containment || layout_containment || style_containment || paint_containment); }
}; };
using CursorData = Variant<NonnullRefPtr<CursorStyleValue>, Cursor>;
using ListStyleType = Variant<CounterStyleNameKeyword, String>; using ListStyleType = Variant<CounterStyleNameKeyword, String>;
class InitialValues { class InitialValues {
@ -94,7 +97,7 @@ public:
static CSS::Clip clip() { return CSS::Clip::make_auto(); } static CSS::Clip clip() { return CSS::Clip::make_auto(); }
static CSS::PreferredColorScheme color_scheme() { return CSS::PreferredColorScheme::Auto; } static CSS::PreferredColorScheme color_scheme() { return CSS::PreferredColorScheme::Auto; }
static CSS::ContentVisibility content_visibility() { return CSS::ContentVisibility::Visible; } static CSS::ContentVisibility content_visibility() { return CSS::ContentVisibility::Visible; }
static CSS::Cursor cursor() { return CSS::Cursor::Auto; } static CursorData cursor() { return { CSS::Cursor::Auto }; }
static CSS::WhiteSpace white_space() { return CSS::WhiteSpace::Normal; } static CSS::WhiteSpace white_space() { return CSS::WhiteSpace::Normal; }
static CSS::WordBreak word_break() { return CSS::WordBreak::Normal; } static CSS::WordBreak word_break() { return CSS::WordBreak::Normal; }
static CSS::LengthOrCalculated word_spacing() { return CSS::Length::make_px(0); } static CSS::LengthOrCalculated word_spacing() { return CSS::Length::make_px(0); }
@ -375,7 +378,7 @@ public:
CSS::Clip clip() const { return m_noninherited.clip; } CSS::Clip clip() const { return m_noninherited.clip; }
CSS::PreferredColorScheme color_scheme() const { return m_inherited.color_scheme; } CSS::PreferredColorScheme color_scheme() const { return m_inherited.color_scheme; }
CSS::ContentVisibility content_visibility() const { return m_inherited.content_visibility; } CSS::ContentVisibility content_visibility() const { return m_inherited.content_visibility; }
CSS::Cursor cursor() const { return m_inherited.cursor; } Vector<CursorData> const& cursor() const { return m_inherited.cursor; }
CSS::ContentData content() const { return m_noninherited.content; } CSS::ContentData content() const { return m_noninherited.content; }
CSS::PointerEvents pointer_events() const { return m_inherited.pointer_events; } CSS::PointerEvents pointer_events() const { return m_inherited.pointer_events; }
CSS::Display display() const { return m_noninherited.display; } CSS::Display display() const { return m_noninherited.display; }
@ -575,7 +578,7 @@ protected:
Optional<Color> accent_color {}; Optional<Color> accent_color {};
Color webkit_text_fill_color { InitialValues::color() }; Color webkit_text_fill_color { InitialValues::color() };
CSS::ContentVisibility content_visibility { InitialValues::content_visibility() }; CSS::ContentVisibility content_visibility { InitialValues::content_visibility() };
CSS::Cursor cursor { InitialValues::cursor() }; Vector<CursorData> cursor { InitialValues::cursor() };
CSS::ImageRendering image_rendering { InitialValues::image_rendering() }; CSS::ImageRendering image_rendering { InitialValues::image_rendering() };
CSS::PointerEvents pointer_events { InitialValues::pointer_events() }; CSS::PointerEvents pointer_events { InitialValues::pointer_events() };
Variant<LengthOrCalculated, NumberOrCalculated> tab_size { InitialValues::tab_size() }; Variant<LengthOrCalculated, NumberOrCalculated> tab_size { InitialValues::tab_size() };
@ -763,7 +766,7 @@ public:
void set_clip(CSS::Clip const& clip) { m_noninherited.clip = clip; } void set_clip(CSS::Clip const& clip) { m_noninherited.clip = clip; }
void set_content(ContentData const& content) { m_noninherited.content = content; } void set_content(ContentData const& content) { m_noninherited.content = content; }
void set_content_visibility(CSS::ContentVisibility content_visibility) { m_inherited.content_visibility = content_visibility; } void set_content_visibility(CSS::ContentVisibility content_visibility) { m_inherited.content_visibility = content_visibility; }
void set_cursor(CSS::Cursor cursor) { m_inherited.cursor = cursor; } void set_cursor(Vector<CursorData> cursor) { m_inherited.cursor = move(cursor); }
void set_image_rendering(CSS::ImageRendering value) { m_inherited.image_rendering = value; } void set_image_rendering(CSS::ImageRendering value) { m_inherited.image_rendering = value; }
void set_pointer_events(CSS::PointerEvents value) { m_inherited.pointer_events = value; } void set_pointer_events(CSS::PointerEvents value) { m_inherited.pointer_events = value; }
void set_background_color(Color color) { m_noninherited.background_color = color; } void set_background_color(Color color) { m_noninherited.background_color = color; }

View file

@ -368,6 +368,7 @@ private:
RefPtr<CSSStyleValue> parse_counter_increment_value(TokenStream<ComponentValue>&); RefPtr<CSSStyleValue> parse_counter_increment_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_counter_reset_value(TokenStream<ComponentValue>&); RefPtr<CSSStyleValue> parse_counter_reset_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_counter_set_value(TokenStream<ComponentValue>&); RefPtr<CSSStyleValue> parse_counter_set_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_cursor_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_display_value(TokenStream<ComponentValue>&); RefPtr<CSSStyleValue> parse_display_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_flex_shorthand_value(TokenStream<ComponentValue>&); RefPtr<CSSStyleValue> parse_flex_shorthand_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_flex_flow_value(TokenStream<ComponentValue>&); RefPtr<CSSStyleValue> parse_flex_flow_value(TokenStream<ComponentValue>&);

View file

@ -26,6 +26,7 @@
#include <LibWeb/CSS/StyleValues/ColorSchemeStyleValue.h> #include <LibWeb/CSS/StyleValues/ColorSchemeStyleValue.h>
#include <LibWeb/CSS/StyleValues/ContentStyleValue.h> #include <LibWeb/CSS/StyleValues/ContentStyleValue.h>
#include <LibWeb/CSS/StyleValues/CounterDefinitionsStyleValue.h> #include <LibWeb/CSS/StyleValues/CounterDefinitionsStyleValue.h>
#include <LibWeb/CSS/StyleValues/CursorStyleValue.h>
#include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h> #include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h>
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h> #include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
#include <LibWeb/CSS/StyleValues/EasingStyleValue.h> #include <LibWeb/CSS/StyleValues/EasingStyleValue.h>
@ -537,6 +538,10 @@ Parser::ParseErrorOr<NonnullRefPtr<CSSStyleValue>> Parser::parse_css_value(Prope
if (auto parsed_value = parse_counter_set_value(tokens); parsed_value && !tokens.has_next_token()) if (auto parsed_value = parse_counter_set_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull(); return parsed_value.release_nonnull();
return ParseError::SyntaxError; return ParseError::SyntaxError;
case PropertyID::Cursor:
if (auto parsed_value = parse_cursor_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull();
return ParseError::SyntaxError;
case PropertyID::Display: case PropertyID::Display:
if (auto parsed_value = parse_display_value(tokens); parsed_value && !tokens.has_next_token()) if (auto parsed_value = parse_display_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull(); return parsed_value.release_nonnull();
@ -958,6 +963,72 @@ RefPtr<CSSStyleValue> Parser::parse_counter_set_value(TokenStream<ComponentValue
return parse_counter_definitions_value(tokens, AllowReversed::No, 0); return parse_counter_definitions_value(tokens, AllowReversed::No, 0);
} }
// https://drafts.csswg.org/css-ui-3/#cursor
RefPtr<CSSStyleValue> Parser::parse_cursor_value(TokenStream<ComponentValue>& tokens)
{
// [ [<url> [<x> <y>]?,]* <built-in-cursor> ]
// So, any number of custom cursor definitions, and then a mandatory cursor name keyword, all comma-separated.
auto transaction = tokens.begin_transaction();
StyleValueVector cursors;
auto parts = parse_a_comma_separated_list_of_component_values(tokens);
for (auto i = 0u; i < parts.size(); ++i) {
auto& part = parts[i];
TokenStream part_tokens { part };
if (i == parts.size() - 1) {
// Cursor keyword
part_tokens.discard_whitespace();
auto keyword_value = parse_keyword_value(part_tokens);
if (!keyword_value || !keyword_to_cursor(keyword_value->to_keyword()).has_value())
return {};
part_tokens.discard_whitespace();
if (part_tokens.has_next_token())
return {};
cursors.append(keyword_value.release_nonnull());
} else {
// Custom cursor definition
// <url> [<x> <y>]?
// "Conforming user agents may, instead of <url>, support <image> which is a superset."
part_tokens.discard_whitespace();
auto image_value = parse_image_value(part_tokens);
if (!image_value)
return {};
part_tokens.discard_whitespace();
if (part_tokens.has_next_token()) {
// x and y, which are both <number>
auto x = parse_number(part_tokens);
part_tokens.discard_whitespace();
auto y = parse_number(part_tokens);
part_tokens.discard_whitespace();
if (!x.has_value() || !y.has_value() || part_tokens.has_next_token())
return nullptr;
cursors.append(CursorStyleValue::create(image_value.release_nonnull(), x.release_value(), y.release_value()));
continue;
}
cursors.append(CursorStyleValue::create(image_value.release_nonnull(), {}, {}));
}
}
if (cursors.is_empty())
return nullptr;
transaction.commit();
if (cursors.size() == 1)
return *cursors.first();
return StyleValueList::create(move(cursors), StyleValueList::Separator::Comma);
}
// https://www.w3.org/TR/css-sizing-4/#aspect-ratio // https://www.w3.org/TR/css-sizing-4/#aspect-ratio
RefPtr<CSSStyleValue> Parser::parse_aspect_ratio_value(TokenStream<ComponentValue>& tokens) RefPtr<CSSStyleValue> Parser::parse_aspect_ratio_value(TokenStream<ComponentValue>& tokens)
{ {

View file

@ -83,9 +83,12 @@ static bool parent_element_for_event_dispatch(Painting::Paintable& paintable, GC
return node && layout_node; return node && layout_node;
} }
static Gfx::StandardCursor cursor_css_to_gfx(CSS::Cursor cursor) static Gfx::Cursor resolve_cursor(Layout::NodeWithStyle const& layout_node, Vector<CSS::CursorData> const& cursor_data, Gfx::StandardCursor auto_cursor)
{ {
switch (cursor) { for (auto const& cursor : cursor_data) {
auto result = cursor.visit(
[auto_cursor](CSS::Cursor css_cursor) -> Optional<Gfx::Cursor> {
switch (css_cursor) {
case CSS::Cursor::Crosshair: case CSS::Cursor::Crosshair:
case CSS::Cursor::Cell: case CSS::Cursor::Cell:
return Gfx::StandardCursor::Crosshair; return Gfx::StandardCursor::Crosshair;
@ -132,16 +135,29 @@ static Gfx::StandardCursor cursor_css_to_gfx(CSS::Cursor cursor)
case CSS::Cursor::ZoomIn: case CSS::Cursor::ZoomIn:
case CSS::Cursor::ZoomOut: case CSS::Cursor::ZoomOut:
return Gfx::StandardCursor::Zoom; return Gfx::StandardCursor::Zoom;
case CSS::Cursor::Auto:
return auto_cursor;
case CSS::Cursor::ContextMenu: case CSS::Cursor::ContextMenu:
case CSS::Cursor::Alias: case CSS::Cursor::Alias:
case CSS::Cursor::Copy: case CSS::Cursor::Copy:
case CSS::Cursor::NoDrop: case CSS::Cursor::NoDrop:
// FIXME: No corresponding GFX Standard Cursor, fallthrough to None // FIXME: No corresponding GFX Standard Cursor, fallthrough to None
case CSS::Cursor::Auto:
case CSS::Cursor::Default: case CSS::Cursor::Default:
default: default:
return Gfx::StandardCursor::None; return Gfx::StandardCursor::None;
} }
},
[&layout_node](NonnullRefPtr<CSS::CursorStyleValue> const& cursor_style_value) -> Optional<Gfx::Cursor> {
if (auto image_cursor = cursor_style_value->make_image_cursor(layout_node); image_cursor.has_value())
return image_cursor.release_value();
return {};
});
if (result.has_value())
return result.release_value();
}
// We should never get here
return Gfx::StandardCursor::None;
} }
// https://drafts.csswg.org/cssom-view/#dom-mouseevent-offsetx // https://drafts.csswg.org/cssom-view/#dom-mouseevent-offsetx
@ -692,7 +708,7 @@ EventResult EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSP
bool hovered_node_changed = false; bool hovered_node_changed = false;
bool is_hovering_link = false; bool is_hovering_link = false;
Gfx::StandardCursor hovered_node_cursor = Gfx::StandardCursor::None; Gfx::Cursor hovered_node_cursor = Gfx::StandardCursor::None;
GC::Ptr<Painting::Paintable> paintable; GC::Ptr<Painting::Paintable> paintable;
Optional<int> start_index; Optional<int> start_index;
@ -721,7 +737,7 @@ EventResult EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSP
return EventResult::Dropped; return EventResult::Dropped;
} }
auto const cursor = paintable->computed_values().cursor(); auto cursor_data = paintable->computed_values().cursor();
auto pointer_events = paintable->computed_values().pointer_events(); auto pointer_events = paintable->computed_values().pointer_events();
// FIXME: Handle other values for pointer-events. // FIXME: Handle other values for pointer-events.
VERIFY(pointer_events != CSS::PointerEvents::None); VERIFY(pointer_events != CSS::PointerEvents::None);
@ -739,15 +755,9 @@ EventResult EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSP
is_hovering_link = true; is_hovering_link = true;
if (paintable->layout_node().is_text_node()) { if (paintable->layout_node().is_text_node()) {
if (cursor == CSS::Cursor::Auto) hovered_node_cursor = resolve_cursor(*paintable->layout_node().parent(), cursor_data, Gfx::StandardCursor::IBeam);
hovered_node_cursor = Gfx::StandardCursor::IBeam;
else
hovered_node_cursor = cursor_css_to_gfx(cursor);
} else if (node->is_element()) { } else if (node->is_element()) {
if (cursor == CSS::Cursor::Auto) hovered_node_cursor = resolve_cursor(static_cast<Layout::NodeWithStyle&>(*layout_node), cursor_data, Gfx::StandardCursor::Arrow);
hovered_node_cursor = Gfx::StandardCursor::Arrow;
else
hovered_node_cursor = cursor_css_to_gfx(cursor);
} }
auto page_offset = compute_mouse_event_page_offset(viewport_position); auto page_offset = compute_mouse_event_page_offset(viewport_position);
@ -793,7 +803,10 @@ EventResult EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSP
auto& page = m_navigable->page(); auto& page = m_navigable->page();
if (page.current_cursor() != hovered_node_cursor) { // FIXME: This check is only approximate. ImageCursors from the same CursorStyleValue share bitmaps, but may repaint them.
// So comparing them does not tell you if they are the same image. Also, the image may change even if the hovered
// node does not.
if (page.current_cursor() != hovered_node_cursor || hovered_node_changed) {
page.set_current_cursor(hovered_node_cursor); page.set_current_cursor(hovered_node_cursor);
page.client().page_did_request_cursor_change(hovered_node_cursor); page.client().page_did_request_cursor_change(hovered_node_cursor);
} }

View file

@ -127,8 +127,8 @@ public:
bool is_in_tooltip_area() const { return m_is_in_tooltip_area; } bool is_in_tooltip_area() const { return m_is_in_tooltip_area; }
void set_is_in_tooltip_area(bool b) { m_is_in_tooltip_area = b; } void set_is_in_tooltip_area(bool b) { m_is_in_tooltip_area = b; }
Gfx::StandardCursor current_cursor() const { return m_current_cursor; } Gfx::Cursor current_cursor() const { return m_current_cursor; }
void set_current_cursor(Gfx::StandardCursor cursor) { m_current_cursor = cursor; } void set_current_cursor(Gfx::Cursor cursor) { m_current_cursor = move(cursor); }
DevicePixelPoint window_position() const { return m_window_position; } DevicePixelPoint window_position() const { return m_window_position; }
void set_window_position(DevicePixelPoint position) { m_window_position = position; } void set_window_position(DevicePixelPoint position) { m_window_position = position; }
@ -258,7 +258,7 @@ private:
bool m_is_hovering_link { false }; bool m_is_hovering_link { false };
bool m_is_in_tooltip_area { false }; bool m_is_in_tooltip_area { false };
Gfx::StandardCursor m_current_cursor { Gfx::StandardCursor::Arrow }; Gfx::Cursor m_current_cursor { Gfx::StandardCursor::Arrow };
DevicePixelPoint m_window_position {}; DevicePixelPoint m_window_position {};
DevicePixelSize m_window_size {}; DevicePixelSize m_window_size {};
@ -338,7 +338,7 @@ public:
virtual void page_did_create_new_document(Web::DOM::Document&) { } virtual void page_did_create_new_document(Web::DOM::Document&) { }
virtual void page_did_change_active_document_in_top_level_browsing_context(Web::DOM::Document&) { } virtual void page_did_change_active_document_in_top_level_browsing_context(Web::DOM::Document&) { }
virtual void page_did_finish_loading(URL::URL const&) { } virtual void page_did_finish_loading(URL::URL const&) { }
virtual void page_did_request_cursor_change(Gfx::StandardCursor) { } virtual void page_did_request_cursor_change(Gfx::Cursor const&) { }
virtual void page_did_request_context_menu(CSSPixelPoint) { } virtual void page_did_request_context_menu(CSSPixelPoint) { }
virtual void page_did_request_link_context_menu(CSSPixelPoint, URL::URL const&, [[maybe_unused]] ByteString const& target, [[maybe_unused]] unsigned modifiers) { } virtual void page_did_request_link_context_menu(CSSPixelPoint, URL::URL const&, [[maybe_unused]] ByteString const& target, [[maybe_unused]] unsigned modifiers) { }
virtual void page_did_request_image_context_menu(CSSPixelPoint, URL::URL const&, [[maybe_unused]] ByteString const& target, [[maybe_unused]] unsigned modifiers, Optional<Gfx::Bitmap const*>) { } virtual void page_did_request_image_context_menu(CSSPixelPoint, URL::URL const&, [[maybe_unused]] ByteString const& target, [[maybe_unused]] unsigned modifiers, Optional<Gfx::Bitmap const*>) { }

View file

@ -1,11 +1,11 @@
/* /*
* Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org> * Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include <LibWeb/DOM/Document.h> #include <LibWeb/DOM/Document.h>
#include <LibWeb/Layout/BlockContainer.h>
#include <LibWeb/Painting/Paintable.h> #include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Painting/PaintableBox.h> #include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/StackingContext.h> #include <LibWeb/Painting/StackingContext.h>

View file

@ -6,8 +6,8 @@
#pragma once #pragma once
#include <AK/NonnullOwnPtr.h>
#include <LibGC/Root.h> #include <LibGC/Root.h>
#include <LibGfx/Cursor.h>
#include <LibWeb/CSS/ComputedValues.h> #include <LibWeb/CSS/ComputedValues.h>
#include <LibWeb/Forward.h> #include <LibWeb/Forward.h>
#include <LibWeb/InvalidateDisplayList.h> #include <LibWeb/InvalidateDisplayList.h>

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org> * Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org> * Copyright (c) 2022-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com> * Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org> * Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
* *

View file

@ -194,7 +194,7 @@ public:
Function<void(URL::URL const&)> on_load_finish; Function<void(URL::URL const&)> on_load_finish;
Function<void(ByteString const& path, i32)> on_request_file; Function<void(ByteString const& path, i32)> on_request_file;
Function<void(Gfx::Bitmap const&)> on_favicon_change; Function<void(Gfx::Bitmap const&)> on_favicon_change;
Function<void(Gfx::StandardCursor)> on_cursor_change; Function<void(Gfx::Cursor const&)> on_cursor_change;
Function<void(Gfx::IntPoint, ByteString const&)> on_request_tooltip_override; Function<void(Gfx::IntPoint, ByteString const&)> on_request_tooltip_override;
Function<void()> on_stop_tooltip_override; Function<void()> on_stop_tooltip_override;
Function<void(ByteString const&)> on_enter_tooltip_area; Function<void(ByteString const&)> on_enter_tooltip_area;

View file

@ -123,16 +123,11 @@ void WebContentClient::did_request_refresh(u64 page_id)
view->reload(); view->reload();
} }
void WebContentClient::did_request_cursor_change(u64 page_id, i32 cursor_type) void WebContentClient::did_request_cursor_change(u64 page_id, Gfx::Cursor const& cursor)
{ {
if (cursor_type < 0 || cursor_type >= (i32)Gfx::StandardCursor::__Count) {
dbgln("DidRequestCursorChange: Bad cursor type");
return;
}
if (auto view = view_for_page_id(page_id); view.has_value()) { if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_cursor_change) if (view->on_cursor_change)
view->on_cursor_change(static_cast<Gfx::StandardCursor>(cursor_type)); view->on_cursor_change(cursor);
} }
} }

View file

@ -54,7 +54,7 @@ private:
virtual void did_paint(u64 page_id, Gfx::IntRect const&, i32) override; virtual void did_paint(u64 page_id, Gfx::IntRect const&, i32) override;
virtual void did_finish_loading(u64 page_id, URL::URL const&) override; virtual void did_finish_loading(u64 page_id, URL::URL const&) override;
virtual void did_request_refresh(u64 page_id) override; virtual void did_request_refresh(u64 page_id) override;
virtual void did_request_cursor_change(u64 page_id, i32) override; virtual void did_request_cursor_change(u64 page_id, Gfx::Cursor const&) override;
virtual void did_change_title(u64 page_id, ByteString const&) override; virtual void did_change_title(u64 page_id, ByteString const&) override;
virtual void did_change_url(u64 page_id, URL::URL const&) override; virtual void did_change_url(u64 page_id, URL::URL const&) override;
virtual void did_request_tooltip_override(u64 page_id, Gfx::IntPoint, ByteString const&) override; virtual void did_request_tooltip_override(u64 page_id, Gfx::IntPoint, ByteString const&) override;

View file

@ -251,9 +251,9 @@ void PageClient::set_viewport_size(Web::DevicePixelSize const& size)
m_pending_set_browser_zoom_request = false; m_pending_set_browser_zoom_request = false;
} }
void PageClient::page_did_request_cursor_change(Gfx::StandardCursor cursor) void PageClient::page_did_request_cursor_change(Gfx::Cursor const& cursor)
{ {
client().async_did_request_cursor_change(m_id, (u32)cursor); client().async_did_request_cursor_change(m_id, cursor);
} }
void PageClient::page_did_layout() void PageClient::page_did_layout()

View file

@ -114,7 +114,7 @@ private:
virtual Web::CSS::PreferredColorScheme preferred_color_scheme() const override { return m_preferred_color_scheme; } virtual Web::CSS::PreferredColorScheme preferred_color_scheme() const override { return m_preferred_color_scheme; }
virtual Web::CSS::PreferredContrast preferred_contrast() const override { return m_preferred_contrast; } virtual Web::CSS::PreferredContrast preferred_contrast() const override { return m_preferred_contrast; }
virtual Web::CSS::PreferredMotion preferred_motion() const override { return m_preferred_motion; } virtual Web::CSS::PreferredMotion preferred_motion() const override { return m_preferred_motion; }
virtual void page_did_request_cursor_change(Gfx::StandardCursor) override; virtual void page_did_request_cursor_change(Gfx::Cursor const&) override;
virtual void page_did_layout() override; virtual void page_did_layout() override;
virtual void page_did_change_title(ByteString const&) override; virtual void page_did_change_title(ByteString const&) override;
virtual void page_did_change_url(URL::URL const&) override; virtual void page_did_change_url(URL::URL const&) override;

View file

@ -1,5 +1,6 @@
#include <LibCore/AnonymousBuffer.h> #include <LibCore/AnonymousBuffer.h>
#include <LibGfx/Color.h> #include <LibGfx/Color.h>
#include <LibGfx/Cursor.h>
#include <LibGfx/ShareableBitmap.h> #include <LibGfx/ShareableBitmap.h>
#include <LibURL/URL.h> #include <LibURL/URL.h>
#include <LibWeb/Cookie/Cookie.h> #include <LibWeb/Cookie/Cookie.h>
@ -24,7 +25,7 @@ endpoint WebContentClient
did_finish_loading(u64 page_id, URL::URL url) =| did_finish_loading(u64 page_id, URL::URL url) =|
did_request_refresh(u64 page_id) =| did_request_refresh(u64 page_id) =|
did_paint(u64 page_id, Gfx::IntRect content_rect, i32 bitmap_id) =| did_paint(u64 page_id, Gfx::IntRect content_rect, i32 bitmap_id) =|
did_request_cursor_change(u64 page_id, i32 cursor_type) =| did_request_cursor_change(u64 page_id, Gfx::Cursor cursor) =|
did_change_title(u64 page_id, ByteString title) =| did_change_title(u64 page_id, ByteString title) =|
did_change_url(u64 page_id, URL::URL url) =| did_change_url(u64 page_id, URL::URL url) =|
did_request_tooltip_override(u64 page_id, Gfx::IntPoint position, ByteString title) =| did_request_tooltip_override(u64 page_id, Gfx::IntPoint position, ByteString title) =|

View file

@ -2,8 +2,8 @@ Harness status: OK
Found 42 tests Found 42 tests
36 Pass 40 Pass
6 Fail 2 Fail
Pass e.style['cursor'] = "auto" should set the property value Pass e.style['cursor'] = "auto" should set the property value
Pass e.style['cursor'] = "default" should set the property value Pass e.style['cursor'] = "default" should set the property value
Pass e.style['cursor'] = "none" should set the property value Pass e.style['cursor'] = "none" should set the property value
@ -40,9 +40,9 @@ Pass e.style['cursor'] = "row-resize" should set the property value
Pass e.style['cursor'] = "all-scroll" should set the property value Pass e.style['cursor'] = "all-scroll" should set the property value
Pass e.style['cursor'] = "zoom-in" should set the property value Pass e.style['cursor'] = "zoom-in" should set the property value
Pass e.style['cursor'] = "zoom-out" should set the property value Pass e.style['cursor'] = "zoom-out" should set the property value
Fail e.style['cursor'] = "url(\"https://example.com/\"), alias" should set the property value Pass e.style['cursor'] = "url(\"https://example.com/\"), alias" should set the property value
Fail e.style['cursor'] = "url(\"https://example.com/\") 1 calc(2 + 0), copy" should set the property value Pass e.style['cursor'] = "url(\"https://example.com/\") 1 calc(2 + 0), copy" should set the property value
Fail e.style['cursor'] = "url(\"https://example.com/\"), url(\"https://example.com/\") 3 -4, move" should set the property value Pass e.style['cursor'] = "url(\"https://example.com/\"), url(\"https://example.com/\") 3 -4, move" should set the property value
Fail e.style['cursor'] = "url(\"https://example.com/\") 5 6, grab" should set the property value Pass e.style['cursor'] = "url(\"https://example.com/\") 5 6, grab" should set the property value
Fail e.style['cursor'] = "image-set(\"https://example.com/\" 1x) 5 6, grab" should set the property value Fail e.style['cursor'] = "image-set(\"https://example.com/\" 1x) 5 6, grab" should set the property value
Fail e.style['cursor'] = "image-set(\"https://example.com/\" 1x, \"https://example.com/highres\" 2x) 5 6, grab" should set the property value Fail e.style['cursor'] = "image-set(\"https://example.com/\" 1x, \"https://example.com/highres\" 2x) 5 6, grab" should set the property value

View file

@ -450,7 +450,15 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_
if (self == nil) { if (self == nil) {
return; return;
} }
if (cursor == Gfx::StandardCursor::Hidden) { if (!cursor.template has<Gfx::StandardCursor>()) {
// FIXME: Implement image cursors in AppKit.
[[NSCursor arrowCursor] set];
return;
}
auto standard_cursor = cursor.template get<Gfx::StandardCursor>();
if (standard_cursor == Gfx::StandardCursor::Hidden) {
if (!m_hidden_cursor.has_value()) { if (!m_hidden_cursor.has_value()) {
m_hidden_cursor.emplace(); m_hidden_cursor.emplace();
} }
@ -460,7 +468,7 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_
m_hidden_cursor.clear(); m_hidden_cursor.clear();
switch (cursor) { switch (standard_cursor) {
case Gfx::StandardCursor::Arrow: case Gfx::StandardCursor::Arrow:
[[NSCursor arrowCursor] set]; [[NSCursor arrowCursor] set];
break; break;

View file

@ -1,6 +1,7 @@
/* /*
* Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org> * Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org> * Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024-2025, Sam Atkins <sam@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -630,9 +631,10 @@ void WebContentView::initialize_client(WebView::ViewImplementation::CreateNewCli
update_screen_rects(); update_screen_rects();
} }
void WebContentView::update_cursor(Gfx::StandardCursor cursor) void WebContentView::update_cursor(Gfx::Cursor cursor)
{ {
switch (cursor) { cursor.visit([this](Gfx::StandardCursor standard_cursor) {
switch (standard_cursor) {
case Gfx::StandardCursor::Hidden: case Gfx::StandardCursor::Hidden:
setCursor(Qt::BlankCursor); setCursor(Qt::BlankCursor);
break; break;
@ -690,7 +692,25 @@ void WebContentView::update_cursor(Gfx::StandardCursor cursor)
default: default:
setCursor(Qt::ArrowCursor); setCursor(Qt::ArrowCursor);
break; break;
} },
[this](Gfx::ImageCursor const& image_cursor) {
if (!image_cursor.bitmap.is_valid()) {
dbgln("Failed to set cursor: Bitmap is invalid.");
return;
} }
auto const& bitmap = *image_cursor.bitmap.bitmap();
auto qimage = QImage { bitmap.scanline_u8(0), bitmap.width(), bitmap.height(), QImage::Format_ARGB32 };
if (qimage.isNull()) {
dbgln("Failed to set cursor: Null QImage.");
return;
}
auto qpixmap = QPixmap::fromImage(qimage);
if (qimage.isNull()) {
dbgln("Failed to set cursor: Couldn't create QPixmap from QImage.");
return;
}
setCursor(QCursor { qpixmap, image_cursor.hotspot.x(), image_cursor.hotspot.y() });
});
} }
Web::DevicePixelSize WebContentView::viewport_size() const Web::DevicePixelSize WebContentView::viewport_size() const

View file

@ -100,7 +100,7 @@ private:
virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override; virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override;
void update_viewport_size(); void update_viewport_size();
void update_cursor(Gfx::StandardCursor cursor); void update_cursor(Gfx::Cursor cursor);
void enqueue_native_event(Web::MouseEvent::Type, QSinglePointEvent const& event); void enqueue_native_event(Web::MouseEvent::Type, QSinglePointEvent const& event);