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

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