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
parent fd2414ba35
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 <LibWeb/CSS/Clip.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/StyleValues/AngleStyleValue.h>
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
#include <LibWeb/CSS/StyleValues/ColorSchemeStyleValue.h>
#include <LibWeb/CSS/StyleValues/ContentStyleValue.h>
@ -32,7 +31,6 @@
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/CSS/StyleValues/PositionStyleValue.h>
#include <LibWeb/CSS/StyleValues/RectStyleValue.h>
#include <LibWeb/CSS/StyleValues/ScrollbarGutterStyleValue.h>
#include <LibWeb/CSS/StyleValues/ShadowStyleValue.h>
#include <LibWeb/CSS/StyleValues/StringStyleValue.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();
}
Cursor ComputedProperties::cursor() const
Vector<CursorData> 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<CursorData> 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

View file

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

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2020-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2023-2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -28,7 +29,7 @@
#include <LibWeb/CSS/Size.h>
#include <LibWeb/CSS/StyleValues/AbstractImageStyleValue.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/Transformation.h>
@ -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<NonnullRefPtr<CursorStyleValue>, Cursor>;
using ListStyleType = Variant<CounterStyleNameKeyword, String>;
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<CursorData> 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<Color> accent_color {};
Color webkit_text_fill_color { InitialValues::color() };
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::PointerEvents pointer_events { InitialValues::pointer_events() };
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_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<CursorData> 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; }

View file

@ -368,6 +368,7 @@ private:
RefPtr<CSSStyleValue> parse_counter_increment_value(TokenStream<ComponentValue>&);
RefPtr<CSSStyleValue> parse_counter_reset_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_flex_shorthand_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/ContentStyleValue.h>
#include <LibWeb/CSS/StyleValues/CounterDefinitionsStyleValue.h>
#include <LibWeb/CSS/StyleValues/CursorStyleValue.h>
#include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h>
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.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())
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<CSSStyleValue> Parser::parse_counter_set_value(TokenStream<ComponentValue
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
RefPtr<CSSStyleValue> Parser::parse_aspect_ratio_value(TokenStream<ComponentValue>& tokens)
{

View file

@ -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<CSS::CursorData> 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<Gfx::Cursor> {
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<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
@ -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<Painting::Paintable> paintable;
Optional<int> 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::NodeWithStyle&>(*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);
}

View file

@ -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<Gfx::Bitmap const*>) { }

View file

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

View file

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

View file

@ -1,6 +1,6 @@
/*
* 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) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*

View file

@ -194,7 +194,7 @@ public:
Function<void(URL::URL const&)> on_load_finish;
Function<void(ByteString const& path, i32)> on_request_file;
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()> on_stop_tooltip_override;
Function<void(ByteString const&)> on_enter_tooltip_area;

View file

@ -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<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_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;

View file

@ -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()

View file

@ -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;

View file

@ -1,5 +1,6 @@
#include <LibCore/AnonymousBuffer.h>
#include <LibGfx/Color.h>
#include <LibGfx/Cursor.h>
#include <LibGfx/ShareableBitmap.h>
#include <LibURL/URL.h>
#include <LibWeb/Cookie/Cookie.h>
@ -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) =|

View file

@ -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

View file

@ -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<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()) {
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;

View file

@ -1,6 +1,7 @@
/*
* Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024-2025, Sam Atkins <sam@ladybird.org>
*
* 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

View file

@ -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);