diff --git a/Libraries/LibWeb/CSS/ComputedProperties.cpp b/Libraries/LibWeb/CSS/ComputedProperties.cpp index 548178ffd0e..89330897b58 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.cpp +++ b/Libraries/LibWeb/CSS/ComputedProperties.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -32,7 +31,6 @@ #include #include #include -#include #include #include #include @@ -954,13 +952,30 @@ ContentVisibility ComputedProperties::content_visibility() const return keyword_to_content_visibility(value.to_keyword()).release_value(); } -Cursor ComputedProperties::cursor() const +Vector ComputedProperties::cursor() const { + // Return the first available cursor. auto const& value = property(PropertyID::Cursor); - // FIXME: We don't currently support custom cursors. - if (value.is_url()) - return Cursor::Auto; - return keyword_to_cursor(value.to_keyword()).release_value(); + Vector cursors; + if (value.is_value_list()) { + for (auto const& item : value.as_value_list().values()) { + 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 diff --git a/Libraries/LibWeb/CSS/ComputedProperties.h b/Libraries/LibWeb/CSS/ComputedProperties.h index dab52629d44..32ce17103ba 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.h +++ b/Libraries/LibWeb/CSS/ComputedProperties.h @@ -94,7 +94,7 @@ public: }; ContentDataAndQuoteNestingLevel content(DOM::Element&, u32 initial_quote_nesting_level) const; ContentVisibility content_visibility() const; - Cursor cursor() const; + Vector cursor() const; Variant tab_size() const; WhiteSpace white_space() const; WordBreak word_break() const; diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index d811d4adc50..debda92e1a7 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2020-2023, Andreas Kling + * Copyright (c) 2023-2025, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ @@ -28,7 +29,7 @@ #include #include #include -#include +#include #include #include @@ -79,6 +80,8 @@ struct Containment { bool is_empty() const { return !(size_containment || inline_size_containment || layout_containment || style_containment || paint_containment); } }; +using CursorData = Variant, Cursor>; + using ListStyleType = Variant; class InitialValues { @@ -94,7 +97,7 @@ public: static CSS::Clip clip() { return CSS::Clip::make_auto(); } static CSS::PreferredColorScheme color_scheme() { return CSS::PreferredColorScheme::Auto; } 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::WordBreak word_break() { return CSS::WordBreak::Normal; } 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::PreferredColorScheme color_scheme() const { return m_inherited.color_scheme; } CSS::ContentVisibility content_visibility() const { return m_inherited.content_visibility; } - CSS::Cursor cursor() const { return m_inherited.cursor; } + Vector const& cursor() const { return m_inherited.cursor; } CSS::ContentData content() const { return m_noninherited.content; } CSS::PointerEvents pointer_events() const { return m_inherited.pointer_events; } CSS::Display display() const { return m_noninherited.display; } @@ -575,7 +578,7 @@ protected: Optional accent_color {}; Color webkit_text_fill_color { InitialValues::color() }; CSS::ContentVisibility content_visibility { InitialValues::content_visibility() }; - CSS::Cursor cursor { InitialValues::cursor() }; + Vector cursor { InitialValues::cursor() }; CSS::ImageRendering image_rendering { InitialValues::image_rendering() }; CSS::PointerEvents pointer_events { InitialValues::pointer_events() }; Variant tab_size { InitialValues::tab_size() }; @@ -763,7 +766,7 @@ public: void set_clip(CSS::Clip const& clip) { m_noninherited.clip = clip; } 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_cursor(CSS::Cursor cursor) { m_inherited.cursor = cursor; } + void set_cursor(Vector cursor) { m_inherited.cursor = move(cursor); } 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_background_color(Color color) { m_noninherited.background_color = color; } diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index 5aa37a63376..e51027eced9 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -368,6 +368,7 @@ private: RefPtr parse_counter_increment_value(TokenStream&); RefPtr parse_counter_reset_value(TokenStream&); RefPtr parse_counter_set_value(TokenStream&); + RefPtr parse_cursor_value(TokenStream&); RefPtr parse_display_value(TokenStream&); RefPtr parse_flex_shorthand_value(TokenStream&); RefPtr parse_flex_flow_value(TokenStream&); diff --git a/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp b/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp index 0ecc1684299..03b0b84e6a3 100644 --- a/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -537,6 +538,10 @@ Parser::ParseErrorOr> Parser::parse_css_value(Prope if (auto parsed_value = parse_counter_set_value(tokens); parsed_value && !tokens.has_next_token()) return parsed_value.release_nonnull(); 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: if (auto parsed_value = parse_display_value(tokens); parsed_value && !tokens.has_next_token()) return parsed_value.release_nonnull(); @@ -958,6 +963,72 @@ RefPtr Parser::parse_counter_set_value(TokenStream Parser::parse_cursor_value(TokenStream& tokens) +{ + // [ [ [ ]?,]* ] + // 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 + // [ ]? + // "Conforming user agents may, instead of , support 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 + 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 RefPtr Parser::parse_aspect_ratio_value(TokenStream& tokens) { diff --git a/Libraries/LibWeb/Page/EventHandler.cpp b/Libraries/LibWeb/Page/EventHandler.cpp index 2a19aaa2029..412040e226b 100644 --- a/Libraries/LibWeb/Page/EventHandler.cpp +++ b/Libraries/LibWeb/Page/EventHandler.cpp @@ -83,65 +83,81 @@ static bool parent_element_for_event_dispatch(Painting::Paintable& paintable, GC 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 const& cursor_data, Gfx::StandardCursor auto_cursor) { - switch (cursor) { - case CSS::Cursor::Crosshair: - case CSS::Cursor::Cell: - return Gfx::StandardCursor::Crosshair; - case CSS::Cursor::Grab: - case CSS::Cursor::Grabbing: - return Gfx::StandardCursor::Drag; - case CSS::Cursor::Pointer: - return Gfx::StandardCursor::Hand; - case CSS::Cursor::Help: - return Gfx::StandardCursor::Help; - case CSS::Cursor::None: - return Gfx::StandardCursor::Hidden; - case CSS::Cursor::NotAllowed: - return Gfx::StandardCursor::Disallowed; - case CSS::Cursor::Text: - case CSS::Cursor::VerticalText: - return Gfx::StandardCursor::IBeam; - case CSS::Cursor::Move: - case CSS::Cursor::AllScroll: - return Gfx::StandardCursor::Move; - case CSS::Cursor::Progress: - case CSS::Cursor::Wait: - return Gfx::StandardCursor::Wait; - case CSS::Cursor::ColResize: - return Gfx::StandardCursor::ResizeColumn; - case CSS::Cursor::EResize: - case CSS::Cursor::WResize: - case CSS::Cursor::EwResize: - return Gfx::StandardCursor::ResizeHorizontal; - case CSS::Cursor::RowResize: - return Gfx::StandardCursor::ResizeRow; - case CSS::Cursor::NResize: - case CSS::Cursor::SResize: - case CSS::Cursor::NsResize: - return Gfx::StandardCursor::ResizeVertical; - case CSS::Cursor::NeResize: - case CSS::Cursor::SwResize: - case CSS::Cursor::NeswResize: - return Gfx::StandardCursor::ResizeDiagonalBLTR; - case CSS::Cursor::NwResize: - case CSS::Cursor::SeResize: - case CSS::Cursor::NwseResize: - return Gfx::StandardCursor::ResizeDiagonalTLBR; - case CSS::Cursor::ZoomIn: - case CSS::Cursor::ZoomOut: - return Gfx::StandardCursor::Zoom; - case CSS::Cursor::ContextMenu: - case CSS::Cursor::Alias: - case CSS::Cursor::Copy: - case CSS::Cursor::NoDrop: - // FIXME: No corresponding GFX Standard Cursor, fallthrough to None - case CSS::Cursor::Auto: - case CSS::Cursor::Default: - default: - return Gfx::StandardCursor::None; + for (auto const& cursor : cursor_data) { + auto result = cursor.visit( + [auto_cursor](CSS::Cursor css_cursor) -> Optional { + switch (css_cursor) { + case CSS::Cursor::Crosshair: + case CSS::Cursor::Cell: + return Gfx::StandardCursor::Crosshair; + case CSS::Cursor::Grab: + case CSS::Cursor::Grabbing: + return Gfx::StandardCursor::Drag; + case CSS::Cursor::Pointer: + return Gfx::StandardCursor::Hand; + case CSS::Cursor::Help: + return Gfx::StandardCursor::Help; + case CSS::Cursor::None: + return Gfx::StandardCursor::Hidden; + case CSS::Cursor::NotAllowed: + return Gfx::StandardCursor::Disallowed; + case CSS::Cursor::Text: + case CSS::Cursor::VerticalText: + return Gfx::StandardCursor::IBeam; + case CSS::Cursor::Move: + case CSS::Cursor::AllScroll: + return Gfx::StandardCursor::Move; + case CSS::Cursor::Progress: + case CSS::Cursor::Wait: + return Gfx::StandardCursor::Wait; + case CSS::Cursor::ColResize: + return Gfx::StandardCursor::ResizeColumn; + case CSS::Cursor::EResize: + case CSS::Cursor::WResize: + case CSS::Cursor::EwResize: + return Gfx::StandardCursor::ResizeHorizontal; + case CSS::Cursor::RowResize: + return Gfx::StandardCursor::ResizeRow; + case CSS::Cursor::NResize: + case CSS::Cursor::SResize: + case CSS::Cursor::NsResize: + return Gfx::StandardCursor::ResizeVertical; + case CSS::Cursor::NeResize: + case CSS::Cursor::SwResize: + case CSS::Cursor::NeswResize: + return Gfx::StandardCursor::ResizeDiagonalBLTR; + case CSS::Cursor::NwResize: + case CSS::Cursor::SeResize: + case CSS::Cursor::NwseResize: + return Gfx::StandardCursor::ResizeDiagonalTLBR; + case CSS::Cursor::ZoomIn: + case CSS::Cursor::ZoomOut: + return Gfx::StandardCursor::Zoom; + case CSS::Cursor::Auto: + return auto_cursor; + case CSS::Cursor::ContextMenu: + case CSS::Cursor::Alias: + case CSS::Cursor::Copy: + case CSS::Cursor::NoDrop: + // FIXME: No corresponding GFX Standard Cursor, fallthrough to None + case CSS::Cursor::Default: + default: + return Gfx::StandardCursor::None; + } + }, + [&layout_node](NonnullRefPtr const& cursor_style_value) -> Optional { + 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 @@ -692,7 +708,7 @@ EventResult EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSP bool hovered_node_changed = false; bool is_hovering_link = false; - Gfx::StandardCursor hovered_node_cursor = Gfx::StandardCursor::None; + Gfx::Cursor hovered_node_cursor = Gfx::StandardCursor::None; GC::Ptr paintable; Optional start_index; @@ -721,7 +737,7 @@ EventResult EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSP 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(); // FIXME: Handle other values for pointer-events. VERIFY(pointer_events != CSS::PointerEvents::None); @@ -739,15 +755,9 @@ EventResult EventHandler::handle_mousemove(CSSPixelPoint viewport_position, CSSP is_hovering_link = true; if (paintable->layout_node().is_text_node()) { - if (cursor == CSS::Cursor::Auto) - hovered_node_cursor = Gfx::StandardCursor::IBeam; - else - hovered_node_cursor = cursor_css_to_gfx(cursor); + hovered_node_cursor = resolve_cursor(*paintable->layout_node().parent(), cursor_data, Gfx::StandardCursor::IBeam); } else if (node->is_element()) { - if (cursor == CSS::Cursor::Auto) - hovered_node_cursor = Gfx::StandardCursor::Arrow; - else - hovered_node_cursor = cursor_css_to_gfx(cursor); + hovered_node_cursor = resolve_cursor(static_cast(*layout_node), cursor_data, Gfx::StandardCursor::Arrow); } 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(); - 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.client().page_did_request_cursor_change(hovered_node_cursor); } diff --git a/Libraries/LibWeb/Page/Page.h b/Libraries/LibWeb/Page/Page.h index 764aedc84cb..f776a01c9e3 100644 --- a/Libraries/LibWeb/Page/Page.h +++ b/Libraries/LibWeb/Page/Page.h @@ -127,8 +127,8 @@ public: 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; } - Gfx::StandardCursor current_cursor() const { return m_current_cursor; } - void set_current_cursor(Gfx::StandardCursor cursor) { m_current_cursor = cursor; } + Gfx::Cursor current_cursor() const { return m_current_cursor; } + void set_current_cursor(Gfx::Cursor cursor) { m_current_cursor = move(cursor); } DevicePixelPoint window_position() const { return m_window_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_in_tooltip_area { false }; - Gfx::StandardCursor m_current_cursor { Gfx::StandardCursor::Arrow }; + Gfx::Cursor m_current_cursor { Gfx::StandardCursor::Arrow }; DevicePixelPoint m_window_position {}; DevicePixelSize m_window_size {}; @@ -338,7 +338,7 @@ public: 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_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_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) { } diff --git a/Libraries/LibWeb/Painting/Paintable.cpp b/Libraries/LibWeb/Painting/Paintable.cpp index 53100a3abe5..6beffd0c00d 100644 --- a/Libraries/LibWeb/Painting/Paintable.cpp +++ b/Libraries/LibWeb/Painting/Paintable.cpp @@ -1,11 +1,11 @@ /* * Copyright (c) 2022-2023, Andreas Kling + * Copyright (c) 2025, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ #include -#include #include #include #include diff --git a/Libraries/LibWeb/Painting/Paintable.h b/Libraries/LibWeb/Painting/Paintable.h index a2ec5ead442..a47211cb64d 100644 --- a/Libraries/LibWeb/Painting/Paintable.h +++ b/Libraries/LibWeb/Painting/Paintable.h @@ -6,8 +6,8 @@ #pragma once -#include #include +#include #include #include #include diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index de22f009f76..70f6c55050f 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -1,6 +1,6 @@ /* * Copyright (c) 2022-2023, Andreas Kling - * Copyright (c) 2022-2023, Sam Atkins + * Copyright (c) 2022-2025, Sam Atkins * Copyright (c) 2024, Aliaksandr Kalenik * Copyright (c) 2025, Jelle Raaijmakers * diff --git a/Libraries/LibWebView/ViewImplementation.h b/Libraries/LibWebView/ViewImplementation.h index 9a85d2d0bfe..875c6f1d347 100644 --- a/Libraries/LibWebView/ViewImplementation.h +++ b/Libraries/LibWebView/ViewImplementation.h @@ -194,7 +194,7 @@ public: Function on_load_finish; Function on_request_file; Function on_favicon_change; - Function on_cursor_change; + Function on_cursor_change; Function on_request_tooltip_override; Function on_stop_tooltip_override; Function on_enter_tooltip_area; diff --git a/Libraries/LibWebView/WebContentClient.cpp b/Libraries/LibWebView/WebContentClient.cpp index 96a811f4663..9f7fdd71071 100644 --- a/Libraries/LibWebView/WebContentClient.cpp +++ b/Libraries/LibWebView/WebContentClient.cpp @@ -123,16 +123,11 @@ void WebContentClient::did_request_refresh(u64 page_id) 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 (view->on_cursor_change) - view->on_cursor_change(static_cast(cursor_type)); + view->on_cursor_change(cursor); } } diff --git a/Libraries/LibWebView/WebContentClient.h b/Libraries/LibWebView/WebContentClient.h index 52d4b7e3b96..22e64927e22 100644 --- a/Libraries/LibWebView/WebContentClient.h +++ b/Libraries/LibWebView/WebContentClient.h @@ -54,7 +54,7 @@ private: 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_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_url(u64 page_id, URL::URL const&) override; virtual void did_request_tooltip_override(u64 page_id, Gfx::IntPoint, ByteString const&) override; diff --git a/Services/WebContent/PageClient.cpp b/Services/WebContent/PageClient.cpp index d555a11442c..b3e0d3b1a94 100644 --- a/Services/WebContent/PageClient.cpp +++ b/Services/WebContent/PageClient.cpp @@ -251,9 +251,9 @@ void PageClient::set_viewport_size(Web::DevicePixelSize const& size) 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() diff --git a/Services/WebContent/PageClient.h b/Services/WebContent/PageClient.h index b6b201c5268..b028f5f12c4 100644 --- a/Services/WebContent/PageClient.h +++ b/Services/WebContent/PageClient.h @@ -114,7 +114,7 @@ private: 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::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_change_title(ByteString const&) override; virtual void page_did_change_url(URL::URL const&) override; diff --git a/Services/WebContent/WebContentClient.ipc b/Services/WebContent/WebContentClient.ipc index d52fe567b66..cab39941070 100644 --- a/Services/WebContent/WebContentClient.ipc +++ b/Services/WebContent/WebContentClient.ipc @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -24,7 +25,7 @@ endpoint WebContentClient did_finish_loading(u64 page_id, URL::URL url) =| did_request_refresh(u64 page_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_url(u64 page_id, URL::URL url) =| did_request_tooltip_override(u64 page_id, Gfx::IntPoint position, ByteString title) =| diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-ui/parsing/cursor-valid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-ui/parsing/cursor-valid.txt index af1af0a3333..17b89f21c00 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-ui/parsing/cursor-valid.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-ui/parsing/cursor-valid.txt @@ -2,8 +2,8 @@ Harness status: OK Found 42 tests -36 Pass -6 Fail +40 Pass +2 Fail Pass e.style['cursor'] = "auto" 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 @@ -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'] = "zoom-in" 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 -Fail 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 -Fail e.style['cursor'] = "url(\"https://example.com/\") 5 6, grab" should set the property value +Pass e.style['cursor'] = "url(\"https://example.com/\"), alias" should set the property value +Pass 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/\"), url(\"https://example.com/\") 3 -4, move" 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, \"https://example.com/highres\" 2x) 5 6, grab" should set the property value \ No newline at end of file diff --git a/UI/AppKit/Interface/LadybirdWebView.mm b/UI/AppKit/Interface/LadybirdWebView.mm index 64144c57c7b..fed1b02b0e0 100644 --- a/UI/AppKit/Interface/LadybirdWebView.mm +++ b/UI/AppKit/Interface/LadybirdWebView.mm @@ -450,7 +450,15 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_ if (self == nil) { return; } - if (cursor == Gfx::StandardCursor::Hidden) { + if (!cursor.template has()) { + // FIXME: Implement image cursors in AppKit. + [[NSCursor arrowCursor] set]; + return; + } + + auto standard_cursor = cursor.template get(); + + if (standard_cursor == Gfx::StandardCursor::Hidden) { if (!m_hidden_cursor.has_value()) { m_hidden_cursor.emplace(); } @@ -460,7 +468,7 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_ m_hidden_cursor.clear(); - switch (cursor) { + switch (standard_cursor) { case Gfx::StandardCursor::Arrow: [[NSCursor arrowCursor] set]; break; diff --git a/UI/Qt/WebContentView.cpp b/UI/Qt/WebContentView.cpp index 265507bc146..b1b71898b91 100644 --- a/UI/Qt/WebContentView.cpp +++ b/UI/Qt/WebContentView.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2022-2023, Andreas Kling * Copyright (c) 2023, Linus Groh + * Copyright (c) 2024-2025, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ @@ -630,67 +631,86 @@ void WebContentView::initialize_client(WebView::ViewImplementation::CreateNewCli update_screen_rects(); } -void WebContentView::update_cursor(Gfx::StandardCursor cursor) +void WebContentView::update_cursor(Gfx::Cursor cursor) { - switch (cursor) { - case Gfx::StandardCursor::Hidden: - setCursor(Qt::BlankCursor); - break; - case Gfx::StandardCursor::Arrow: - setCursor(Qt::ArrowCursor); - break; - case Gfx::StandardCursor::Crosshair: - setCursor(Qt::CrossCursor); - break; - case Gfx::StandardCursor::IBeam: - setCursor(Qt::IBeamCursor); - break; - case Gfx::StandardCursor::ResizeHorizontal: - setCursor(Qt::SizeHorCursor); - break; - case Gfx::StandardCursor::ResizeVertical: - setCursor(Qt::SizeVerCursor); - break; - case Gfx::StandardCursor::ResizeDiagonalTLBR: - setCursor(Qt::SizeFDiagCursor); - break; - case Gfx::StandardCursor::ResizeDiagonalBLTR: - setCursor(Qt::SizeBDiagCursor); - break; - case Gfx::StandardCursor::ResizeColumn: - setCursor(Qt::SplitHCursor); - break; - case Gfx::StandardCursor::ResizeRow: - setCursor(Qt::SplitVCursor); - break; - case Gfx::StandardCursor::Hand: - setCursor(Qt::PointingHandCursor); - break; - case Gfx::StandardCursor::Help: - setCursor(Qt::WhatsThisCursor); - break; - case Gfx::StandardCursor::Drag: - setCursor(Qt::ClosedHandCursor); - break; - case Gfx::StandardCursor::DragCopy: - setCursor(Qt::DragCopyCursor); - break; - case Gfx::StandardCursor::Move: - setCursor(Qt::DragMoveCursor); - break; - case Gfx::StandardCursor::Wait: - setCursor(Qt::BusyCursor); - break; - case Gfx::StandardCursor::Disallowed: - setCursor(Qt::ForbiddenCursor); - break; - case Gfx::StandardCursor::Eyedropper: - case Gfx::StandardCursor::Zoom: - // FIXME: No corresponding Qt cursors, default to Arrow - default: - setCursor(Qt::ArrowCursor); - break; - } + cursor.visit([this](Gfx::StandardCursor standard_cursor) { + switch (standard_cursor) { + case Gfx::StandardCursor::Hidden: + setCursor(Qt::BlankCursor); + break; + case Gfx::StandardCursor::Arrow: + setCursor(Qt::ArrowCursor); + break; + case Gfx::StandardCursor::Crosshair: + setCursor(Qt::CrossCursor); + break; + case Gfx::StandardCursor::IBeam: + setCursor(Qt::IBeamCursor); + break; + case Gfx::StandardCursor::ResizeHorizontal: + setCursor(Qt::SizeHorCursor); + break; + case Gfx::StandardCursor::ResizeVertical: + setCursor(Qt::SizeVerCursor); + break; + case Gfx::StandardCursor::ResizeDiagonalTLBR: + setCursor(Qt::SizeFDiagCursor); + break; + case Gfx::StandardCursor::ResizeDiagonalBLTR: + setCursor(Qt::SizeBDiagCursor); + break; + case Gfx::StandardCursor::ResizeColumn: + setCursor(Qt::SplitHCursor); + break; + case Gfx::StandardCursor::ResizeRow: + setCursor(Qt::SplitVCursor); + break; + case Gfx::StandardCursor::Hand: + setCursor(Qt::PointingHandCursor); + break; + case Gfx::StandardCursor::Help: + setCursor(Qt::WhatsThisCursor); + break; + case Gfx::StandardCursor::Drag: + setCursor(Qt::ClosedHandCursor); + break; + case Gfx::StandardCursor::DragCopy: + setCursor(Qt::DragCopyCursor); + break; + case Gfx::StandardCursor::Move: + setCursor(Qt::DragMoveCursor); + break; + case Gfx::StandardCursor::Wait: + setCursor(Qt::BusyCursor); + break; + case Gfx::StandardCursor::Disallowed: + setCursor(Qt::ForbiddenCursor); + break; + case Gfx::StandardCursor::Eyedropper: + case Gfx::StandardCursor::Zoom: + // FIXME: No corresponding Qt cursors, default to Arrow + default: + setCursor(Qt::ArrowCursor); + 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 diff --git a/UI/Qt/WebContentView.h b/UI/Qt/WebContentView.h index 292753e26ff..93fa0b30468 100644 --- a/UI/Qt/WebContentView.h +++ b/UI/Qt/WebContentView.h @@ -100,7 +100,7 @@ private: virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override; 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);