diff --git a/src/backends/meta-cursor-sprite-xcursor.h b/src/backends/meta-cursor-sprite-xcursor.h index a3f599b26..33f112ba2 100644 --- a/src/backends/meta-cursor-sprite-xcursor.h +++ b/src/backends/meta-cursor-sprite-xcursor.h @@ -38,6 +38,7 @@ MetaCursor meta_cursor_sprite_xcursor_get_cursor (MetaCursorSpriteXcursor *sprit XcursorImage * meta_cursor_sprite_xcursor_get_current_image (MetaCursorSpriteXcursor *sprite_xcursor); +META_EXPORT_TEST const char * meta_cursor_get_name (MetaCursor cursor); const char * meta_cursor_get_legacy_name (MetaCursor cursor); diff --git a/src/backends/meta-cursor-tracker-private.h b/src/backends/meta-cursor-tracker-private.h index 995cbb5a4..48e86e72a 100644 --- a/src/backends/meta-cursor-tracker-private.h +++ b/src/backends/meta-cursor-tracker-private.h @@ -42,6 +42,9 @@ void meta_cursor_tracker_set_root_cursor (MetaCursorTracker *tracker, void meta_cursor_tracker_invalidate_position (MetaCursorTracker *tracker); +META_EXPORT_TEST +gboolean meta_cursor_tracker_has_window_cursor (MetaCursorTracker *tracker); + void meta_cursor_tracker_track_position (MetaCursorTracker *tracker); void meta_cursor_tracker_untrack_position (MetaCursorTracker *tracker); diff --git a/src/backends/meta-cursor-tracker.c b/src/backends/meta-cursor-tracker.c index 0680a1945..e6c30aa69 100644 --- a/src/backends/meta-cursor-tracker.c +++ b/src/backends/meta-cursor-tracker.c @@ -346,6 +346,15 @@ set_window_cursor (MetaCursorTracker *tracker, sync_cursor (tracker); } +gboolean +meta_cursor_tracker_has_window_cursor (MetaCursorTracker *tracker) +{ + MetaCursorTrackerPrivate *priv = + meta_cursor_tracker_get_instance_private (tracker); + + return priv->has_window_cursor; +} + /** * meta_cursor_tracker_get_sprite: * @tracker: a #MetaCursorTracker diff --git a/src/backends/meta-logical-monitor.h b/src/backends/meta-logical-monitor.h index 51b4c070f..58b266681 100644 --- a/src/backends/meta-logical-monitor.h +++ b/src/backends/meta-logical-monitor.h @@ -47,6 +47,7 @@ struct _MetaLogicalMonitor typedef struct _MetaLogicalMonitorId MetaLogicalMonitorId; #define META_TYPE_LOGICAL_MONITOR (meta_logical_monitor_get_type ()) +META_EXPORT_TEST G_DECLARE_FINAL_TYPE (MetaLogicalMonitor, meta_logical_monitor, META, LOGICAL_MONITOR, GObject) @@ -75,6 +76,7 @@ gboolean meta_logical_monitor_is_primary (MetaLogicalMonitor *logical_monitor); void meta_logical_monitor_make_primary (MetaLogicalMonitor *logical_monitor); +META_EXPORT_TEST float meta_logical_monitor_get_scale (MetaLogicalMonitor *logical_monitor); MtkMonitorTransform meta_logical_monitor_get_transform (MetaLogicalMonitor *logical_monitor); diff --git a/src/core/meta-fraction.h b/src/core/meta-fraction.h index d24ae2ce0..6704282bf 100644 --- a/src/core/meta-fraction.h +++ b/src/core/meta-fraction.h @@ -19,10 +19,13 @@ #pragma once +#include "core/util-private.h" + typedef struct _MetaFraction { int num; int denom; } MetaFraction; +META_EXPORT_TEST MetaFraction meta_fraction_from_double (double src); diff --git a/src/tests/cursor-tests.c b/src/tests/cursor-tests.c new file mode 100644 index 000000000..3c0c6a462 --- /dev/null +++ b/src/tests/cursor-tests.c @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2024 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#include "config.h" + +#include "backends/meta-cursor-sprite-xcursor.h" +#include "backends/meta-cursor-tracker-private.h" +#include "backends/meta-logical-monitor.h" +#include "clutter/clutter.h" +#include "compositor/meta-window-actor-private.h" +#include "core/meta-fraction.h" +#include "core/util-private.h" +#include "meta/meta-wayland-compositor.h" +#include "tests/meta-test/meta-context-test.h" +#include "tests/meta-monitor-test-utils.h" +#include "tests/meta-ref-test.h" +#include "tests/meta-wayland-test-driver.h" +#include "tests/meta-wayland-test-utils.h" + +struct _MetaCrossOverlay +{ + GObject parent; +}; + +static MetaContext *test_context; +static MetaWaylandTestDriver *test_driver; + +static void clutter_content_iface_init (ClutterContentInterface *iface); + +#define META_TYPE_CROSS_OVERLAY (meta_cross_overlay_get_type ()) +G_DECLARE_FINAL_TYPE (MetaCrossOverlay, meta_cross_overlay, + META, CROSS_OVERLAY, GObject) +G_DEFINE_TYPE_WITH_CODE (MetaCrossOverlay, meta_cross_overlay, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTENT, + clutter_content_iface_init)) + +static void +meta_cross_overlay_paint_content (ClutterContent *content, + ClutterActor *actor, + ClutterPaintNode *node, + ClutterPaintContext *paint_context) +{ + g_autoptr (ClutterPaintNode) cross_node = NULL; + ClutterActorBox allocation; + ClutterActorBox horizontal; + ClutterActorBox vertical; + CoglColor color; + + clutter_actor_get_allocation_box (actor, &allocation); + + cogl_color_init_from_4f (&color, 0.0f, 0.0f, 0.0f, 1.0f); + cross_node = clutter_color_node_new (&color); + clutter_paint_node_add_child (node, cross_node); + + horizontal = (ClutterActorBox) { + .x1 = allocation.x1, + .y1 = (allocation.y2 - allocation.y1) / 2 - 0.5f, + .x2 = allocation.x2, + .y2 = (allocation.y2 - allocation.y1) / 2 + 0.5f, + }; + vertical = (ClutterActorBox) { + .x1 = (allocation.x2 - allocation.x1) / 2 - 0.5f, + .y1 = allocation.y1, + .x2 = (allocation.x2 - allocation.x1) / 2 + 0.5f, + .y2 = allocation.y2, + }; + + clutter_paint_node_add_rectangle (cross_node, &horizontal); + clutter_paint_node_add_rectangle (cross_node, &vertical); +} + +static void +clutter_content_iface_init (ClutterContentInterface *iface) +{ + iface->paint_content = meta_cross_overlay_paint_content; +} + +static void +meta_cross_overlay_class_init (MetaCrossOverlayClass *klass) +{ +} + +static void +meta_cross_overlay_init (MetaCrossOverlay *overlay) +{ +} + +static void +on_stage_size_changed (ClutterActor *stage, + GParamSpec *pspec, + ClutterActor *overlay_actor) +{ + float width, height; + + clutter_actor_get_size (stage, &width, &height); + clutter_actor_set_size (overlay_actor, width, height); +} + +static ClutterActor * +create_overlay_actor (void) +{ + MetaBackend *backend = meta_context_get_backend (test_context); + ClutterActor *stage = meta_backend_get_stage (backend); + ClutterActor *actor; + g_autoptr (ClutterContent) content = NULL; + + content = g_object_new (META_TYPE_CROSS_OVERLAY, NULL); + actor = clutter_actor_new (); + clutter_actor_set_content (actor, content); + clutter_actor_set_name (actor, "cross-overlay"); + clutter_actor_show (actor); + + clutter_actor_add_child (stage, actor); + g_signal_connect_object (stage, "notify::size", + G_CALLBACK (on_stage_size_changed), + actor, + G_CONNECT_DEFAULT); + + return actor; +} + +static ClutterStageView * +setup_test_case (int width, + int height, + float scale, + MetaLogicalMonitorLayoutMode layout_mode, + ClutterVirtualInputDevice *virtual_pointer) +{ + MetaBackend *backend = meta_context_get_backend (test_context); + MetaMonitorManager *monitor_manager = + meta_backend_get_monitor_manager (backend); + MetaMonitorManagerTest *monitor_manager_test = + META_MONITOR_MANAGER_TEST (monitor_manager); + MetaRenderer *renderer = meta_backend_get_renderer (backend); + GList *logical_monitors; + MetaLogicalMonitor *logical_monitor; + MetaFraction scale_fraction; + GList *views; + g_autofree char *output_serial = NULL; + MetaMonitorTestSetup *test_setup; + MonitorTestCaseSetup test_case_setup = { + .modes = { + { + .width = width, + .height = height, + .refresh_rate = 60.0 + }, + }, + .n_modes = 1, + .outputs = { + { + .crtc = 0, + .modes = { 0 }, + .n_modes = 1, + .preferred_mode = 0, + .possible_crtcs = { 0 }, + .n_possible_crtcs = 1, + .width_mm = 150, + .height_mm = 85, + .scale = scale, + }, + }, + .n_outputs = 1, + .crtcs = { + { + .current_mode = -1 + }, + }, + .n_crtcs = 1 + }; + static int output_serial_counter = 0x12300000; + + /* Always generate unique serials to never trigger policy trying to inherit + * the scale from previous configurations. + */ + output_serial = g_strdup_printf ("0x%x", output_serial_counter++); + test_case_setup.outputs[0].serial = output_serial; + + meta_monitor_manager_test_set_layout_mode (monitor_manager_test, + layout_mode); + test_setup = meta_create_monitor_test_setup (backend, + &test_case_setup, + MONITOR_TEST_FLAG_NO_STORED); + meta_monitor_manager_test_emulate_hotplug (monitor_manager_test, test_setup); + + logical_monitors = meta_monitor_manager_get_logical_monitors (monitor_manager); + g_assert_cmpuint (g_list_length (logical_monitors), ==, 1); + logical_monitor = META_LOGICAL_MONITOR (logical_monitors->data); + g_assert_cmpfloat_with_epsilon (meta_logical_monitor_get_scale (logical_monitor), + scale, + FLT_EPSILON); + + scale_fraction = meta_fraction_from_double (scale); + + meta_wayland_test_driver_set_property_int (test_driver, + "scale-num", + scale_fraction.num); + meta_wayland_test_driver_set_property_int (test_driver, + "scale-denom", + scale_fraction.denom); + + switch (layout_mode) + { + case META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL: + clutter_virtual_input_device_notify_absolute_motion (virtual_pointer, + g_get_monotonic_time (), + width / scale / 2.0f, + height / scale / 2.0f); + break; + case META_LOGICAL_MONITOR_LAYOUT_MODE_PHYSICAL: + clutter_virtual_input_device_notify_absolute_motion (virtual_pointer, + g_get_monotonic_time (), + width / 2.0f, + height / 2.0f); + break; + } + + meta_flush_input (test_context); + + views = meta_renderer_get_views (renderer); + g_assert_cmpuint (g_list_length (views), ==, 1); + return CLUTTER_STAGE_VIEW (views->data); +} + +static const char * +layout_mode_to_string (MetaLogicalMonitorLayoutMode layout_mode) +{ + switch (layout_mode) + { + case META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL: + return "logical"; + case META_LOGICAL_MONITOR_LAYOUT_MODE_PHYSICAL: + return "physical"; + } + g_assert_not_reached (); +} + +static void +wait_for_no_windows (void) +{ + MetaDisplay *display = meta_context_get_display (test_context); + + while (TRUE) + { + g_autoptr (GList) windows = NULL; + + windows = meta_display_list_all_windows (display); + if (!windows) + return; + + g_main_context_iteration (NULL, TRUE); + } +} + +static void +meta_wait_for_window_cursor (void) +{ + MetaBackend *backend = meta_context_get_backend (test_context); + MetaCursorTracker *cursor_tracker = meta_backend_get_cursor_tracker (backend); + + while (!meta_cursor_tracker_has_window_cursor (cursor_tracker)) + g_main_context_iteration (NULL, TRUE); +} + +static void +meta_test_native_cursor_scaling (void) +{ + MetaBackend *backend = meta_context_get_backend (test_context); + MetaDisplay *display = meta_context_get_display (test_context); + ClutterSeat *seat = meta_backend_get_default_seat (backend); + g_autoptr (ClutterVirtualInputDevice) virtual_pointer = NULL; + ClutterActor *overlay_actor; + ClutterStageView *view; + MetaCursor cursor; + const char *cursor_name; + struct { + int width; + int height; + float scale; + MetaLogicalMonitorLayoutMode layout_mode; + } test_cases[] = { + { + .width = 1920, .height = 1080, .scale = 1.0, + .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL, + }, + { + .width = 1920, .height = 1080, .scale = 1.0, + .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_PHYSICAL, + }, + { + .width = 1920, .height = 1080, .scale = 2.0, + .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL, + }, + { + .width = 1920, .height = 1080, .scale = 2.0, + .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_PHYSICAL, + }, + }; + int i; + + cursor = META_CURSOR_MOVE_OR_RESIZE_WINDOW; + meta_display_set_cursor (display, cursor); + virtual_pointer = clutter_seat_create_virtual_device (seat, + CLUTTER_POINTER_DEVICE); + overlay_actor = create_overlay_actor (); + + for (i = 0; i < G_N_ELEMENTS (test_cases); i++) + { + MetaWaylandTestClient *test_client; + g_autofree char *ref_test_name = NULL; + MetaWindow *window; + MetaWindowActor *window_actor; + + g_debug ("Testing monitor resolution %dx%d with scale %f and " + "%s layout mode", + test_cases[i].width, test_cases[i].height, test_cases[i].scale, + layout_mode_to_string (test_cases[i].layout_mode)); + + wait_for_no_windows (); + + ref_test_name = g_strdup_printf ("%s/%d", g_test_get_path (), i); + + view = setup_test_case (test_cases[i].width, test_cases[i].height, + test_cases[i].scale, + test_cases[i].layout_mode, + virtual_pointer); + meta_ref_test_verify_view (view, + ref_test_name, + 0, + meta_ref_test_determine_ref_test_flag ()); + + cursor_name = meta_cursor_get_name (cursor); + test_client = + meta_wayland_test_client_new_with_args (test_context, + "cursor-tests-client", + cursor_name, + NULL); + meta_wayland_test_driver_wait_for_sync_point (test_driver, 0); + + window = meta_find_window_from_title (test_context, + "cursor-tests-surface"); + g_assert_nonnull (window); + meta_wait_for_window_shown (window); + window_actor = meta_window_actor_from_window (window); + g_assert_nonnull (window_actor); + meta_wait_for_window_cursor (); + + meta_ref_test_verify_view (view, + ref_test_name, + 1, + meta_ref_test_determine_ref_test_flag ()); + + meta_wayland_test_driver_emit_sync_event (test_driver, 0); + + g_object_add_weak_pointer (G_OBJECT (window_actor), + (gpointer *) &window_actor); + meta_wayland_test_client_finish (test_client); + while (window_actor) + g_main_context_iteration (NULL, TRUE); + } + + clutter_actor_destroy (overlay_actor); +} + +static void +init_tests (void) +{ + g_test_add_func ("/backends/native/cursor/scaling", + meta_test_native_cursor_scaling); +} + +static void +on_before_tests (void) +{ + MetaWaylandCompositor *compositor = + meta_context_get_wayland_compositor (test_context); + + test_driver = meta_wayland_test_driver_new (compositor); + meta_wayland_test_driver_set_property_int (test_driver, + "cursor-theme-size", + meta_prefs_get_cursor_size ()); +} + +static void +on_after_tests (void) +{ + g_clear_object (&test_driver); +} + +int +main (int argc, + char **argv) +{ + g_autoptr (MetaContext) context = NULL; + + context = meta_create_test_context (META_CONTEXT_TEST_TYPE_TEST, + (META_CONTEXT_TEST_FLAG_NO_X11 | + META_CONTEXT_TEST_FLAG_TEST_CLIENT | + META_CONTEXT_TEST_FLAG_NO_ANIMATIONS)); + g_assert_true (meta_context_configure (context, &argc, &argv, NULL)); + meta_context_test_set_background_color (META_CONTEXT_TEST (context), + COGL_COLOR_INIT (255, 255, 255, 255)); + + test_context = context; + + init_tests (); + + g_signal_connect (context, "before-tests", + G_CALLBACK (on_before_tests), NULL); + g_signal_connect (context, "after-tests", + G_CALLBACK (on_after_tests), NULL); + + return meta_context_test_run_tests (META_CONTEXT_TEST (context), + META_TEST_RUN_FLAG_NONE); +} diff --git a/src/tests/meson.build b/src/tests/meson.build index 487421d26..77fab2ffd 100644 --- a/src/tests/meson.build +++ b/src/tests/meson.build @@ -447,6 +447,17 @@ test_cases += [ 'meta-thread-impl-test.h', ], }, + { + 'name': 'cursor-tests', + 'suite': 'backends/native', + 'sources': [ + 'cursor-tests.c', + wayland_test_utils, + ], + 'depends': [ + test_client_executables.get('cursor-tests-client'), + ] + }, ] # KMS tests diff --git a/src/tests/ref-tests/backends_native_cursor_scaling_0_0.ref.png b/src/tests/ref-tests/backends_native_cursor_scaling_0_0.ref.png new file mode 100644 index 000000000..bbf50dfda Binary files /dev/null and b/src/tests/ref-tests/backends_native_cursor_scaling_0_0.ref.png differ diff --git a/src/tests/ref-tests/backends_native_cursor_scaling_0_1.ref.png b/src/tests/ref-tests/backends_native_cursor_scaling_0_1.ref.png new file mode 100644 index 000000000..bbf50dfda Binary files /dev/null and b/src/tests/ref-tests/backends_native_cursor_scaling_0_1.ref.png differ diff --git a/src/tests/ref-tests/backends_native_cursor_scaling_1_0.ref.png b/src/tests/ref-tests/backends_native_cursor_scaling_1_0.ref.png new file mode 100644 index 000000000..bbf50dfda Binary files /dev/null and b/src/tests/ref-tests/backends_native_cursor_scaling_1_0.ref.png differ diff --git a/src/tests/ref-tests/backends_native_cursor_scaling_1_1.ref.png b/src/tests/ref-tests/backends_native_cursor_scaling_1_1.ref.png new file mode 100644 index 000000000..bbf50dfda Binary files /dev/null and b/src/tests/ref-tests/backends_native_cursor_scaling_1_1.ref.png differ diff --git a/src/tests/ref-tests/backends_native_cursor_scaling_2_0.ref.png b/src/tests/ref-tests/backends_native_cursor_scaling_2_0.ref.png new file mode 100644 index 000000000..7c58f816f Binary files /dev/null and b/src/tests/ref-tests/backends_native_cursor_scaling_2_0.ref.png differ diff --git a/src/tests/ref-tests/backends_native_cursor_scaling_2_1.ref.png b/src/tests/ref-tests/backends_native_cursor_scaling_2_1.ref.png new file mode 100644 index 000000000..7c58f816f Binary files /dev/null and b/src/tests/ref-tests/backends_native_cursor_scaling_2_1.ref.png differ diff --git a/src/tests/ref-tests/backends_native_cursor_scaling_3_0.ref.png b/src/tests/ref-tests/backends_native_cursor_scaling_3_0.ref.png new file mode 100644 index 000000000..237553ad2 Binary files /dev/null and b/src/tests/ref-tests/backends_native_cursor_scaling_3_0.ref.png differ diff --git a/src/tests/ref-tests/backends_native_cursor_scaling_3_1.ref.png b/src/tests/ref-tests/backends_native_cursor_scaling_3_1.ref.png new file mode 100644 index 000000000..237553ad2 Binary files /dev/null and b/src/tests/ref-tests/backends_native_cursor_scaling_3_1.ref.png differ diff --git a/src/tests/wayland-test-clients/cursor-tests-client.c b/src/tests/wayland-test-clients/cursor-tests-client.c new file mode 100644 index 000000000..3ccd4bc50 --- /dev/null +++ b/src/tests/wayland-test-clients/cursor-tests-client.c @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2024 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#include "config.h" + +#include + +#include "wayland-test-client-utils.h" + +static char *cursor_name; + +static struct wl_surface *cursor_surface; + +static void +on_pointer_enter (WaylandSurface *surface, + struct wl_pointer *pointer, + uint32_t serial) +{ + WaylandDisplay *display = surface->display; + struct wl_cursor_theme *cursor_theme; + struct wl_cursor *cursor; + struct wl_cursor_image *image; + struct wl_buffer *buffer; + int theme_size; + int num, denom; + float scale; + int ceiled_scale; + + theme_size = lookup_property_int (display, "cursor-theme-size"); + num = lookup_property_int (display, "scale-num"); + denom = lookup_property_int (display, "scale-denom"); + scale = (float) num / (float) denom; + ceiled_scale = (int) ceilf (scale); + + cursor_theme = wl_cursor_theme_load (NULL, + (int) (theme_size * ceilf (scale)), + display->shm); + cursor = wl_cursor_theme_get_cursor (cursor_theme, cursor_name); + + if (!cursor_surface) + cursor_surface = wl_compositor_create_surface (display->compositor); + + image = cursor->images[0]; + buffer = wl_cursor_image_get_buffer (image); + g_assert_nonnull (buffer); + + wl_pointer_set_cursor (pointer, serial, + cursor_surface, + image->hotspot_x / ceiled_scale, + image->hotspot_y / ceiled_scale); + wl_surface_attach (cursor_surface, buffer, 0, 0); + wl_surface_damage (cursor_surface, 0, 0, + image->width, image->height); + wl_surface_set_buffer_scale (cursor_surface, + ceiled_scale); + wl_surface_commit (cursor_surface); + + wl_cursor_theme_destroy (cursor_theme); + + test_driver_sync_point (display->test_driver, 0, NULL); +} + +int +main (int argc, + char **argv) +{ + g_autoptr (WaylandDisplay) display = NULL; + g_autoptr (WaylandSurface) surface = NULL; + + cursor_name = argv[1]; + + display = wayland_display_new (WAYLAND_DISPLAY_CAPABILITY_TEST_DRIVER); + surface = wayland_surface_new (display, + "cursor-tests-surface", + 100, 100, 0xffffffff); + g_signal_connect (surface, "pointer-enter", + G_CALLBACK (on_pointer_enter), NULL); + xdg_toplevel_set_fullscreen (surface->xdg_toplevel, NULL); + wl_surface_commit (surface->wl_surface); + + wait_for_sync_event (display, 0); + + return EXIT_SUCCESS; +} diff --git a/src/tests/wayland-test-clients/meson.build b/src/tests/wayland-test-clients/meson.build index bdea36df3..122912d39 100644 --- a/src/tests/wayland-test-clients/meson.build +++ b/src/tests/wayland-test-clients/meson.build @@ -102,6 +102,12 @@ wayland_test_clients = [ { 'name': 'ycbcr', }, + { + 'name': 'cursor-tests-client', + 'extra_deps': [ + wayland_cursor_dep, + ], + }, ] test_client_executables = {}