diff --git a/clutter/clutter/clutter-backend.c b/clutter/clutter/clutter-backend.c index ace8cd93a..671869b6d 100644 --- a/clutter/clutter/clutter-backend.c +++ b/clutter/clutter/clutter-backend.c @@ -64,6 +64,9 @@ #ifdef CLUTTER_WINDOWING_EGL #include "egl/clutter-backend-eglnative.h" #endif +#ifdef CLUTTER_WINDOWING_WAYLAND_CLIENT +#include "wayland-client/clutter-backend-wayland-client.h" +#endif #ifdef CLUTTER_HAS_WAYLAND_COMPOSITOR_SUPPORT #include @@ -452,6 +455,9 @@ static const struct { #endif #ifdef CLUTTER_WINDOWING_EGL { CLUTTER_WINDOWING_EGL, clutter_backend_egl_native_new }, +#endif +#ifdef CLUTTER_WINDOWING_WAYLAND_CLIENT + { CLUTTER_WINDOWING_WAYLAND_CLIENT, clutter_backend_wayland_client_new }, #endif { NULL, NULL }, }; diff --git a/clutter/clutter/clutter-build-config.h.meson b/clutter/clutter/clutter-build-config.h.meson index 27ac248e8..20b7f6351 100644 --- a/clutter/clutter/clutter-build-config.h.meson +++ b/clutter/clutter/clutter-build-config.h.meson @@ -7,6 +7,9 @@ /* Have evdev support for input handling */ #mesondefine HAVE_EVDEV +/* Have Wayland client backend support */ +#mesondefine HAVE_WAYLAND_CLIENT + /* Building with libwacom for advanced tablet management */ #mesondefine HAVE_LIBWACOM diff --git a/clutter/clutter/cogl/clutter-stage-cogl.c b/clutter/clutter/cogl/clutter-stage-cogl.c index c931855bd..433cf290f 100644 --- a/clutter/clutter/cogl/clutter-stage-cogl.c +++ b/clutter/clutter/cogl/clutter-stage-cogl.c @@ -595,7 +595,7 @@ clutter_stage_cogl_redraw_view (ClutterStageWindow *stage_window, cogl_clutter_winsys_has_feature (COGL_WINSYS_FEATURE_SWAP_REGION); has_buffer_age = - COGL_ONSCREEN (onscreen) && + cogl_is_onscreen (onscreen) && cogl_clutter_winsys_has_feature (COGL_WINSYS_FEATURE_BUFFER_AGE); redraw_clip = clutter_stage_view_take_redraw_clip (view); diff --git a/clutter/clutter/meson.build b/clutter/clutter/meson.build index cc67e6460..275a398a3 100644 --- a/clutter/clutter/meson.build +++ b/clutter/clutter/meson.build @@ -296,6 +296,62 @@ if have_wayland clutter_backend_private_headers += clutter_wayland_private_headers endif +if have_wayland_client + # Generate Wayland client protocols + wayland_scanner = find_program('wayland-scanner') + wayland_protocols_dir = wayland_protocols_dep.get_variable(pkgconfig: 'pkgdatadir') + + # xdg-shell protocol (required by layer-shell) + xdg_shell_xml = join_paths(wayland_protocols_dir, 'stable', 'xdg-shell', 'xdg-shell.xml') + + xdg_shell_client_header = custom_target('xdg-shell-client-protocol.h', + input: xdg_shell_xml, + output: 'xdg-shell-client-protocol.h', + command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'], + ) + + xdg_shell_protocol_code = custom_target('xdg-shell-protocol.c', + input: xdg_shell_xml, + output: 'xdg-shell-protocol.c', + command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'], + ) + + # layer-shell protocol + layer_shell_xml = files('wayland-client/wlr-layer-shell-unstable-v1.xml') + + layer_shell_client_header = custom_target('wlr-layer-shell-client-protocol.h', + input: layer_shell_xml, + output: 'wlr-layer-shell-unstable-v1-client-protocol.h', + command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'], + ) + + layer_shell_protocol_code = custom_target('wlr-layer-shell-protocol.c', + input: layer_shell_xml, + output: 'wlr-layer-shell-unstable-v1-protocol.c', + command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'], + ) + + clutter_wayland_client_sources = [ + 'wayland-client/clutter-backend-wayland-client.c', + 'wayland-client/clutter-stage-wayland-client.c', + 'wayland-client/clutter-seat-wayland-client.c', + 'wayland-client/clutter-keymap-wayland-client.c', + xdg_shell_protocol_code, + layer_shell_protocol_code, + ] + clutter_backend_sources += clutter_wayland_client_sources + + clutter_wayland_client_private_headers = [ + 'wayland-client/clutter-backend-wayland-client.h', + 'wayland-client/clutter-stage-wayland-client.h', + 'wayland-client/clutter-seat-wayland-client.h', + 'wayland-client/clutter-keymap-wayland-client.h', + xdg_shell_client_header, + layer_shell_client_header, + ] + clutter_backend_private_headers += clutter_wayland_client_private_headers +endif + cally_headers = [ 'cally/cally-actor.h', 'cally/cally-clone.h', @@ -334,6 +390,7 @@ cdata = configuration_data() cdata.set_quoted('MUFFIN_VERSION', meson.project_version()) cdata.set('CLUTTER_DRIVERS', '"*"') cdata.set('HAVE_EVDEV', have_native_backend) +cdata.set('HAVE_WAYLAND_CLIENT', have_wayland_client) cdata.set('HAVE_LIBWACOM', have_libwacom) cdata.set('HAVE_PANGO_FT2', have_pango_ft2) @@ -364,6 +421,11 @@ if have_native_backend '#define CLUTTER_INPUT_EVDEV "evdev"', ] endif +if have_wayland_client + clutter_config_defines += [ + '#define CLUTTER_WINDOWING_WAYLAND_CLIENT "wayland-client"', + ] +endif clutter_config_defines += [ '#define CLUTTER_INPUT_NULL "null"', ] diff --git a/clutter/clutter/wayland-client/clutter-backend-wayland-client.c b/clutter/clutter/wayland-client/clutter-backend-wayland-client.c new file mode 100644 index 000000000..b5da051f2 --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-backend-wayland-client.c @@ -0,0 +1,1158 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Copyright (C) 2024 Linux Mint + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Michael Webster + */ + +#include "clutter-build-config.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include "clutter-backend-wayland-client.h" +#include "clutter-stage-wayland-client.h" +#include "clutter-seat-wayland-client.h" +#include "clutter-keymap-wayland-client.h" +#include "clutter-input-device-private.h" +#include "clutter-event-private.h" +#include "wlr-layer-shell-unstable-v1-client-protocol.h" + +#include "clutter-backend.h" +#include "clutter-debug.h" +#include "clutter-event.h" +#include "clutter-input-method.h" +#include "clutter-main.h" +#include "clutter-private.h" +#include "clutter-settings-private.h" +#include "clutter-stage-private.h" + +/* Minimal no-op input method so ClutterText's input focus system works + * without critical warnings. All methods are no-ops; key events pass + * through to ClutterText's normal key handling. */ +#define CLUTTER_TYPE_INPUT_METHOD_WAYLAND_CLIENT (clutter_input_method_wayland_client_get_type ()) +G_DECLARE_FINAL_TYPE (ClutterInputMethodWaylandClient, + clutter_input_method_wayland_client, + CLUTTER, INPUT_METHOD_WAYLAND_CLIENT, + ClutterInputMethod) + +struct _ClutterInputMethodWaylandClient +{ + ClutterInputMethod parent_instance; +}; + +G_DEFINE_TYPE (ClutterInputMethodWaylandClient, + clutter_input_method_wayland_client, + CLUTTER_TYPE_INPUT_METHOD) + +static void +im_wl_focus_in (ClutterInputMethod *im, + ClutterInputFocus *focus) +{ +} + +static void +im_wl_focus_out (ClutterInputMethod *im) +{ +} + +static void +im_wl_reset (ClutterInputMethod *im) +{ +} + +static void +im_wl_set_cursor_location (ClutterInputMethod *im, + const graphene_rect_t *rect) +{ +} + +static void +im_wl_set_surrounding (ClutterInputMethod *im, + const gchar *text, + guint cursor, + guint anchor) +{ +} + +static void +im_wl_update_content_hints (ClutterInputMethod *im, + ClutterInputContentHintFlags hints) +{ +} + +static void +im_wl_update_content_purpose (ClutterInputMethod *im, + ClutterInputContentPurpose purpose) +{ +} + +static gboolean +im_wl_filter_key_event (ClutterInputMethod *im, + const ClutterEvent *key) +{ + return FALSE; +} + +static void +clutter_input_method_wayland_client_class_init (ClutterInputMethodWaylandClientClass *klass) +{ + ClutterInputMethodClass *im_class = CLUTTER_INPUT_METHOD_CLASS (klass); + + im_class->focus_in = im_wl_focus_in; + im_class->focus_out = im_wl_focus_out; + im_class->reset = im_wl_reset; + im_class->set_cursor_location = im_wl_set_cursor_location; + im_class->set_surrounding = im_wl_set_surrounding; + im_class->update_content_hints = im_wl_update_content_hints; + im_class->update_content_purpose = im_wl_update_content_purpose; + im_class->filter_key_event = im_wl_filter_key_event; +} + +static void +clutter_input_method_wayland_client_init (ClutterInputMethodWaylandClient *im) +{ +} + +G_DEFINE_TYPE (ClutterBackendWaylandClient, + clutter_backend_wayland_client, + CLUTTER_TYPE_BACKEND) + +/* Forward declarations */ +static const struct wl_seat_listener seat_listener; +static const struct wl_keyboard_listener keyboard_listener; + +/* Registry handlers */ +static void +registry_global (void *data, + struct wl_registry *registry, + uint32_t name, + const char *interface, + uint32_t version) +{ + ClutterBackendWaylandClient *backend = data; + + CLUTTER_NOTE (BACKEND, "Wayland registry: %s (v%d)", interface, version); + + if (strcmp (interface, wl_compositor_interface.name) == 0) + { + backend->wl_compositor = wl_registry_bind (registry, name, + &wl_compositor_interface, + MIN (version, 4)); + } + else if (strcmp (interface, wl_shm_interface.name) == 0) + { + backend->wl_shm = wl_registry_bind (registry, name, + &wl_shm_interface, + MIN (version, 1)); + } + else if (strcmp (interface, wl_seat_interface.name) == 0) + { + backend->wl_seat = wl_registry_bind (registry, name, + &wl_seat_interface, + MIN (version, 5)); + wl_seat_add_listener (backend->wl_seat, &seat_listener, backend); + } + else if (strcmp (interface, wl_output_interface.name) == 0) + { + if (!backend->wl_output) + { + backend->wl_output = wl_registry_bind (registry, name, + &wl_output_interface, + MIN (version, 2)); + } + } + else if (strcmp (interface, zwlr_layer_shell_v1_interface.name) == 0) + { + backend->layer_shell = wl_registry_bind (registry, name, + &zwlr_layer_shell_v1_interface, + MIN (version, 4)); + } +} + +static void +registry_global_remove (void *data, + struct wl_registry *registry, + uint32_t name) +{ + /* TODO: Handle output removal, etc. */ +} + +static const struct wl_registry_listener registry_listener = { + .global = registry_global, + .global_remove = registry_global_remove, +}; + +/* Surface-to-stage mapping helpers */ +void +clutter_backend_wayland_client_register_surface (ClutterBackendWaylandClient *backend, + struct wl_surface *surface, + gpointer stage) +{ + g_return_if_fail (CLUTTER_IS_BACKEND_WAYLAND_CLIENT (backend)); + g_return_if_fail (surface != NULL); + + if (!backend->surface_to_stage) + backend->surface_to_stage = g_hash_table_new (g_direct_hash, g_direct_equal); + + g_hash_table_insert (backend->surface_to_stage, surface, stage); +} + +void +clutter_backend_wayland_client_unregister_surface (ClutterBackendWaylandClient *backend, + struct wl_surface *surface) +{ + g_return_if_fail (CLUTTER_IS_BACKEND_WAYLAND_CLIENT (backend)); + + if (backend->surface_to_stage && surface) + g_hash_table_remove (backend->surface_to_stage, surface); +} + +static ClutterStageWaylandClient * +find_stage_for_surface (ClutterBackendWaylandClient *backend, + struct wl_surface *surface) +{ + if (!backend->surface_to_stage) + return NULL; + + return g_hash_table_lookup (backend->surface_to_stage, surface); +} + +/* wl_pointer listener */ +static uint32_t +wl_button_to_clutter_button (uint32_t wl_button) +{ + switch (wl_button) + { + case BTN_LEFT: return 1; + case BTN_MIDDLE: return 2; + case BTN_RIGHT: return 3; + default: return wl_button - BTN_LEFT + 1; + } +} + +static void +pointer_handle_enter (void *data, + struct wl_pointer *pointer, + uint32_t serial, + struct wl_surface *surface, + wl_fixed_t sx, + wl_fixed_t sy) +{ + ClutterBackendWaylandClient *backend_wl = data; + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (backend_wl->seat); + ClutterStageWaylandClient *stage_wl; + ClutterStage *stage; + ClutterEvent *event; + float x, y; + + stage_wl = find_stage_for_surface (backend_wl, surface); + if (!stage_wl) + return; + + stage = CLUTTER_STAGE_COGL (stage_wl)->wrapper; + if (!stage) + return; + + x = wl_fixed_to_double (sx); + y = wl_fixed_to_double (sy); + + backend_wl->pointer_focus_surface = surface; + backend_wl->pointer_x = x; + backend_wl->pointer_y = y; + + _clutter_input_device_set_stage (seat_wl->pointer_device, stage); + _clutter_input_device_set_coords (seat_wl->pointer_device, NULL, x, y, stage); + + event = clutter_event_new (CLUTTER_ENTER); + event->crossing.time = g_get_monotonic_time () / 1000; + event->crossing.flags = 0; + event->crossing.stage = stage; + event->crossing.source = CLUTTER_ACTOR (stage); + event->crossing.x = x; + event->crossing.y = y; + event->crossing.related = NULL; + clutter_event_set_device (event, seat_wl->pointer_device); + clutter_event_set_source_device (event, seat_wl->pointer_device); + clutter_do_event (event); + clutter_event_free (event); +} + +static void +pointer_handle_leave (void *data, + struct wl_pointer *pointer, + uint32_t serial, + struct wl_surface *surface) +{ + ClutterBackendWaylandClient *backend_wl = data; + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (backend_wl->seat); + ClutterStageWaylandClient *stage_wl; + ClutterStage *stage; + ClutterEvent *event; + + stage_wl = find_stage_for_surface (backend_wl, surface); + if (!stage_wl) + return; + + stage = CLUTTER_STAGE_COGL (stage_wl)->wrapper; + if (!stage) + return; + + event = clutter_event_new (CLUTTER_LEAVE); + event->crossing.time = g_get_monotonic_time () / 1000; + event->crossing.flags = 0; + event->crossing.stage = stage; + event->crossing.source = CLUTTER_ACTOR (stage); + event->crossing.x = backend_wl->pointer_x; + event->crossing.y = backend_wl->pointer_y; + event->crossing.related = NULL; + clutter_event_set_device (event, seat_wl->pointer_device); + clutter_event_set_source_device (event, seat_wl->pointer_device); + clutter_do_event (event); + clutter_event_free (event); + + backend_wl->pointer_focus_surface = NULL; + _clutter_input_device_set_stage (seat_wl->pointer_device, NULL); +} + +static void +pointer_handle_motion (void *data, + struct wl_pointer *pointer, + uint32_t time, + wl_fixed_t sx, + wl_fixed_t sy) +{ + ClutterBackendWaylandClient *backend_wl = data; + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (backend_wl->seat); + ClutterStageWaylandClient *stage_wl; + ClutterStage *stage; + ClutterEvent *event; + float x, y; + + stage_wl = find_stage_for_surface (backend_wl, backend_wl->pointer_focus_surface); + if (!stage_wl) + return; + + stage = CLUTTER_STAGE_COGL (stage_wl)->wrapper; + if (!stage) + return; + + x = wl_fixed_to_double (sx); + y = wl_fixed_to_double (sy); + + backend_wl->pointer_x = x; + backend_wl->pointer_y = y; + + _clutter_input_device_set_coords (seat_wl->pointer_device, NULL, x, y, stage); + + event = clutter_event_new (CLUTTER_MOTION); + event->motion.time = time; + event->motion.flags = 0; + event->motion.stage = stage; + event->motion.x = x; + event->motion.y = y; + event->motion.modifier_state = backend_wl->modifier_state; + event->motion.axes = NULL; + clutter_event_set_device (event, seat_wl->pointer_device); + clutter_event_set_source_device (event, seat_wl->pointer_device); + clutter_do_event (event); + clutter_event_free (event); +} + +static void +pointer_handle_button (void *data, + struct wl_pointer *pointer, + uint32_t serial, + uint32_t time, + uint32_t button, + uint32_t state) +{ + ClutterBackendWaylandClient *backend_wl = data; + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (backend_wl->seat); + ClutterStageWaylandClient *stage_wl; + ClutterStage *stage; + ClutterEvent *event; + ClutterEventType event_type; + + stage_wl = find_stage_for_surface (backend_wl, backend_wl->pointer_focus_surface); + if (!stage_wl) + return; + + stage = CLUTTER_STAGE_COGL (stage_wl)->wrapper; + if (!stage) + return; + + backend_wl->pointer_button_serial = serial; + + event_type = (state == WL_POINTER_BUTTON_STATE_PRESSED) + ? CLUTTER_BUTTON_PRESS + : CLUTTER_BUTTON_RELEASE; + + event = clutter_event_new (event_type); + event->button.time = time; + event->button.flags = 0; + event->button.stage = stage; + event->button.x = backend_wl->pointer_x; + event->button.y = backend_wl->pointer_y; + event->button.modifier_state = backend_wl->modifier_state; + event->button.button = wl_button_to_clutter_button (button); + event->button.click_count = 1; + event->button.axes = NULL; + clutter_event_set_device (event, seat_wl->pointer_device); + clutter_event_set_source_device (event, seat_wl->pointer_device); + clutter_do_event (event); + clutter_event_free (event); +} + +static void +pointer_handle_axis (void *data, + struct wl_pointer *pointer, + uint32_t time, + uint32_t axis, + wl_fixed_t value) +{ + ClutterBackendWaylandClient *backend_wl = data; + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (backend_wl->seat); + ClutterStageWaylandClient *stage_wl; + ClutterStage *stage; + ClutterEvent *event; + ClutterScrollDirection direction; + double val; + + stage_wl = find_stage_for_surface (backend_wl, backend_wl->pointer_focus_surface); + if (!stage_wl) + return; + + stage = CLUTTER_STAGE_COGL (stage_wl)->wrapper; + if (!stage) + return; + + val = wl_fixed_to_double (value); + + if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) + direction = (val > 0) ? CLUTTER_SCROLL_DOWN : CLUTTER_SCROLL_UP; + else + direction = (val > 0) ? CLUTTER_SCROLL_RIGHT : CLUTTER_SCROLL_LEFT; + + event = clutter_event_new (CLUTTER_SCROLL); + event->scroll.time = time; + event->scroll.flags = 0; + event->scroll.stage = stage; + event->scroll.x = backend_wl->pointer_x; + event->scroll.y = backend_wl->pointer_y; + event->scroll.direction = direction; + event->scroll.modifier_state = backend_wl->modifier_state; + event->scroll.axes = NULL; + event->scroll.scroll_source = CLUTTER_SCROLL_SOURCE_UNKNOWN; + event->scroll.finish_flags = CLUTTER_SCROLL_FINISHED_NONE; + clutter_event_set_device (event, seat_wl->pointer_device); + clutter_event_set_source_device (event, seat_wl->pointer_device); + clutter_do_event (event); + clutter_event_free (event); +} + +static void +pointer_handle_frame (void *data, struct wl_pointer *pointer) +{ +} + +static void +pointer_handle_axis_source (void *data, struct wl_pointer *pointer, uint32_t axis_source) +{ +} + +static void +pointer_handle_axis_stop (void *data, struct wl_pointer *pointer, uint32_t time, uint32_t axis) +{ +} + +static void +pointer_handle_axis_discrete (void *data, struct wl_pointer *pointer, uint32_t axis, int32_t discrete) +{ +} + +static const struct wl_pointer_listener pointer_listener = { + .enter = pointer_handle_enter, + .leave = pointer_handle_leave, + .motion = pointer_handle_motion, + .button = pointer_handle_button, + .axis = pointer_handle_axis, + .frame = pointer_handle_frame, + .axis_source = pointer_handle_axis_source, + .axis_stop = pointer_handle_axis_stop, + .axis_discrete = pointer_handle_axis_discrete, +}; + +/* wl_keyboard listener */ +static void +keyboard_handle_keymap (void *data, + struct wl_keyboard *keyboard, + uint32_t format, + int32_t fd, + uint32_t size) +{ + ClutterBackendWaylandClient *backend_wl = data; + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (backend_wl->seat); + char *map_str; + + if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) + { + close (fd); + return; + } + + map_str = mmap (NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); + if (map_str == MAP_FAILED) + { + close (fd); + return; + } + + if (!backend_wl->xkb_context) + backend_wl->xkb_context = xkb_context_new (XKB_CONTEXT_NO_FLAGS); + + g_clear_pointer (&backend_wl->xkb_keymap, xkb_keymap_unref); + g_clear_pointer (&backend_wl->xkb_state, xkb_state_unref); + + backend_wl->xkb_keymap = xkb_keymap_new_from_string (backend_wl->xkb_context, + map_str, + XKB_KEYMAP_FORMAT_TEXT_V1, + XKB_KEYMAP_COMPILE_NO_FLAGS); + munmap (map_str, size); + close (fd); + + if (!backend_wl->xkb_keymap) + { + g_warning ("Failed to compile XKB keymap"); + return; + } + + backend_wl->xkb_state = xkb_state_new (backend_wl->xkb_keymap); + + if (seat_wl->keymap) + clutter_keymap_wayland_client_set_xkb_state ( + CLUTTER_KEYMAP_WAYLAND_CLIENT (seat_wl->keymap), + backend_wl->xkb_state); + + CLUTTER_NOTE (BACKEND, "Wayland keyboard keymap updated"); +} + +static void +keyboard_handle_enter (void *data, + struct wl_keyboard *keyboard, + uint32_t serial, + struct wl_surface *surface, + struct wl_array *keys) +{ + ClutterBackendWaylandClient *backend_wl = data; + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (backend_wl->seat); + ClutterStageWaylandClient *stage_wl; + ClutterStage *stage; + + stage_wl = find_stage_for_surface (backend_wl, surface); + if (!stage_wl) + return; + + stage = CLUTTER_STAGE_COGL (stage_wl)->wrapper; + if (!stage) + return; + + backend_wl->keyboard_focus_surface = surface; + _clutter_input_device_set_stage (seat_wl->keyboard_device, stage); + + CLUTTER_NOTE (BACKEND, "Keyboard focus entered surface"); +} + +static void +keyboard_handle_leave (void *data, + struct wl_keyboard *keyboard, + uint32_t serial, + struct wl_surface *surface) +{ + ClutterBackendWaylandClient *backend_wl = data; + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (backend_wl->seat); + + backend_wl->keyboard_focus_surface = NULL; + _clutter_input_device_set_stage (seat_wl->keyboard_device, NULL); + + CLUTTER_NOTE (BACKEND, "Keyboard focus left surface"); +} + +static void +keyboard_handle_key (void *data, + struct wl_keyboard *keyboard, + uint32_t serial, + uint32_t time, + uint32_t key, + uint32_t state) +{ + ClutterBackendWaylandClient *backend_wl = data; + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (backend_wl->seat); + ClutterStageWaylandClient *stage_wl; + ClutterStage *stage; + ClutterEvent *event; + xkb_keysym_t sym; + const xkb_keysym_t *syms; + xkb_keycode_t xkb_key; + char buffer[8]; + int n; + + if (!backend_wl->xkb_state) + return; + + stage_wl = find_stage_for_surface (backend_wl, backend_wl->keyboard_focus_surface); + if (!stage_wl) + return; + + stage = CLUTTER_STAGE_COGL (stage_wl)->wrapper; + if (!stage) + return; + + /* Wayland sends evdev keycodes (base 0), XKB expects base 8 */ + xkb_key = key + 8; + + n = xkb_key_get_syms (backend_wl->xkb_state, xkb_key, &syms); + if (n == 1) + sym = syms[0]; + else + sym = XKB_KEY_NoSymbol; + + if (state) + event = clutter_event_new (CLUTTER_KEY_PRESS); + else + event = clutter_event_new (CLUTTER_KEY_RELEASE); + + event->key.stage = stage; + event->key.time = time; + event->key.hardware_keycode = xkb_key; + event->key.keyval = sym; + + _clutter_event_set_state_full (event, + 0, + xkb_state_serialize_mods (backend_wl->xkb_state, + XKB_STATE_MODS_DEPRESSED), + xkb_state_serialize_mods (backend_wl->xkb_state, + XKB_STATE_MODS_LATCHED), + xkb_state_serialize_mods (backend_wl->xkb_state, + XKB_STATE_MODS_LOCKED), + xkb_state_serialize_mods (backend_wl->xkb_state, + XKB_STATE_MODS_EFFECTIVE)); + + event->key.modifier_state = xkb_state_serialize_mods (backend_wl->xkb_state, + XKB_STATE_MODS_EFFECTIVE); + + clutter_event_set_device (event, seat_wl->keyboard_device); + clutter_event_set_source_device (event, seat_wl->keyboard_device); + + n = xkb_keysym_to_utf8 (sym, buffer, sizeof (buffer)); + if (n == 0) + { + event->key.unicode_value = (gunichar) '\0'; + } + else + { + event->key.unicode_value = g_utf8_get_char_validated (buffer, n); + if (event->key.unicode_value == (gunichar) -1 || + event->key.unicode_value == (gunichar) -2) + event->key.unicode_value = (gunichar) '\0'; + } + + /* Update xkb_state for the key press/release so that subsequent + * modifier queries are accurate (this matters for key sequences + * like Shift+a). The compositor also sends explicit modifier + * updates via the modifiers callback, but updating here keeps + * state consistent between key and modifier events. */ + xkb_state_update_key (backend_wl->xkb_state, xkb_key, + state ? XKB_KEY_DOWN : XKB_KEY_UP); + + backend_wl->modifier_state = xkb_state_serialize_mods (backend_wl->xkb_state, + XKB_STATE_MODS_EFFECTIVE); + + clutter_do_event (event); + clutter_event_free (event); +} + +static void +keyboard_handle_modifiers (void *data, + struct wl_keyboard *keyboard, + uint32_t serial, + uint32_t mods_depressed, + uint32_t mods_latched, + uint32_t mods_locked, + uint32_t group) +{ + ClutterBackendWaylandClient *backend_wl = data; + + if (!backend_wl->xkb_state) + return; + + xkb_state_update_mask (backend_wl->xkb_state, + mods_depressed, mods_latched, mods_locked, + 0, 0, group); + + backend_wl->modifier_state = xkb_state_serialize_mods (backend_wl->xkb_state, + XKB_STATE_MODS_EFFECTIVE); +} + +static void +keyboard_handle_repeat_info (void *data, + struct wl_keyboard *keyboard, + int32_t rate, + int32_t delay) +{ + CLUTTER_NOTE (BACKEND, "Keyboard repeat: rate=%d delay=%d", rate, delay); +} + +static const struct wl_keyboard_listener keyboard_listener = { + .keymap = keyboard_handle_keymap, + .enter = keyboard_handle_enter, + .leave = keyboard_handle_leave, + .key = keyboard_handle_key, + .modifiers = keyboard_handle_modifiers, + .repeat_info = keyboard_handle_repeat_info, +}; + +/* wl_seat listener */ +static void +seat_handle_capabilities (void *data, + struct wl_seat *seat, + uint32_t capabilities) +{ + ClutterBackendWaylandClient *backend_wl = data; + + if ((capabilities & WL_SEAT_CAPABILITY_POINTER) && !backend_wl->wl_pointer) + { + backend_wl->wl_pointer = wl_seat_get_pointer (backend_wl->wl_seat); + wl_pointer_add_listener (backend_wl->wl_pointer, &pointer_listener, backend_wl); + CLUTTER_NOTE (BACKEND, "Wayland pointer acquired"); + } + else if (!(capabilities & WL_SEAT_CAPABILITY_POINTER) && backend_wl->wl_pointer) + { + wl_pointer_destroy (backend_wl->wl_pointer); + backend_wl->wl_pointer = NULL; + CLUTTER_NOTE (BACKEND, "Wayland pointer lost"); + } + + if ((capabilities & WL_SEAT_CAPABILITY_KEYBOARD) && !backend_wl->wl_keyboard) + { + backend_wl->wl_keyboard = wl_seat_get_keyboard (backend_wl->wl_seat); + wl_keyboard_add_listener (backend_wl->wl_keyboard, &keyboard_listener, backend_wl); + CLUTTER_NOTE (BACKEND, "Wayland keyboard acquired"); + } + else if (!(capabilities & WL_SEAT_CAPABILITY_KEYBOARD) && backend_wl->wl_keyboard) + { + wl_keyboard_destroy (backend_wl->wl_keyboard); + backend_wl->wl_keyboard = NULL; + CLUTTER_NOTE (BACKEND, "Wayland keyboard lost"); + } +} + +static void +seat_handle_name (void *data, struct wl_seat *seat, const char *name) +{ + CLUTTER_NOTE (BACKEND, "Wayland seat name: %s", name); +} + +static const struct wl_seat_listener seat_listener = { + .capabilities = seat_handle_capabilities, + .name = seat_handle_name, +}; + +/* GSource for Wayland display events */ +typedef struct { + GSource source; + ClutterBackendWaylandClient *backend; + GPollFD pfd; +} WaylandEventSource; + +static gboolean +wayland_event_source_prepare (GSource *source, + gint *timeout) +{ + WaylandEventSource *wl_source = (WaylandEventSource *) source; + struct wl_display *display = wl_source->backend->wl_display; + + *timeout = -1; + + if (!display) + return FALSE; + + /* Dispatch any events already in the default queue but not yet + * dispatched. This is critical: eglSwapBuffers (called during redraw) + * internally reads from the Wayland fd to process buffer release + * events on Cogl's private event queue. That read also pulls in our + * input events, placing them in the default queue without dispatching + * them. Without this call those events would sit undispatched until + * new data arrives on the fd. */ + if (wl_display_prepare_read (display) != 0) + { + wl_display_dispatch_pending (display); + wl_display_flush (display); + return FALSE; + } + + wl_display_flush (display); + wl_display_cancel_read (display); + + return FALSE; +} + +static gboolean +wayland_event_source_check (GSource *source) +{ + WaylandEventSource *wl_source = (WaylandEventSource *) source; + + return (wl_source->pfd.revents & G_IO_IN) != 0; +} + +static gboolean +wayland_event_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + WaylandEventSource *wl_source = (WaylandEventSource *) source; + struct wl_display *display = wl_source->backend->wl_display; + + if (wl_source->pfd.revents & (G_IO_ERR | G_IO_HUP)) + { + g_warning ("Wayland display error"); + return G_SOURCE_REMOVE; + } + + if (wl_source->pfd.revents & G_IO_IN) + { + if (wl_display_dispatch (display) == -1) + { + g_warning ("Wayland display dispatch failed: %s", strerror (errno)); + return G_SOURCE_REMOVE; + } + } + + return G_SOURCE_CONTINUE; +} + +static void +wayland_event_source_finalize (GSource *source) +{ +} + +static GSourceFuncs wayland_event_source_funcs = { + .prepare = wayland_event_source_prepare, + .check = wayland_event_source_check, + .dispatch = wayland_event_source_dispatch, + .finalize = wayland_event_source_finalize, +}; + +static GSource * +wayland_event_source_new (ClutterBackendWaylandClient *backend) +{ + GSource *source; + WaylandEventSource *wl_source; + + source = g_source_new (&wayland_event_source_funcs, sizeof (WaylandEventSource)); + g_source_set_name (source, "Wayland Event Source"); + + wl_source = (WaylandEventSource *) source; + wl_source->backend = backend; + wl_source->pfd.fd = wl_display_get_fd (backend->wl_display); + wl_source->pfd.events = G_IO_IN | G_IO_ERR | G_IO_HUP; + + g_source_add_poll (source, &wl_source->pfd); + + return source; +} + +/* Backend vfuncs */ +static gboolean +clutter_backend_wayland_client_post_parse (ClutterBackend *backend, + GError **error) +{ + ClutterBackendWaylandClient *backend_wl = CLUTTER_BACKEND_WAYLAND_CLIENT (backend); + const char *display_name; + + display_name = g_getenv ("WAYLAND_DISPLAY"); + if (!display_name) + display_name = "wayland-0"; + + CLUTTER_NOTE (BACKEND, "Connecting to Wayland display '%s'", display_name); + + backend_wl->wl_display = wl_display_connect (display_name); + if (!backend_wl->wl_display) + { + g_set_error (error, CLUTTER_INIT_ERROR, + CLUTTER_INIT_ERROR_BACKEND, + "Failed to connect to Wayland display '%s': %s", + display_name, strerror (errno)); + return FALSE; + } + + /* Get registry and bind globals */ + backend_wl->wl_registry = wl_display_get_registry (backend_wl->wl_display); + wl_registry_add_listener (backend_wl->wl_registry, ®istry_listener, backend_wl); + wl_display_roundtrip (backend_wl->wl_display); + + /* Check required interfaces */ + if (!backend_wl->wl_compositor) + { + g_set_error_literal (error, CLUTTER_INIT_ERROR, + CLUTTER_INIT_ERROR_BACKEND, + "wl_compositor not available from Wayland compositor"); + return FALSE; + } + + if (!backend_wl->layer_shell) + { + g_set_error_literal (error, CLUTTER_INIT_ERROR, + CLUTTER_INIT_ERROR_BACKEND, + "zwlr_layer_shell_v1 not available from Wayland compositor"); + return FALSE; + } + + CLUTTER_NOTE (BACKEND, "Connected to Wayland display, protocols bound"); + + /* Create event source for Wayland events */ + backend_wl->wayland_source = wayland_event_source_new (backend_wl); + g_source_attach (backend_wl->wayland_source, NULL); + + return TRUE; +} + +static CoglRenderer * +clutter_backend_wayland_client_get_renderer (ClutterBackend *backend, + GError **error) +{ + ClutterBackendWaylandClient *backend_wl = CLUTTER_BACKEND_WAYLAND_CLIENT (backend); + CoglRenderer *renderer; + + CLUTTER_NOTE (BACKEND, "Creating Cogl renderer for Wayland EGL"); + + renderer = cogl_renderer_new (); + + cogl_renderer_set_winsys_id (renderer, COGL_WINSYS_ID_EGL_WAYLAND); + cogl_wayland_renderer_set_foreign_display (renderer, backend_wl->wl_display); + + if (!cogl_renderer_connect (renderer, error)) + { + cogl_object_unref (renderer); + return NULL; + } + + return renderer; +} + +static CoglDisplay * +clutter_backend_wayland_client_get_display (ClutterBackend *backend, + CoglRenderer *renderer, + CoglSwapChain *swap_chain, + GError **error) +{ + CoglOnscreenTemplate *onscreen_template; + CoglDisplay *display; + + CLUTTER_NOTE (BACKEND, "Creating CoglDisplay for Wayland"); + + onscreen_template = cogl_onscreen_template_new (swap_chain); + cogl_swap_chain_set_has_alpha (swap_chain, TRUE); + + if (!cogl_renderer_check_onscreen_template (renderer, onscreen_template, error)) + { + cogl_object_unref (onscreen_template); + return NULL; + } + + display = cogl_display_new (renderer, onscreen_template); + cogl_object_unref (onscreen_template); + + return display; +} + +static ClutterStageWindow * +clutter_backend_wayland_client_create_stage (ClutterBackend *backend, + ClutterStage *wrapper, + GError **error) +{ + CLUTTER_NOTE (BACKEND, "Creating Wayland client stage"); + + return g_object_new (CLUTTER_TYPE_STAGE_WAYLAND_CLIENT, + "wrapper", wrapper, + "backend", backend, + NULL); +} + +static ClutterFeatureFlags +clutter_backend_wayland_client_get_features (ClutterBackend *backend) +{ + ClutterFeatureFlags flags; + + flags = CLUTTER_BACKEND_CLASS (clutter_backend_wayland_client_parent_class)->get_features (backend); + flags |= CLUTTER_FEATURE_STAGE_MULTIPLE; + + return flags; +} + +static void +clutter_backend_wayland_client_init_events (ClutterBackend *backend) +{ + ClutterBackendWaylandClient *backend_wl = CLUTTER_BACKEND_WAYLAND_CLIENT (backend); + ClutterInputMethod *im; + + CLUTTER_NOTE (BACKEND, "Initializing Wayland input events"); + + backend_wl->seat = clutter_seat_wayland_client_new (CLUTTER_BACKEND (backend_wl)); + + im = g_object_new (CLUTTER_TYPE_INPUT_METHOD_WAYLAND_CLIENT, NULL); + clutter_backend_set_input_method (backend, im); + g_object_unref (im); +} + +static ClutterSeat * +clutter_backend_wayland_client_get_default_seat (ClutterBackend *backend) +{ + ClutterBackendWaylandClient *backend_wl = CLUTTER_BACKEND_WAYLAND_CLIENT (backend); + + return backend_wl->seat; +} + +static void +clutter_backend_wayland_client_dispose (GObject *object) +{ + ClutterBackendWaylandClient *backend_wl = CLUTTER_BACKEND_WAYLAND_CLIENT (object); + + g_clear_object (&backend_wl->seat); + g_clear_object (&backend_wl->xsettings); + + G_OBJECT_CLASS (clutter_backend_wayland_client_parent_class)->dispose (object); +} + +static void +clutter_backend_wayland_client_finalize (GObject *object) +{ + ClutterBackendWaylandClient *backend_wl = CLUTTER_BACKEND_WAYLAND_CLIENT (object); + + if (backend_wl->wayland_source) + { + g_source_destroy (backend_wl->wayland_source); + g_source_unref (backend_wl->wayland_source); + } + + g_clear_pointer (&backend_wl->surface_to_stage, g_hash_table_destroy); + + g_clear_pointer (&backend_wl->xkb_state, xkb_state_unref); + g_clear_pointer (&backend_wl->xkb_keymap, xkb_keymap_unref); + g_clear_pointer (&backend_wl->xkb_context, xkb_context_unref); + + if (backend_wl->layer_shell) + zwlr_layer_shell_v1_destroy (backend_wl->layer_shell); + + if (backend_wl->wl_pointer) + wl_pointer_destroy (backend_wl->wl_pointer); + + if (backend_wl->wl_keyboard) + wl_keyboard_destroy (backend_wl->wl_keyboard); + + if (backend_wl->wl_seat) + wl_seat_destroy (backend_wl->wl_seat); + + if (backend_wl->wl_output) + wl_output_destroy (backend_wl->wl_output); + + if (backend_wl->wl_shm) + wl_shm_destroy (backend_wl->wl_shm); + + if (backend_wl->wl_compositor) + wl_compositor_destroy (backend_wl->wl_compositor); + + if (backend_wl->wl_registry) + wl_registry_destroy (backend_wl->wl_registry); + + if (backend_wl->wl_display) + wl_display_disconnect (backend_wl->wl_display); + + G_OBJECT_CLASS (clutter_backend_wayland_client_parent_class)->finalize (object); +} + +static void +clutter_backend_wayland_client_class_init (ClutterBackendWaylandClientClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ClutterBackendClass *backend_class = CLUTTER_BACKEND_CLASS (klass); + + object_class->dispose = clutter_backend_wayland_client_dispose; + object_class->finalize = clutter_backend_wayland_client_finalize; + + backend_class->post_parse = clutter_backend_wayland_client_post_parse; + backend_class->get_renderer = clutter_backend_wayland_client_get_renderer; + backend_class->get_display = clutter_backend_wayland_client_get_display; + backend_class->create_stage = clutter_backend_wayland_client_create_stage; + backend_class->get_features = clutter_backend_wayland_client_get_features; + backend_class->init_events = clutter_backend_wayland_client_init_events; + backend_class->get_default_seat = clutter_backend_wayland_client_get_default_seat; +} + +static void +clutter_backend_wayland_client_init (ClutterBackendWaylandClient *backend) +{ +} + +ClutterBackend * +clutter_backend_wayland_client_new (void) +{ + return g_object_new (CLUTTER_TYPE_BACKEND_WAYLAND_CLIENT, NULL); +} + +/* Accessors */ +struct wl_display * +clutter_backend_wayland_client_get_wl_display (ClutterBackendWaylandClient *backend) +{ + g_return_val_if_fail (CLUTTER_IS_BACKEND_WAYLAND_CLIENT (backend), NULL); + return backend->wl_display; +} + +struct wl_compositor * +clutter_backend_wayland_client_get_compositor (ClutterBackendWaylandClient *backend) +{ + g_return_val_if_fail (CLUTTER_IS_BACKEND_WAYLAND_CLIENT (backend), NULL); + return backend->wl_compositor; +} + +struct zwlr_layer_shell_v1 * +clutter_backend_wayland_client_get_layer_shell (ClutterBackendWaylandClient *backend) +{ + g_return_val_if_fail (CLUTTER_IS_BACKEND_WAYLAND_CLIENT (backend), NULL); + return backend->layer_shell; +} + +struct wl_output * +clutter_backend_wayland_client_get_output (ClutterBackendWaylandClient *backend) +{ + g_return_val_if_fail (CLUTTER_IS_BACKEND_WAYLAND_CLIENT (backend), NULL); + return backend->wl_output; +} diff --git a/clutter/clutter/wayland-client/clutter-backend-wayland-client.h b/clutter/clutter/wayland-client/clutter-backend-wayland-client.h new file mode 100644 index 000000000..bf363a42b --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-backend-wayland-client.h @@ -0,0 +1,118 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Copyright (C) 2024 Linux Mint + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Michael Webster + */ + +#ifndef __CLUTTER_BACKEND_WAYLAND_CLIENT_H__ +#define __CLUTTER_BACKEND_WAYLAND_CLIENT_H__ + +#include +#include +#include +#include + +#include +#include "clutter-backend-private.h" + +G_BEGIN_DECLS + +#define CLUTTER_TYPE_BACKEND_WAYLAND_CLIENT (clutter_backend_wayland_client_get_type ()) +#define CLUTTER_BACKEND_WAYLAND_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CLUTTER_TYPE_BACKEND_WAYLAND_CLIENT, ClutterBackendWaylandClient)) +#define CLUTTER_IS_BACKEND_WAYLAND_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CLUTTER_TYPE_BACKEND_WAYLAND_CLIENT)) +#define CLUTTER_BACKEND_WAYLAND_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CLUTTER_TYPE_BACKEND_WAYLAND_CLIENT, ClutterBackendWaylandClientClass)) +#define CLUTTER_IS_BACKEND_WAYLAND_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CLUTTER_TYPE_BACKEND_WAYLAND_CLIENT)) +#define CLUTTER_BACKEND_WAYLAND_CLIENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CLUTTER_TYPE_BACKEND_WAYLAND_CLIENT, ClutterBackendWaylandClientClass)) + +typedef struct _ClutterBackendWaylandClient ClutterBackendWaylandClient; +typedef struct _ClutterBackendWaylandClientClass ClutterBackendWaylandClientClass; + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ClutterBackendWaylandClient, g_object_unref) + +struct _ClutterBackendWaylandClient +{ + ClutterBackend parent_instance; + + /* Wayland connection */ + struct wl_display *wl_display; + struct wl_registry *wl_registry; + struct wl_compositor *wl_compositor; + struct wl_shm *wl_shm; + struct wl_seat *wl_seat; + struct wl_output *wl_output; + + /* Layer shell protocol */ + struct zwlr_layer_shell_v1 *layer_shell; + + /* Event source for Wayland display */ + GSource *wayland_source; + + /* Input seat */ + ClutterSeat *seat; + + /* Wayland input devices */ + struct wl_pointer *wl_pointer; + struct wl_keyboard *wl_keyboard; + + /* Pointer state tracking */ + struct wl_surface *pointer_focus_surface; + double pointer_x; + double pointer_y; + uint32_t pointer_button_serial; + ClutterModifierType modifier_state; + + /* Keyboard state tracking */ + struct wl_surface *keyboard_focus_surface; + struct xkb_context *xkb_context; + struct xkb_keymap *xkb_keymap; + struct xkb_state *xkb_state; + + /* Surface-to-stage mapping */ + GHashTable *surface_to_stage; + + /* Settings (font options, etc.) */ + GSettings *xsettings; +}; + +struct _ClutterBackendWaylandClientClass +{ + ClutterBackendClass parent_class; +}; + +GType clutter_backend_wayland_client_get_type (void) G_GNUC_CONST; + +ClutterBackend * clutter_backend_wayland_client_new (void); + +/* Accessors for Wayland objects (for use by stage) */ +struct wl_display * clutter_backend_wayland_client_get_wl_display (ClutterBackendWaylandClient *backend); +struct wl_compositor * clutter_backend_wayland_client_get_compositor (ClutterBackendWaylandClient *backend); +struct zwlr_layer_shell_v1 * clutter_backend_wayland_client_get_layer_shell (ClutterBackendWaylandClient *backend); +struct wl_output * clutter_backend_wayland_client_get_output (ClutterBackendWaylandClient *backend); + +void clutter_backend_wayland_client_register_surface (ClutterBackendWaylandClient *backend, + struct wl_surface *surface, + gpointer stage); +void clutter_backend_wayland_client_unregister_surface (ClutterBackendWaylandClient *backend, + struct wl_surface *surface); + +G_END_DECLS + +#endif /* __CLUTTER_BACKEND_WAYLAND_CLIENT_H__ */ diff --git a/clutter/clutter/wayland-client/clutter-keymap-wayland-client.c b/clutter/clutter/wayland-client/clutter-keymap-wayland-client.c new file mode 100644 index 000000000..537ed97f9 --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-keymap-wayland-client.c @@ -0,0 +1,93 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Copyright (C) 2024 Linux Mint + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Michael Webster + */ + +#include "clutter-build-config.h" + +#include "clutter-keymap-wayland-client.h" + +G_DEFINE_TYPE (ClutterKeymapWaylandClient, + clutter_keymap_wayland_client, + CLUTTER_TYPE_KEYMAP) + +static gboolean +clutter_keymap_wayland_client_get_num_lock_state (ClutterKeymap *keymap) +{ + ClutterKeymapWaylandClient *keymap_wl = CLUTTER_KEYMAP_WAYLAND_CLIENT (keymap); + + if (keymap_wl->xkb_state) + return xkb_state_mod_name_is_active (keymap_wl->xkb_state, + XKB_MOD_NAME_NUM, + XKB_STATE_MODS_LOCKED); + + return FALSE; +} + +static gboolean +clutter_keymap_wayland_client_get_caps_lock_state (ClutterKeymap *keymap) +{ + ClutterKeymapWaylandClient *keymap_wl = CLUTTER_KEYMAP_WAYLAND_CLIENT (keymap); + + if (keymap_wl->xkb_state) + return xkb_state_mod_name_is_active (keymap_wl->xkb_state, + XKB_MOD_NAME_CAPS, + XKB_STATE_MODS_LOCKED); + + return FALSE; +} + +static PangoDirection +clutter_keymap_wayland_client_get_direction (ClutterKeymap *keymap) +{ + return PANGO_DIRECTION_LTR; +} + +static void +clutter_keymap_wayland_client_class_init (ClutterKeymapWaylandClientClass *klass) +{ + ClutterKeymapClass *keymap_class = CLUTTER_KEYMAP_CLASS (klass); + + keymap_class->get_num_lock_state = clutter_keymap_wayland_client_get_num_lock_state; + keymap_class->get_caps_lock_state = clutter_keymap_wayland_client_get_caps_lock_state; + keymap_class->get_direction = clutter_keymap_wayland_client_get_direction; +} + +static void +clutter_keymap_wayland_client_init (ClutterKeymapWaylandClient *keymap) +{ +} + +ClutterKeymap * +clutter_keymap_wayland_client_new (void) +{ + return g_object_new (CLUTTER_TYPE_KEYMAP_WAYLAND_CLIENT, NULL); +} + +void +clutter_keymap_wayland_client_set_xkb_state (ClutterKeymapWaylandClient *keymap, + struct xkb_state *xkb_state) +{ + g_return_if_fail (CLUTTER_IS_KEYMAP_WAYLAND_CLIENT (keymap)); + + keymap->xkb_state = xkb_state; +} diff --git a/clutter/clutter/wayland-client/clutter-keymap-wayland-client.h b/clutter/clutter/wayland-client/clutter-keymap-wayland-client.h new file mode 100644 index 000000000..78245b221 --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-keymap-wayland-client.h @@ -0,0 +1,65 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Copyright (C) 2024 Linux Mint + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Michael Webster + */ + +#ifndef __CLUTTER_KEYMAP_WAYLAND_CLIENT_H__ +#define __CLUTTER_KEYMAP_WAYLAND_CLIENT_H__ + +#include +#include +#include + +G_BEGIN_DECLS + +#define CLUTTER_TYPE_KEYMAP_WAYLAND_CLIENT (clutter_keymap_wayland_client_get_type ()) +#define CLUTTER_KEYMAP_WAYLAND_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CLUTTER_TYPE_KEYMAP_WAYLAND_CLIENT, ClutterKeymapWaylandClient)) +#define CLUTTER_IS_KEYMAP_WAYLAND_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CLUTTER_TYPE_KEYMAP_WAYLAND_CLIENT)) +#define CLUTTER_KEYMAP_WAYLAND_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CLUTTER_TYPE_KEYMAP_WAYLAND_CLIENT, ClutterKeymapWaylandClientClass)) +#define CLUTTER_IS_KEYMAP_WAYLAND_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CLUTTER_TYPE_KEYMAP_WAYLAND_CLIENT)) +#define CLUTTER_KEYMAP_WAYLAND_CLIENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CLUTTER_TYPE_KEYMAP_WAYLAND_CLIENT, ClutterKeymapWaylandClientClass)) + +typedef struct _ClutterKeymapWaylandClient ClutterKeymapWaylandClient; +typedef struct _ClutterKeymapWaylandClientClass ClutterKeymapWaylandClientClass; + +struct _ClutterKeymapWaylandClient +{ + ClutterKeymap parent_instance; + + struct xkb_state *xkb_state; +}; + +struct _ClutterKeymapWaylandClientClass +{ + ClutterKeymapClass parent_class; +}; + +GType clutter_keymap_wayland_client_get_type (void) G_GNUC_CONST; + +ClutterKeymap * clutter_keymap_wayland_client_new (void); + +void clutter_keymap_wayland_client_set_xkb_state (ClutterKeymapWaylandClient *keymap, + struct xkb_state *xkb_state); + +G_END_DECLS + +#endif /* __CLUTTER_KEYMAP_WAYLAND_CLIENT_H__ */ diff --git a/clutter/clutter/wayland-client/clutter-seat-wayland-client.c b/clutter/clutter/wayland-client/clutter-seat-wayland-client.c new file mode 100644 index 000000000..3d1981c31 --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-seat-wayland-client.c @@ -0,0 +1,211 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Copyright (C) 2024 Linux Mint + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Michael Webster + */ + +#include "clutter-build-config.h" + +#include "clutter-seat-wayland-client.h" +#include "clutter-keymap-wayland-client.h" +#include "clutter-input-device-private.h" + +G_DEFINE_TYPE (ClutterSeatWaylandClient, + clutter_seat_wayland_client, + CLUTTER_TYPE_SEAT) + +static ClutterInputDevice * +clutter_seat_wayland_client_get_pointer (ClutterSeat *seat) +{ + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (seat); + return seat_wl->pointer_device; +} + +static ClutterInputDevice * +clutter_seat_wayland_client_get_keyboard (ClutterSeat *seat) +{ + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (seat); + return seat_wl->keyboard_device; +} + +static GList * +clutter_seat_wayland_client_list_devices (ClutterSeat *seat) +{ + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (seat); + GList *devices = NULL; + + if (seat_wl->pointer_device) + devices = g_list_prepend (devices, seat_wl->pointer_device); + if (seat_wl->keyboard_device) + devices = g_list_prepend (devices, seat_wl->keyboard_device); + + return devices; +} + +static void +clutter_seat_wayland_client_bell_notify (ClutterSeat *seat) +{ + /* No bell support */ +} + +static ClutterKeymap * +clutter_seat_wayland_client_get_keymap (ClutterSeat *seat) +{ + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (seat); + + if (!seat_wl->keymap) + seat_wl->keymap = clutter_keymap_wayland_client_new (); + + return seat_wl->keymap; +} + +static void +clutter_seat_wayland_client_compress_motion (ClutterSeat *seat, + ClutterEvent *event, + const ClutterEvent *to_discard) +{ + /* No motion compression */ +} + +static gboolean +clutter_seat_wayland_client_handle_device_event (ClutterSeat *seat, + ClutterEvent *event) +{ + return FALSE; +} + +static void +clutter_seat_wayland_client_warp_pointer (ClutterSeat *seat, + int x, + int y) +{ + /* Cannot warp pointer as Wayland client */ +} + +static void +clutter_seat_wayland_client_copy_event_data (ClutterSeat *seat, + const ClutterEvent *src, + ClutterEvent *dest) +{ + /* No special event data to copy */ +} + +static void +clutter_seat_wayland_client_free_event_data (ClutterSeat *seat, + ClutterEvent *event) +{ + /* No special event data to free */ +} + +static void +clutter_seat_wayland_client_apply_kbd_a11y_settings (ClutterSeat *seat, + ClutterKbdA11ySettings *settings) +{ + /* No keyboard a11y support yet */ +} + +static ClutterVirtualInputDevice * +clutter_seat_wayland_client_create_virtual_device (ClutterSeat *seat, + ClutterInputDeviceType device_type) +{ + /* No virtual input device support */ + return NULL; +} + +static ClutterVirtualDeviceType +clutter_seat_wayland_client_get_supported_virtual_device_types (ClutterSeat *seat) +{ + return CLUTTER_VIRTUAL_DEVICE_TYPE_NONE; +} + +static void +clutter_seat_wayland_client_finalize (GObject *object) +{ + ClutterSeatWaylandClient *seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (object); + + g_clear_object (&seat_wl->pointer_device); + g_clear_object (&seat_wl->keyboard_device); + g_clear_object (&seat_wl->keymap); + + G_OBJECT_CLASS (clutter_seat_wayland_client_parent_class)->finalize (object); +} + +static void +clutter_seat_wayland_client_class_init (ClutterSeatWaylandClientClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ClutterSeatClass *seat_class = CLUTTER_SEAT_CLASS (klass); + + object_class->finalize = clutter_seat_wayland_client_finalize; + + seat_class->get_pointer = clutter_seat_wayland_client_get_pointer; + seat_class->get_keyboard = clutter_seat_wayland_client_get_keyboard; + seat_class->list_devices = clutter_seat_wayland_client_list_devices; + seat_class->bell_notify = clutter_seat_wayland_client_bell_notify; + seat_class->get_keymap = clutter_seat_wayland_client_get_keymap; + seat_class->compress_motion = clutter_seat_wayland_client_compress_motion; + seat_class->handle_device_event = clutter_seat_wayland_client_handle_device_event; + seat_class->warp_pointer = clutter_seat_wayland_client_warp_pointer; + seat_class->copy_event_data = clutter_seat_wayland_client_copy_event_data; + seat_class->free_event_data = clutter_seat_wayland_client_free_event_data; + seat_class->apply_kbd_a11y_settings = clutter_seat_wayland_client_apply_kbd_a11y_settings; + seat_class->create_virtual_device = clutter_seat_wayland_client_create_virtual_device; + seat_class->get_supported_virtual_device_types = clutter_seat_wayland_client_get_supported_virtual_device_types; +} + +static void +clutter_seat_wayland_client_init (ClutterSeatWaylandClient *seat) +{ +} + +ClutterSeat * +clutter_seat_wayland_client_new (ClutterBackend *backend) +{ + ClutterSeat *seat; + ClutterSeatWaylandClient *seat_wl; + + seat = g_object_new (CLUTTER_TYPE_SEAT_WAYLAND_CLIENT, NULL); + seat_wl = CLUTTER_SEAT_WAYLAND_CLIENT (seat); + + seat_wl->backend = backend; + + seat_wl->pointer_device = g_object_new (CLUTTER_TYPE_INPUT_DEVICE, + "name", "Wayland Pointer", + "device-type", CLUTTER_POINTER_DEVICE, + "device-mode", CLUTTER_INPUT_MODE_MASTER, + "has-cursor", TRUE, + "seat", seat, + "backend", backend, + "enabled", TRUE, + NULL); + + seat_wl->keyboard_device = g_object_new (CLUTTER_TYPE_INPUT_DEVICE, + "name", "Wayland Keyboard", + "device-type", CLUTTER_KEYBOARD_DEVICE, + "device-mode", CLUTTER_INPUT_MODE_MASTER, + "has-cursor", FALSE, + "seat", seat, + "backend", backend, + "enabled", TRUE, + NULL); + + return seat; +} diff --git a/clutter/clutter/wayland-client/clutter-seat-wayland-client.h b/clutter/clutter/wayland-client/clutter-seat-wayland-client.h new file mode 100644 index 000000000..62f09ad1c --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-seat-wayland-client.h @@ -0,0 +1,65 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Copyright (C) 2024 Linux Mint + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Michael Webster + */ + +#ifndef __CLUTTER_SEAT_WAYLAND_CLIENT_H__ +#define __CLUTTER_SEAT_WAYLAND_CLIENT_H__ + +#include +#include +#include + +G_BEGIN_DECLS + +#define CLUTTER_TYPE_SEAT_WAYLAND_CLIENT (clutter_seat_wayland_client_get_type ()) +#define CLUTTER_SEAT_WAYLAND_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CLUTTER_TYPE_SEAT_WAYLAND_CLIENT, ClutterSeatWaylandClient)) +#define CLUTTER_IS_SEAT_WAYLAND_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CLUTTER_TYPE_SEAT_WAYLAND_CLIENT)) +#define CLUTTER_SEAT_WAYLAND_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CLUTTER_TYPE_SEAT_WAYLAND_CLIENT, ClutterSeatWaylandClientClass)) +#define CLUTTER_IS_SEAT_WAYLAND_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CLUTTER_TYPE_SEAT_WAYLAND_CLIENT)) +#define CLUTTER_SEAT_WAYLAND_CLIENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CLUTTER_TYPE_SEAT_WAYLAND_CLIENT, ClutterSeatWaylandClientClass)) + +typedef struct _ClutterSeatWaylandClient ClutterSeatWaylandClient; +typedef struct _ClutterSeatWaylandClientClass ClutterSeatWaylandClientClass; + +struct _ClutterSeatWaylandClient +{ + ClutterSeat parent_instance; + + ClutterBackend *backend; + ClutterKeymap *keymap; + ClutterInputDevice *pointer_device; + ClutterInputDevice *keyboard_device; +}; + +struct _ClutterSeatWaylandClientClass +{ + ClutterSeatClass parent_class; +}; + +GType clutter_seat_wayland_client_get_type (void) G_GNUC_CONST; + +ClutterSeat * clutter_seat_wayland_client_new (ClutterBackend *backend); + +G_END_DECLS + +#endif /* __CLUTTER_SEAT_WAYLAND_CLIENT_H__ */ diff --git a/clutter/clutter/wayland-client/clutter-stage-wayland-client.c b/clutter/clutter/wayland-client/clutter-stage-wayland-client.c new file mode 100644 index 000000000..d2ca8e906 --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-stage-wayland-client.c @@ -0,0 +1,564 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Copyright (C) 2024 Linux Mint + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Michael Webster + */ + +#include "clutter-build-config.h" + +#include +#include + +#include "clutter-stage-wayland-client.h" +#include "clutter-backend-wayland-client.h" + +#include "clutter-actor-private.h" +#include "clutter-backend-private.h" +#include "clutter-debug.h" +#include "clutter-event.h" +#include "clutter-main.h" +#include "clutter-muffin.h" +#include "clutter-paint-context.h" +#include "clutter-private.h" +#include "clutter-stage-private.h" +#include "clutter-stage-view.h" +#include "clutter-text.h" +#include "cogl/clutter-stage-cogl.h" + +#include + +static void clutter_stage_window_iface_init (ClutterStageWindowInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (ClutterStageWaylandClient, + clutter_stage_wayland_client, + CLUTTER_TYPE_STAGE_COGL, + G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_STAGE_WINDOW, + clutter_stage_window_iface_init)) + +/* Forward declarations */ +static void schedule_frame_callback (ClutterStageWaylandClient *stage_wl); + +/* Layer surface listeners */ +static void +layer_surface_configure (void *data, + struct zwlr_layer_surface_v1 *layer_surface, + uint32_t serial, + uint32_t width, + uint32_t height) +{ + ClutterStageWaylandClient *stage_wl = data; + ClutterStageCogl *stage_cogl = CLUTTER_STAGE_COGL (stage_wl); + + CLUTTER_NOTE (BACKEND, "Layer surface configure: %dx%d (serial %d)", + width, height, serial); + + zwlr_layer_surface_v1_ack_configure (layer_surface, serial); + + if (width == 0 || height == 0) + return; + + stage_wl->width = width; + stage_wl->height = height; + + /* Resize the onscreen (which resizes the internal wl_egl_window) */ + if (stage_wl->onscreen) + { + cogl_wayland_onscreen_resize (stage_wl->onscreen, width, height, 0, 0); + CLUTTER_NOTE (BACKEND, "Resized onscreen to %dx%d", width, height); + } + + stage_wl->configured = TRUE; + + /* Create or update the stage view for resource scale calculation */ + { + cairo_rectangle_int_t view_layout = { + .x = 0, + .y = 0, + .width = width, + .height = height + }; + + if (stage_wl->view) + { + /* Update existing view layout */ + g_object_set (stage_wl->view, + "layout", &view_layout, + NULL); + } + else + { + /* Create new view */ + stage_wl->view = g_object_new (CLUTTER_TYPE_STAGE_VIEW_COGL, + "layout", &view_layout, + "framebuffer", COGL_FRAMEBUFFER (stage_wl->onscreen), + "scale", 1.0f, + NULL); + CLUTTER_NOTE (BACKEND, "Created stage view for resource scale"); + } + } + + /* Notify stage of size change and set the viewport immediately */ + clutter_actor_set_size (CLUTTER_ACTOR (stage_cogl->wrapper), width, height); + _clutter_stage_set_viewport (stage_cogl->wrapper, 0, 0, width, height); + + /* Force allocation of the stage */ + { + ClutterActorBox box = { 0, 0, width, height }; + clutter_actor_allocate (CLUTTER_ACTOR (stage_cogl->wrapper), &box, + CLUTTER_ALLOCATION_NONE); + } + + /* Schedule initial redraw */ + clutter_stage_ensure_redraw (stage_cogl->wrapper); +} + +static void +layer_surface_closed (void *data, + struct zwlr_layer_surface_v1 *layer_surface) +{ + ClutterStageWaylandClient *stage_wl = data; + + CLUTTER_NOTE (BACKEND, "Layer surface closed"); + + /* Emit delete-event on stage */ + ClutterEvent *event = clutter_event_new (CLUTTER_DELETE); + event->any.stage = CLUTTER_STAGE_COGL (stage_wl)->wrapper; + event->any.time = g_get_monotonic_time () / 1000; + clutter_stage_event (CLUTTER_STAGE_COGL (stage_wl)->wrapper, event); + clutter_event_free (event); +} + +static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { + .configure = layer_surface_configure, + .closed = layer_surface_closed, +}; + +/* Frame callback */ +static void +frame_callback_done (void *data, + struct wl_callback *callback, + uint32_t time) +{ + ClutterStageWaylandClient *stage_wl = data; + + wl_callback_destroy (callback); + stage_wl->frame_callback = NULL; + + /* Schedule next frame if needed */ + if (stage_wl->shown && stage_wl->configured) + { + clutter_stage_schedule_update (CLUTTER_STAGE_COGL (stage_wl)->wrapper); + } +} + +static const struct wl_callback_listener frame_callback_listener = { + .done = frame_callback_done, +}; + +static void +schedule_frame_callback (ClutterStageWaylandClient *stage_wl) +{ + if (stage_wl->frame_callback) + return; + + stage_wl->frame_callback = wl_surface_frame (stage_wl->wl_surface); + wl_callback_add_listener (stage_wl->frame_callback, + &frame_callback_listener, + stage_wl); +} + +/* Stage window interface implementation */ +static gboolean +clutter_stage_wayland_client_realize (ClutterStageWindow *stage_window) +{ + ClutterStageWaylandClient *stage_wl = CLUTTER_STAGE_WAYLAND_CLIENT (stage_window); + ClutterStageCogl *stage_cogl = CLUTTER_STAGE_COGL (stage_wl); + ClutterBackendWaylandClient *backend_wl; + CoglContext *cogl_context; + struct zwlr_layer_shell_v1 *layer_shell; + struct wl_output *output; + GError *error = NULL; + + CLUTTER_NOTE (BACKEND, "Realizing Wayland client stage"); + + backend_wl = CLUTTER_BACKEND_WAYLAND_CLIENT (stage_cogl->backend); + cogl_context = clutter_backend_get_cogl_context (stage_cogl->backend); + + layer_shell = clutter_backend_wayland_client_get_layer_shell (backend_wl); + output = clutter_backend_wayland_client_get_output (backend_wl); + + /* Create CoglOnscreen first - this creates the wl_surface via Cogl winsys */ + stage_wl->onscreen = cogl_onscreen_new (cogl_context, 1, 40); + + if (!cogl_framebuffer_allocate (COGL_FRAMEBUFFER (stage_wl->onscreen), &error)) + { + g_warning ("Failed to allocate onscreen framebuffer: %s", error->message); + g_error_free (error); + return FALSE; + } + + /* Get the wl_surface created by Cogl */ + stage_wl->wl_surface = cogl_wayland_onscreen_get_wl_surface (stage_wl->onscreen); + if (!stage_wl->wl_surface) + { + g_warning ("Failed to get wl_surface from onscreen"); + cogl_object_unref (stage_wl->onscreen); + stage_wl->onscreen = NULL; + return FALSE; + } + + /* Register surface with backend for input event routing */ + clutter_backend_wayland_client_register_surface (backend_wl, + stage_wl->wl_surface, + stage_wl); + + /* Create layer surface using the Cogl-created wl_surface */ + stage_wl->layer_surface = zwlr_layer_shell_v1_get_layer_surface ( + layer_shell, + stage_wl->wl_surface, + output, + stage_wl->layer, + "clutter-stage" + ); + + if (!stage_wl->layer_surface) + { + g_warning ("Failed to create layer surface"); + cogl_object_unref (stage_wl->onscreen); + stage_wl->onscreen = NULL; + stage_wl->wl_surface = NULL; + return FALSE; + } + + /* Apply layer-shell configuration */ + zwlr_layer_surface_v1_set_anchor (stage_wl->layer_surface, stage_wl->anchor); + + if (stage_wl->exclusive_zone >= 0) + zwlr_layer_surface_v1_set_exclusive_zone (stage_wl->layer_surface, + stage_wl->exclusive_zone); + + zwlr_layer_surface_v1_set_margin (stage_wl->layer_surface, + stage_wl->margin_top, + stage_wl->margin_right, + stage_wl->margin_bottom, + stage_wl->margin_left); + + /* Allow keyboard focus on click (needed for text entry etc.) */ + zwlr_layer_surface_v1_set_keyboard_interactivity ( + stage_wl->layer_surface, + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND); + + /* Set initial size (0 = let compositor decide based on anchors) */ + zwlr_layer_surface_v1_set_size (stage_wl->layer_surface, 0, 40); + + zwlr_layer_surface_v1_add_listener (stage_wl->layer_surface, + &layer_surface_listener, + stage_wl); + + /* Initial commit to get configure event */ + wl_surface_commit (stage_wl->wl_surface); + + CLUTTER_NOTE (BACKEND, "Wayland client stage realized, waiting for configure"); + + return TRUE; +} + +static void +clutter_stage_wayland_client_unrealize (ClutterStageWindow *stage_window) +{ + ClutterStageWaylandClient *stage_wl = CLUTTER_STAGE_WAYLAND_CLIENT (stage_window); + + CLUTTER_NOTE (BACKEND, "Unrealizing Wayland client stage"); + + if (stage_wl->frame_callback) + { + wl_callback_destroy (stage_wl->frame_callback); + stage_wl->frame_callback = NULL; + } + + /* Destroy stage view */ + g_clear_object (&stage_wl->view); + + /* Unregister surface from backend before destroying */ + if (stage_wl->wl_surface) + { + ClutterStageCogl *stage_cogl = CLUTTER_STAGE_COGL (stage_wl); + ClutterBackendWaylandClient *backend_wl; + + backend_wl = CLUTTER_BACKEND_WAYLAND_CLIENT (stage_cogl->backend); + clutter_backend_wayland_client_unregister_surface (backend_wl, + stage_wl->wl_surface); + } + + /* Destroy layer surface before onscreen (which owns wl_surface) */ + if (stage_wl->layer_surface) + { + zwlr_layer_surface_v1_destroy (stage_wl->layer_surface); + stage_wl->layer_surface = NULL; + } + + /* Destroying onscreen will destroy the wl_surface and egl_window */ + if (stage_wl->onscreen) + { + cogl_object_unref (stage_wl->onscreen); + stage_wl->onscreen = NULL; + } + + stage_wl->wl_surface = NULL; + stage_wl->configured = FALSE; +} + +static void +clutter_stage_wayland_client_show (ClutterStageWindow *stage_window, + gboolean do_raise) +{ + ClutterStageWaylandClient *stage_wl = CLUTTER_STAGE_WAYLAND_CLIENT (stage_window); + + CLUTTER_NOTE (BACKEND, "Showing Wayland client stage"); + + stage_wl->shown = TRUE; + + /* Map the actor */ + clutter_actor_map (CLUTTER_ACTOR (CLUTTER_STAGE_COGL (stage_wl)->wrapper)); +} + +static void +clutter_stage_wayland_client_hide (ClutterStageWindow *stage_window) +{ + ClutterStageWaylandClient *stage_wl = CLUTTER_STAGE_WAYLAND_CLIENT (stage_window); + + CLUTTER_NOTE (BACKEND, "Hiding Wayland client stage"); + + stage_wl->shown = FALSE; + + /* Unmap the actor */ + clutter_actor_unmap (CLUTTER_ACTOR (CLUTTER_STAGE_COGL (stage_wl)->wrapper)); +} + +static void +clutter_stage_wayland_client_resize (ClutterStageWindow *stage_window, + gint width, + gint height) +{ + ClutterStageWaylandClient *stage_wl = CLUTTER_STAGE_WAYLAND_CLIENT (stage_window); + + CLUTTER_NOTE (BACKEND, "Resize request: %dx%d", width, height); + + /* For layer shell, we can request a specific size, but the compositor decides */ + if (stage_wl->layer_surface) + { + zwlr_layer_surface_v1_set_size (stage_wl->layer_surface, width, height); + wl_surface_commit (stage_wl->wl_surface); + } +} + +static void +clutter_stage_wayland_client_get_geometry (ClutterStageWindow *stage_window, + cairo_rectangle_int_t *geometry) +{ + ClutterStageWaylandClient *stage_wl = CLUTTER_STAGE_WAYLAND_CLIENT (stage_window); + + geometry->x = 0; + geometry->y = 0; + geometry->width = stage_wl->width; + geometry->height = stage_wl->height; +} + +static GList * +clutter_stage_wayland_client_get_views (ClutterStageWindow *stage_window) +{ + ClutterStageWaylandClient *stage_wl = CLUTTER_STAGE_WAYLAND_CLIENT (stage_window); + + if (stage_wl->view) + return g_list_prepend (NULL, stage_wl->view); + + return NULL; +} + +static gboolean +clutter_stage_wayland_client_can_clip_redraws (ClutterStageWindow *stage_window) +{ + return TRUE; +} + +static void +clutter_stage_wayland_client_redraw (ClutterStageWindow *stage_window) +{ + ClutterStageWaylandClient *stage_wl = CLUTTER_STAGE_WAYLAND_CLIENT (stage_window); + ClutterStageCogl *stage_cogl = CLUTTER_STAGE_COGL (stage_wl); + + if (!stage_wl->configured || !stage_wl->onscreen) + return; + + if (stage_cogl->wrapper) + { + ClutterStage *stage = stage_cogl->wrapper; + CoglFramebuffer *fb = COGL_FRAMEBUFFER (stage_wl->onscreen); + ClutterPaintContext *paint_context; + CoglMatrix identity; + + /* Ensure actors are laid out before painting */ + _clutter_stage_maybe_relayout (CLUTTER_ACTOR (stage)); + + /* Clear the framebuffer */ + cogl_framebuffer_clear4f (fb, COGL_BUFFER_BIT_COLOR | COGL_BUFFER_BIT_DEPTH, + 0.2f, 0.2f, 0.3f, 1.0f); + + /* Use orthographic projection for 2D panel */ + cogl_matrix_init_identity (&identity); + cogl_framebuffer_set_viewport (fb, 0, 0, stage_wl->width, stage_wl->height); + cogl_framebuffer_orthographic (fb, 0, 0, stage_wl->width, stage_wl->height, -1, 1); + cogl_framebuffer_set_modelview_matrix (fb, &identity); + + /* Disable the stage's model-view transform (designed for perspective) */ + _clutter_actor_set_enable_model_view_transform (CLUTTER_ACTOR (stage), FALSE); + + /* Create paint context and paint the stage */ + paint_context = clutter_paint_context_new_for_framebuffer (fb); + clutter_actor_paint (CLUTTER_ACTOR (stage), paint_context); + clutter_paint_context_unref (paint_context); + + /* Re-enable for future use */ + _clutter_actor_set_enable_model_view_transform (CLUTTER_ACTOR (stage), TRUE); + + cogl_onscreen_swap_buffers (stage_wl->onscreen); + } + + /* Schedule frame callback */ + schedule_frame_callback (stage_wl); + wl_surface_commit (stage_wl->wl_surface); +} + +static void +clutter_stage_wayland_client_finish_frame (ClutterStageWindow *stage_window) +{ + ClutterStageWaylandClient *stage_wl = CLUTTER_STAGE_WAYLAND_CLIENT (stage_window); + ClutterBackendWaylandClient *backend_wl; + + backend_wl = CLUTTER_BACKEND_WAYLAND_CLIENT (CLUTTER_STAGE_COGL (stage_wl)->backend); + + /* Flush Wayland connection */ + wl_display_flush (clutter_backend_wayland_client_get_wl_display (backend_wl)); +} + +static void +clutter_stage_window_iface_init (ClutterStageWindowInterface *iface) +{ + iface->realize = clutter_stage_wayland_client_realize; + iface->unrealize = clutter_stage_wayland_client_unrealize; + iface->show = clutter_stage_wayland_client_show; + iface->hide = clutter_stage_wayland_client_hide; + iface->resize = clutter_stage_wayland_client_resize; + iface->get_geometry = clutter_stage_wayland_client_get_geometry; + iface->get_views = clutter_stage_wayland_client_get_views; + iface->can_clip_redraws = clutter_stage_wayland_client_can_clip_redraws; + iface->redraw = clutter_stage_wayland_client_redraw; + iface->finish_frame = clutter_stage_wayland_client_finish_frame; +} + +static void +clutter_stage_wayland_client_finalize (GObject *object) +{ + ClutterStageWaylandClient *stage_wl = CLUTTER_STAGE_WAYLAND_CLIENT (object); + + clutter_stage_wayland_client_unrealize (CLUTTER_STAGE_WINDOW (stage_wl)); + + G_OBJECT_CLASS (clutter_stage_wayland_client_parent_class)->finalize (object); +} + +static void +clutter_stage_wayland_client_class_init (ClutterStageWaylandClientClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = clutter_stage_wayland_client_finalize; +} + +static void +clutter_stage_wayland_client_init (ClutterStageWaylandClient *stage) +{ + /* Default to top layer, bottom-anchored panel */ + stage->layer = CLUTTER_LAYER_SHELL_LAYER_TOP; + stage->anchor = CLUTTER_LAYER_SHELL_ANCHOR_BOTTOM | + CLUTTER_LAYER_SHELL_ANCHOR_LEFT | + CLUTTER_LAYER_SHELL_ANCHOR_RIGHT; + stage->exclusive_zone = 40; + stage->margin_top = 0; + stage->margin_bottom = 0; + stage->margin_left = 0; + stage->margin_right = 0; +} + +/* Configuration API */ +void +clutter_stage_wayland_client_set_layer (ClutterStageWaylandClient *stage, + ClutterLayerShellLayer layer) +{ + g_return_if_fail (CLUTTER_IS_STAGE_WAYLAND_CLIENT (stage)); + stage->layer = layer; +} + +void +clutter_stage_wayland_client_set_anchor (ClutterStageWaylandClient *stage, + uint32_t anchor) +{ + g_return_if_fail (CLUTTER_IS_STAGE_WAYLAND_CLIENT (stage)); + stage->anchor = anchor; + + if (stage->layer_surface) + { + zwlr_layer_surface_v1_set_anchor (stage->layer_surface, anchor); + wl_surface_commit (stage->wl_surface); + } +} + +void +clutter_stage_wayland_client_set_exclusive_zone (ClutterStageWaylandClient *stage, + int32_t zone) +{ + g_return_if_fail (CLUTTER_IS_STAGE_WAYLAND_CLIENT (stage)); + stage->exclusive_zone = zone; + + if (stage->layer_surface) + { + zwlr_layer_surface_v1_set_exclusive_zone (stage->layer_surface, zone); + wl_surface_commit (stage->wl_surface); + } +} + +void +clutter_stage_wayland_client_set_margin (ClutterStageWaylandClient *stage, + int32_t top, int32_t right, + int32_t bottom, int32_t left) +{ + g_return_if_fail (CLUTTER_IS_STAGE_WAYLAND_CLIENT (stage)); + stage->margin_top = top; + stage->margin_right = right; + stage->margin_bottom = bottom; + stage->margin_left = left; + + if (stage->layer_surface) + { + zwlr_layer_surface_v1_set_margin (stage->layer_surface, + top, right, bottom, left); + wl_surface_commit (stage->wl_surface); + } +} diff --git a/clutter/clutter/wayland-client/clutter-stage-wayland-client.h b/clutter/clutter/wayland-client/clutter-stage-wayland-client.h new file mode 100644 index 000000000..5ffa5e525 --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-stage-wayland-client.h @@ -0,0 +1,109 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Copyright (C) 2024 Linux Mint + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Michael Webster + */ + +#ifndef __CLUTTER_STAGE_WAYLAND_CLIENT_H__ +#define __CLUTTER_STAGE_WAYLAND_CLIENT_H__ + +#include +#include + +#include +#include + +#include "cogl/clutter-stage-cogl.h" +#include "wlr-layer-shell-unstable-v1-client-protocol.h" + +G_BEGIN_DECLS + +#define CLUTTER_TYPE_STAGE_WAYLAND_CLIENT (clutter_stage_wayland_client_get_type ()) + +G_DECLARE_FINAL_TYPE (ClutterStageWaylandClient, + clutter_stage_wayland_client, + CLUTTER, STAGE_WAYLAND_CLIENT, + ClutterStageCogl) + +/* Layer shell configuration */ +typedef enum { + CLUTTER_LAYER_SHELL_LAYER_BACKGROUND = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, + CLUTTER_LAYER_SHELL_LAYER_BOTTOM = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM, + CLUTTER_LAYER_SHELL_LAYER_TOP = ZWLR_LAYER_SHELL_V1_LAYER_TOP, + CLUTTER_LAYER_SHELL_LAYER_OVERLAY = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, +} ClutterLayerShellLayer; + +typedef enum { + CLUTTER_LAYER_SHELL_ANCHOR_TOP = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP, + CLUTTER_LAYER_SHELL_ANCHOR_BOTTOM = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM, + CLUTTER_LAYER_SHELL_ANCHOR_LEFT = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT, + CLUTTER_LAYER_SHELL_ANCHOR_RIGHT = ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT, +} ClutterLayerShellAnchor; + +struct _ClutterStageWaylandClient +{ + ClutterStageCogl parent_instance; + + /* Cogl onscreen (owns the wl_surface and egl_window) */ + CoglOnscreen *onscreen; + + /* Wayland surface (owned by Cogl, don't destroy directly) */ + struct wl_surface *wl_surface; + + /* Layer shell surface (we own this) */ + struct zwlr_layer_surface_v1 *layer_surface; + + /* Layer shell configuration */ + ClutterLayerShellLayer layer; + uint32_t anchor; + int32_t exclusive_zone; + int32_t margin_top; + int32_t margin_bottom; + int32_t margin_left; + int32_t margin_right; + + /* State */ + gboolean configured; + gboolean shown; + int width; + int height; + + /* Frame callback */ + struct wl_callback *frame_callback; + + /* Stage view for resource scale */ + ClutterStageView *view; +}; + +/* Configuration API */ +void clutter_stage_wayland_client_set_layer (ClutterStageWaylandClient *stage, + ClutterLayerShellLayer layer); +void clutter_stage_wayland_client_set_anchor (ClutterStageWaylandClient *stage, + uint32_t anchor); +void clutter_stage_wayland_client_set_exclusive_zone (ClutterStageWaylandClient *stage, + int32_t zone); +void clutter_stage_wayland_client_set_margin (ClutterStageWaylandClient *stage, + int32_t top, int32_t right, + int32_t bottom, int32_t left); + +G_END_DECLS + +#endif /* __CLUTTER_STAGE_WAYLAND_CLIENT_H__ */ diff --git a/clutter/clutter/wayland-client/wlr-layer-shell-unstable-v1.xml b/clutter/clutter/wayland-client/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 000000000..6998081ec --- /dev/null +++ b/clutter/clutter/wayland-client/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,406 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Types of keyboard interaction possible for layer shell surfaces. The + rationale for this is twofold: (1) some applications are not interested + in keyboard events and not allowing them to be focused can improve the + desktop experience; (2) some applications will want to take exclusive + keyboard focus. + + + + + This value indicates that this surface is not interested in keyboard + events and the compositor should never assign it the keyboard focus. + + This is the default value, set for newly created layer shell surfaces. + + This is useful for e.g. desktop widgets that display information or + only have interaction with non-keyboard input devices. + + + + + Request exclusive keyboard focus if this surface is above the shell surface layer. + + For the top and overlay layers, the seat will always give + exclusive keyboard focus to the top-most layer which has keyboard + interactivity set to exclusive. If this layer contains multiple + surfaces with keyboard interactivity set to exclusive, the compositor + determines the one receiving keyboard events in an implementation- + defined manner. In this case, no guarantee is made when this surface + will receive keyboard focus (if ever). + + For the bottom and background layers, the compositor is allowed to use + normal focus semantics. + + This setting is mainly intended for applications that need to ensure + they receive all keyboard events, such as a lock screen or a password + prompt. + + + + + This requests the compositor to allow this surface to be focused and + unfocused by the user in an implementation-defined manner. The user + should be able to unfocus this surface even regardless of the layer + it is on. + + Typically, the compositor will want to use its normal mechanism to + manage keyboard focus between layer shell surfaces with this setting + and regular toplevels on the desktop layer (e.g. click to focus). + Nevertheless, it is possible for a compositor to require a special + interaction to focus or unfocus layer shell surfaces (e.g. requiring + a click even if focus follows the mouse normally, or providing a + keybinding to switch focus between layers). + + This setting is mainly intended for desktop shell components (e.g. + panels) that allow keyboard interaction. Using this option can allow + implementing a desktop shell that can be fully usable without the + mouse. + + + + + + + Set how keyboard events are delivered to this surface. By default, + layer shell surfaces do not receive keyboard events; this request can + be used to change this. + + This setting is inherited by child surfaces set by the get_popup + request. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Keyboard interactivity is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + + + + + + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + + + + + + + + + Requests an edge for the exclusive zone to apply. The exclusive + edge will be automatically deduced from anchor points when possible, + but when the surface is anchored to a corner, it will be necessary + to set it explicitly to disambiguate, as it is not possible to deduce + which one of the two corner edges should be used. + + The edge must be one the surface is anchored to, otherwise the + invalid_exclusive_edge protocol error will be raised. + + + + + diff --git a/clutter/meson.build b/clutter/meson.build index a9dc2c61d..e0fa790bb 100644 --- a/clutter/meson.build +++ b/clutter/meson.build @@ -77,6 +77,13 @@ if have_libwacom ] endif +if have_wayland_client + clutter_pkg_private_deps += [ + wayland_client_dep, + xkbcommon_dep, + ] +endif + clutter_deps = [ clutter_pkg_deps, clutter_pkg_private_deps, diff --git a/cogl/cogl/cogl-defines.h.meson b/cogl/cogl/cogl-defines.h.meson index 4c9535239..fb46a80c1 100644 --- a/cogl/cogl/cogl-defines.h.meson +++ b/cogl/cogl/cogl-defines.h.meson @@ -41,6 +41,7 @@ #mesondefine COGL_HAS_GLX_SUPPORT #mesondefine COGL_HAS_WAYLAND_EGL_SERVER_SUPPORT #mesondefine COGL_HAS_EGL_PLATFORM_XLIB_SUPPORT +#mesondefine COGL_HAS_EGL_PLATFORM_WAYLAND_CLIENT_SUPPORT #mesondefine COGL_HAS_EGL_SUPPORT #mesondefine COGL_HAS_X11 #mesondefine COGL_HAS_X11_SUPPORT diff --git a/cogl/cogl/cogl-renderer-private.h b/cogl/cogl/cogl-renderer-private.h index 62e99fdb0..b7d871c3c 100644 --- a/cogl/cogl/cogl-renderer-private.h +++ b/cogl/cogl/cogl-renderer-private.h @@ -69,6 +69,10 @@ struct _CoglRenderer gboolean xlib_want_reset_on_video_memory_purge; #endif +#ifdef COGL_HAS_EGL_PLATFORM_WAYLAND_CLIENT_SUPPORT + struct wl_display *foreign_wayland_display; +#endif + CoglDriver driver; unsigned long private_features [COGL_FLAGS_N_LONGS_FOR_SIZE (COGL_N_PRIVATE_FEATURES)]; diff --git a/cogl/cogl/cogl-renderer.c b/cogl/cogl/cogl-renderer.c index 2149f38dc..5acbe628e 100644 --- a/cogl/cogl/cogl-renderer.c +++ b/cogl/cogl/cogl-renderer.c @@ -53,6 +53,9 @@ #ifdef COGL_HAS_GLX_SUPPORT #include "winsys/cogl-winsys-glx-private.h" #endif +#ifdef COGL_HAS_EGL_PLATFORM_WAYLAND_CLIENT_SUPPORT +#include "winsys/cogl-winsys-egl-wayland-private.h" +#endif #ifdef COGL_HAS_XLIB_SUPPORT #include "cogl-xlib-renderer.h" @@ -135,6 +138,9 @@ static CoglWinsysVtableGetter _cogl_winsys_vtable_getters[] = #ifdef COGL_HAS_EGL_PLATFORM_XLIB_SUPPORT _cogl_winsys_egl_xlib_get_vtable, #endif +#ifdef COGL_HAS_EGL_PLATFORM_WAYLAND_CLIENT_SUPPORT + _cogl_winsys_egl_wayland_get_vtable, +#endif }; static void _cogl_renderer_free (CoglRenderer *renderer); @@ -244,6 +250,28 @@ cogl_xlib_renderer_request_reset_on_video_memory_purge (CoglRenderer *renderer, } #endif /* COGL_HAS_XLIB_SUPPORT */ +#ifdef COGL_HAS_EGL_PLATFORM_WAYLAND_CLIENT_SUPPORT +COGL_EXPORT void +cogl_wayland_renderer_set_foreign_display (CoglRenderer *renderer, + struct wl_display *display) +{ + g_return_if_fail (cogl_is_renderer (renderer)); + + /* NB: Renderers are considered immutable once connected */ + g_return_if_fail (!renderer->connected); + + renderer->foreign_wayland_display = display; +} + +COGL_EXPORT struct wl_display * +cogl_wayland_renderer_get_display (CoglRenderer *renderer) +{ + g_return_val_if_fail (cogl_is_renderer (renderer), NULL); + + return renderer->foreign_wayland_display; +} +#endif /* COGL_HAS_EGL_PLATFORM_WAYLAND_CLIENT_SUPPORT */ + gboolean cogl_renderer_check_onscreen_template (CoglRenderer *renderer, CoglOnscreenTemplate *onscreen_template, diff --git a/cogl/cogl/cogl-renderer.h b/cogl/cogl/cogl-renderer.h index b8eb6e545..87734678e 100644 --- a/cogl/cogl/cogl-renderer.h +++ b/cogl/cogl/cogl-renderer.h @@ -169,6 +169,7 @@ typedef enum COGL_WINSYS_ID_STUB, COGL_WINSYS_ID_GLX, COGL_WINSYS_ID_EGL_XLIB, + COGL_WINSYS_ID_EGL_WAYLAND, COGL_WINSYS_ID_CUSTOM, } CoglWinsysID; diff --git a/cogl/cogl/cogl-wayland-client.h b/cogl/cogl/cogl-wayland-client.h new file mode 100644 index 000000000..68e3a028d --- /dev/null +++ b/cogl/cogl/cogl-wayland-client.h @@ -0,0 +1,132 @@ +/* + * Cogl + * + * A Low Level GPU Graphics and Utilities API + * + * Copyright (C) 2024 Linux Mint + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Authors: + * Michael Webster + */ + +#ifndef __COGL_WAYLAND_CLIENT_H__ +#define __COGL_WAYLAND_CLIENT_H__ + +/* NB: this is a top-level header that can be included directly but we + * want to be careful not to define __COGL_H_INSIDE__ when this is + * included internally while building Cogl itself since + * __COGL_H_INSIDE__ is used in headers to guard public vs private api + * definitions + */ +#ifndef COGL_COMPILATION + +#ifndef __COGL_H_INSIDE__ +#define __COGL_H_INSIDE__ +#define __COGL_WAYLAND_CLIENT_H_MUST_UNDEF_COGL_H_INSIDE__ +#endif + +#endif /* COGL_COMPILATION */ + +#include +#include + +#include + +G_BEGIN_DECLS + +/** + * cogl_wayland_renderer_set_foreign_display: + * @renderer: A #CoglRenderer + * @display: A Wayland display + * + * Allows you to specify a foreign Wayland display for Cogl to use + * as a Wayland client. This must be called before the renderer is + * connected. + * + * Since: 1.20 + * Stability: Unstable + */ +COGL_EXPORT void +cogl_wayland_renderer_set_foreign_display (CoglRenderer *renderer, + struct wl_display *display); + +/** + * cogl_wayland_renderer_get_display: + * @renderer: A #CoglRenderer + * + * Retrieves the Wayland display that Cogl is using as a client. This + * may be a foreign display that was set with + * cogl_wayland_renderer_set_foreign_display() or it will be the + * display that Cogl created internally. + * + * Return value: The Wayland display currently used by Cogl + * + * Since: 1.20 + * Stability: Unstable + */ +COGL_EXPORT struct wl_display * +cogl_wayland_renderer_get_display (CoglRenderer *renderer); + +/** + * cogl_wayland_onscreen_get_wl_surface: + * @onscreen: A #CoglOnscreen + * + * Gets the underlying wl_surface for an onscreen framebuffer. + * + * Return value: The wl_surface for this onscreen + * + * Since: 1.20 + * Stability: Unstable + */ +COGL_EXPORT struct wl_surface * +cogl_wayland_onscreen_get_wl_surface (CoglOnscreen *onscreen); + +/** + * cogl_wayland_onscreen_resize: + * @onscreen: A #CoglOnscreen + * @width: New width + * @height: New height + * @offset_x: X offset for content + * @offset_y: Y offset for content + * + * Resizes the underlying wl_egl_window for an onscreen framebuffer. + * This should be called when the Wayland surface is resized. + * + * Since: 1.20 + * Stability: Unstable + */ +COGL_EXPORT void +cogl_wayland_onscreen_resize (CoglOnscreen *onscreen, + int width, + int height, + int offset_x, + int offset_y); + +G_END_DECLS + +#ifdef __COGL_WAYLAND_CLIENT_H_MUST_UNDEF_COGL_H_INSIDE__ +#undef __COGL_H_INSIDE__ +#undef __COGL_WAYLAND_CLIENT_H_MUST_UNDEF_COGL_H_INSIDE__ +#endif + +#endif /* __COGL_WAYLAND_CLIENT_H__ */ diff --git a/cogl/cogl/meson.build b/cogl/cogl/meson.build index 55523dd3f..9acc71ea6 100644 --- a/cogl/cogl/meson.build +++ b/cogl/cogl/meson.build @@ -7,6 +7,7 @@ cdata.set('CLUTTER_COGL_HAS_GL', have_gl) cdata.set('COGL_HAS_GLX_SUPPORT', have_glx) cdata.set('COGL_HAS_WAYLAND_EGL_SERVER_SUPPORT', have_wayland) cdata.set('COGL_HAS_EGL_PLATFORM_XLIB_SUPPORT', have_egl_xlib) +cdata.set('COGL_HAS_EGL_PLATFORM_WAYLAND_CLIENT_SUPPORT', have_wayland_client) cdata.set('COGL_HAS_EGL_SUPPORT', have_egl) cdata.set('COGL_HAS_X11', have_x11) cdata.set('COGL_HAS_X11_SUPPORT', have_x11) @@ -415,6 +416,17 @@ if have_egl_xlib ] endif +if have_wayland_client + cogl_sources += [ + 'winsys/cogl-winsys-egl-wayland.c', + 'winsys/cogl-winsys-egl-wayland-private.h', + 'cogl-wayland-client.h', + ] + cogl_nonintrospected_headers += [ + 'cogl-wayland-client.h', + ] +endif + cogl_introspected_headers = [ cogl_headers, cogl_deprecated_headers, diff --git a/cogl/cogl/winsys/cogl-winsys-egl-wayland-private.h b/cogl/cogl/winsys/cogl-winsys-egl-wayland-private.h new file mode 100644 index 000000000..54a2017a0 --- /dev/null +++ b/cogl/cogl/winsys/cogl-winsys-egl-wayland-private.h @@ -0,0 +1,40 @@ +/* + * Cogl + * + * A Low Level GPU Graphics and Utilities API + * + * Copyright (C) 2024 Linux Mint + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Authors: + * Michael Webster + */ + +#ifndef __COGL_WINSYS_EGL_WAYLAND_PRIVATE_H +#define __COGL_WINSYS_EGL_WAYLAND_PRIVATE_H + +#include "winsys/cogl-winsys-private.h" + +COGL_EXPORT const CoglWinsysVtable * +_cogl_winsys_egl_wayland_get_vtable (void); + +#endif /* __COGL_WINSYS_EGL_WAYLAND_PRIVATE_H */ diff --git a/cogl/cogl/winsys/cogl-winsys-egl-wayland.c b/cogl/cogl/winsys/cogl-winsys-egl-wayland.c new file mode 100644 index 000000000..3e5ab9f2d --- /dev/null +++ b/cogl/cogl/winsys/cogl-winsys-egl-wayland.c @@ -0,0 +1,540 @@ +/* + * Cogl + * + * A Low Level GPU Graphics and Utilities API + * + * Copyright (C) 2024 Linux Mint + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Authors: + * Michael Webster + * + * Wayland client winsys for Cogl - allows Cogl to run as a Wayland client + */ + +#include "cogl-config.h" + +#include +#include +#include + +/* EGL Wayland platform defines - may not be in all EGL headers */ +#ifndef EGL_PLATFORM_WAYLAND_KHR +#define EGL_PLATFORM_WAYLAND_KHR 0x31D8 +#endif +#ifndef EGL_PLATFORM_WAYLAND_EXT +#define EGL_PLATFORM_WAYLAND_EXT 0x31D8 +#endif + +#include "cogl-wayland-client.h" +#include "cogl-macros.h" +#include "winsys/cogl-winsys-egl-wayland-private.h" +#include "winsys/cogl-winsys-egl-private.h" +#include "cogl-renderer-private.h" +#include "cogl-onscreen-private.h" +#include "cogl-framebuffer-private.h" +#include "cogl-display-private.h" +#include "cogl-private.h" + +static const CoglWinsysEGLVtable _cogl_winsys_egl_vtable; + +typedef struct _CoglRendererWayland +{ + struct wl_display *wl_display; + struct wl_registry *wl_registry; + struct wl_compositor *wl_compositor; + gboolean own_display; +} CoglRendererWayland; + +typedef struct _CoglDisplayWayland +{ + struct wl_surface *dummy_surface; + struct wl_egl_window *dummy_egl_window; +} CoglDisplayWayland; + +typedef struct _CoglOnscreenWayland +{ + struct wl_surface *wl_surface; + struct wl_egl_window *wl_egl_window; + int pending_width; + int pending_height; +} CoglOnscreenWayland; + +/* Registry listener */ +static void +registry_global (void *data, + struct wl_registry *registry, + uint32_t name, + const char *interface, + uint32_t version) +{ + CoglRendererWayland *wayland_renderer = data; + + if (strcmp (interface, "wl_compositor") == 0) + { + wayland_renderer->wl_compositor = + wl_registry_bind (registry, name, &wl_compositor_interface, + MIN ((uint32_t) version, 4)); + } +} + +static void +registry_global_remove (void *data, + struct wl_registry *registry, + uint32_t name) +{ +} + +static const struct wl_registry_listener registry_listener = { + registry_global, + registry_global_remove +}; + +static void +_cogl_winsys_renderer_disconnect (CoglRenderer *renderer) +{ + CoglRendererEGL *egl_renderer = renderer->winsys; + CoglRendererWayland *wayland_renderer = egl_renderer->platform; + + eglTerminate (egl_renderer->edpy); + + if (wayland_renderer->wl_compositor) + wl_compositor_destroy (wayland_renderer->wl_compositor); + + if (wayland_renderer->wl_registry) + wl_registry_destroy (wayland_renderer->wl_registry); + + if (wayland_renderer->own_display && wayland_renderer->wl_display) + wl_display_disconnect (wayland_renderer->wl_display); + + g_free (wayland_renderer); + + g_slice_free (CoglRendererEGL, egl_renderer); +} + +static EGLDisplay +_cogl_winsys_egl_get_display (void *native) +{ + EGLDisplay dpy = NULL; + const char *client_exts = eglQueryString (NULL, EGL_EXTENSIONS); + + if (client_exts && g_strstr_len (client_exts, -1, "EGL_KHR_platform_base")) + { + PFNEGLGETPLATFORMDISPLAYEXTPROC get_platform_display = + (void *) eglGetProcAddress ("eglGetPlatformDisplay"); + + if (get_platform_display) + dpy = get_platform_display (EGL_PLATFORM_WAYLAND_KHR, native, NULL); + + if (dpy) + return dpy; + } + + if (client_exts && g_strstr_len (client_exts, -1, "EGL_EXT_platform_base")) + { + PFNEGLGETPLATFORMDISPLAYEXTPROC get_platform_display = + (void *) eglGetProcAddress ("eglGetPlatformDisplayEXT"); + + if (get_platform_display) + dpy = get_platform_display (EGL_PLATFORM_WAYLAND_EXT, native, NULL); + + if (dpy) + return dpy; + } + + return eglGetDisplay ((EGLNativeDisplayType) native); +} + +static gboolean +_cogl_winsys_renderer_connect (CoglRenderer *renderer, + GError **error) +{ + CoglRendererEGL *egl_renderer; + CoglRendererWayland *wayland_renderer; + + renderer->winsys = g_slice_new0 (CoglRendererEGL); + egl_renderer = renderer->winsys; + wayland_renderer = g_new0 (CoglRendererWayland, 1); + egl_renderer->platform = wayland_renderer; + egl_renderer->platform_vtable = &_cogl_winsys_egl_vtable; + + /* Check for foreign display */ + wayland_renderer->wl_display = renderer->foreign_wayland_display; + if (wayland_renderer->wl_display) + { + wayland_renderer->own_display = FALSE; + } + else + { + wayland_renderer->wl_display = wl_display_connect (NULL); + wayland_renderer->own_display = TRUE; + } + + if (!wayland_renderer->wl_display) + { + g_set_error (error, COGL_WINSYS_ERROR, + COGL_WINSYS_ERROR_INIT, + "Failed to connect to Wayland display"); + goto error; + } + + /* Get registry and bind compositor */ + wayland_renderer->wl_registry = + wl_display_get_registry (wayland_renderer->wl_display); + wl_registry_add_listener (wayland_renderer->wl_registry, + ®istry_listener, + wayland_renderer); + wl_display_roundtrip (wayland_renderer->wl_display); + + if (!wayland_renderer->wl_compositor) + { + g_set_error (error, COGL_WINSYS_ERROR, + COGL_WINSYS_ERROR_INIT, + "wl_compositor not available"); + goto error; + } + + egl_renderer->edpy = _cogl_winsys_egl_get_display (wayland_renderer->wl_display); + + if (!_cogl_winsys_egl_renderer_connect_common (renderer, error)) + goto error; + + return TRUE; + +error: + _cogl_winsys_renderer_disconnect (renderer); + return FALSE; +} + +static int +_cogl_winsys_egl_add_config_attributes (CoglDisplay *display, + CoglFramebufferConfig *config, + EGLint *attributes) +{ + int i = 0; + + attributes[i++] = EGL_SURFACE_TYPE; + attributes[i++] = EGL_WINDOW_BIT; + + return i; +} + +static gboolean +_cogl_winsys_egl_choose_config (CoglDisplay *display, + EGLint *attributes, + EGLConfig *out_config, + GError **error) +{ + CoglRenderer *renderer = display->renderer; + CoglRendererEGL *egl_renderer = renderer->winsys; + EGLint config_count = 0; + EGLBoolean status; + + status = eglChooseConfig (egl_renderer->edpy, + attributes, + out_config, 1, + &config_count); + if (status != EGL_TRUE || config_count == 0) + { + g_set_error (error, COGL_WINSYS_ERROR, + COGL_WINSYS_ERROR_CREATE_CONTEXT, + "No compatible EGL configs found"); + return FALSE; + } + + return TRUE; +} + +static gboolean +_cogl_winsys_egl_display_setup (CoglDisplay *display, + GError **error) +{ + CoglDisplayEGL *egl_display = display->winsys; + CoglDisplayWayland *wayland_display; + + wayland_display = g_slice_new0 (CoglDisplayWayland); + egl_display->platform = wayland_display; + + return TRUE; +} + +static void +_cogl_winsys_egl_display_destroy (CoglDisplay *display) +{ + CoglDisplayEGL *egl_display = display->winsys; + CoglDisplayWayland *wayland_display = egl_display->platform; + + g_slice_free (CoglDisplayWayland, wayland_display); +} + +static gboolean +_cogl_winsys_egl_context_created (CoglDisplay *display, + GError **error) +{ + CoglRenderer *renderer = display->renderer; + CoglRendererEGL *egl_renderer = renderer->winsys; + CoglRendererWayland *wayland_renderer = egl_renderer->platform; + CoglDisplayEGL *egl_display = display->winsys; + CoglDisplayWayland *wayland_display = egl_display->platform; + + /* Create dummy surface for context */ + wayland_display->dummy_surface = + wl_compositor_create_surface (wayland_renderer->wl_compositor); + if (!wayland_display->dummy_surface) + { + g_set_error (error, COGL_WINSYS_ERROR, + COGL_WINSYS_ERROR_CREATE_CONTEXT, + "Failed to create dummy wl_surface"); + return FALSE; + } + + wayland_display->dummy_egl_window = + wl_egl_window_create (wayland_display->dummy_surface, 1, 1); + if (!wayland_display->dummy_egl_window) + { + g_set_error (error, COGL_WINSYS_ERROR, + COGL_WINSYS_ERROR_CREATE_CONTEXT, + "Failed to create dummy wl_egl_window"); + return FALSE; + } + + egl_display->dummy_surface = + eglCreateWindowSurface (egl_renderer->edpy, + egl_display->egl_config, + (EGLNativeWindowType) wayland_display->dummy_egl_window, + NULL); + if (egl_display->dummy_surface == EGL_NO_SURFACE) + { + g_set_error (error, COGL_WINSYS_ERROR, + COGL_WINSYS_ERROR_CREATE_CONTEXT, + "Failed to create dummy EGL surface"); + return FALSE; + } + + if (!_cogl_winsys_egl_make_current (display, + egl_display->dummy_surface, + egl_display->dummy_surface, + egl_display->egl_context)) + { + g_set_error (error, COGL_WINSYS_ERROR, + COGL_WINSYS_ERROR_CREATE_CONTEXT, + "Failed to make context current"); + return FALSE; + } + + return TRUE; +} + +static void +_cogl_winsys_egl_cleanup_context (CoglDisplay *display) +{ + CoglRenderer *renderer = display->renderer; + CoglRendererEGL *egl_renderer = renderer->winsys; + CoglDisplayEGL *egl_display = display->winsys; + CoglDisplayWayland *wayland_display = egl_display->platform; + + if (egl_display->dummy_surface != EGL_NO_SURFACE) + { + eglDestroySurface (egl_renderer->edpy, egl_display->dummy_surface); + egl_display->dummy_surface = EGL_NO_SURFACE; + } + + if (wayland_display->dummy_egl_window) + { + wl_egl_window_destroy (wayland_display->dummy_egl_window); + wayland_display->dummy_egl_window = NULL; + } + + if (wayland_display->dummy_surface) + { + wl_surface_destroy (wayland_display->dummy_surface); + wayland_display->dummy_surface = NULL; + } +} + +static gboolean +_cogl_winsys_egl_onscreen_init (CoglOnscreen *onscreen, + EGLConfig egl_config, + GError **error) +{ + CoglFramebuffer *framebuffer = COGL_FRAMEBUFFER (onscreen); + CoglContext *context = framebuffer->context; + CoglRenderer *renderer = context->display->renderer; + CoglRendererEGL *egl_renderer = renderer->winsys; + CoglRendererWayland *wayland_renderer = egl_renderer->platform; + CoglOnscreenEGL *egl_onscreen = onscreen->winsys; + CoglOnscreenWayland *wayland_onscreen; + int width, height; + + wayland_onscreen = g_slice_new0 (CoglOnscreenWayland); + egl_onscreen->platform = wayland_onscreen; + + width = cogl_framebuffer_get_width (framebuffer); + height = cogl_framebuffer_get_height (framebuffer); + + /* Create wl_surface */ + wayland_onscreen->wl_surface = + wl_compositor_create_surface (wayland_renderer->wl_compositor); + if (!wayland_onscreen->wl_surface) + { + g_set_error (error, COGL_WINSYS_ERROR, + COGL_WINSYS_ERROR_CREATE_ONSCREEN, + "Failed to create wl_surface"); + return FALSE; + } + + wayland_onscreen->wl_egl_window = + wl_egl_window_create (wayland_onscreen->wl_surface, width, height); + if (!wayland_onscreen->wl_egl_window) + { + g_set_error (error, COGL_WINSYS_ERROR, + COGL_WINSYS_ERROR_CREATE_ONSCREEN, + "Failed to create wl_egl_window"); + return FALSE; + } + + egl_onscreen->egl_surface = + eglCreateWindowSurface (egl_renderer->edpy, + egl_config, + (EGLNativeWindowType) wayland_onscreen->wl_egl_window, + NULL); + if (egl_onscreen->egl_surface == EGL_NO_SURFACE) + { + g_set_error (error, COGL_WINSYS_ERROR, + COGL_WINSYS_ERROR_CREATE_ONSCREEN, + "Failed to create EGL surface"); + return FALSE; + } + + return TRUE; +} + +static void +_cogl_winsys_egl_onscreen_deinit (CoglOnscreen *onscreen) +{ + CoglOnscreenEGL *egl_onscreen = onscreen->winsys; + CoglOnscreenWayland *wayland_onscreen = egl_onscreen->platform; + + if (wayland_onscreen->wl_egl_window) + { + wl_egl_window_destroy (wayland_onscreen->wl_egl_window); + wayland_onscreen->wl_egl_window = NULL; + } + + if (wayland_onscreen->wl_surface) + { + wl_surface_destroy (wayland_onscreen->wl_surface); + wayland_onscreen->wl_surface = NULL; + } + + g_slice_free (CoglOnscreenWayland, wayland_onscreen); +} + +static void +_cogl_winsys_onscreen_set_visibility (CoglOnscreen *onscreen, + gboolean visibility) +{ + /* For Wayland, visibility is controlled by buffer attachment */ +} + +/* Get the wl_surface for an onscreen */ +COGL_EXPORT struct wl_surface * +cogl_wayland_onscreen_get_wl_surface (CoglOnscreen *onscreen) +{ + CoglOnscreenEGL *egl_onscreen; + CoglOnscreenWayland *wayland_onscreen; + + g_return_val_if_fail (onscreen != NULL, NULL); + g_return_val_if_fail (onscreen->winsys != NULL, NULL); + + egl_onscreen = onscreen->winsys; + wayland_onscreen = egl_onscreen->platform; + + return wayland_onscreen->wl_surface; +} + +/* Resize an onscreen's wl_egl_window */ +COGL_EXPORT void +cogl_wayland_onscreen_resize (CoglOnscreen *onscreen, + int width, + int height, + int offset_x, + int offset_y) +{ + CoglOnscreenEGL *egl_onscreen; + CoglOnscreenWayland *wayland_onscreen; + + g_return_if_fail (onscreen != NULL); + g_return_if_fail (onscreen->winsys != NULL); + + egl_onscreen = onscreen->winsys; + wayland_onscreen = egl_onscreen->platform; + + if (wayland_onscreen->wl_egl_window) + { + wl_egl_window_resize (wayland_onscreen->wl_egl_window, + width, height, offset_x, offset_y); + } + + _cogl_framebuffer_winsys_update_size (COGL_FRAMEBUFFER (onscreen), + width, height); +} + +static const CoglWinsysEGLVtable +_cogl_winsys_egl_vtable = +{ + .add_config_attributes = _cogl_winsys_egl_add_config_attributes, + .choose_config = _cogl_winsys_egl_choose_config, + .display_setup = _cogl_winsys_egl_display_setup, + .display_destroy = _cogl_winsys_egl_display_destroy, + .context_created = _cogl_winsys_egl_context_created, + .cleanup_context = _cogl_winsys_egl_cleanup_context, + .onscreen_init = _cogl_winsys_egl_onscreen_init, + .onscreen_deinit = _cogl_winsys_egl_onscreen_deinit, +}; + +COGL_EXPORT const CoglWinsysVtable * +_cogl_winsys_egl_wayland_get_vtable (void) +{ + static gboolean vtable_inited = FALSE; + static CoglWinsysVtable vtable; + + if (!vtable_inited) + { + /* The EGL_WAYLAND winsys is a subclass of the EGL winsys so we + start by copying its vtable */ + vtable = *_cogl_winsys_egl_get_vtable (); + + vtable.id = COGL_WINSYS_ID_EGL_WAYLAND; + vtable.name = "EGL_WAYLAND"; + vtable.constraints |= COGL_RENDERER_CONSTRAINT_USES_EGL; + + vtable.renderer_connect = _cogl_winsys_renderer_connect; + vtable.renderer_disconnect = _cogl_winsys_renderer_disconnect; + + vtable.onscreen_set_visibility = _cogl_winsys_onscreen_set_visibility; + + vtable_inited = TRUE; + } + + return &vtable; +} diff --git a/cogl/meson.build b/cogl/meson.build index c85067c58..886cce324 100644 --- a/cogl/meson.build +++ b/cogl/meson.build @@ -41,6 +41,13 @@ if have_wayland ] endif +if have_wayland_client + cogl_pkg_deps += [ + wayland_client_dep, + wayland_egl_dep, + ] +endif + if have_egl cogl_pkg_deps += [ egl_dep, diff --git a/debian/libmuffin0.symbols b/debian/libmuffin0.symbols index 928320459..002e59011 100644 --- a/debian/libmuffin0.symbols +++ b/debian/libmuffin0.symbols @@ -587,7 +587,7 @@ libmuffin-clutter-0.so.0 libmuffin0 #MINVER# clutter_event_get_event_sequence@Base 5.3.0 clutter_event_get_flags@Base 5.3.0 clutter_event_get_gesture_motion_delta@Base 5.3.0 - clutter_event_get_gesture_motion_delta_unaccelerated@Base 6.7.0 + clutter_event_get_gesture_motion_delta_unaccelerated@Base 6.6.3 clutter_event_get_gesture_phase@Base 5.3.0 clutter_event_get_gesture_pinch_angle_delta@Base 5.3.0 clutter_event_get_gesture_pinch_scale@Base 5.3.0 @@ -878,7 +878,6 @@ libmuffin-clutter-0.so.0 libmuffin0 #MINVER# clutter_matrix_init_identity@Base 5.3.0 clutter_modifier_type_get_type@Base 5.3.0 clutter_offscreen_effect_create_texture@Base 5.3.0 - clutter_offscreen_effect_get_pipeline@Base 6.6.0 clutter_offscreen_effect_get_pipeline@Base 6.4.1 clutter_offscreen_effect_get_target@Base 5.3.0 clutter_offscreen_effect_get_target_rect@Base 5.3.0 @@ -1454,6 +1453,7 @@ libmuffin-cogl-0.so.0 libmuffin0 #MINVER# _cogl_winsys_egl_get_vtable@Base 5.3.0 _cogl_winsys_egl_make_current@Base 5.3.0 _cogl_winsys_egl_renderer_connect_common@Base 5.3.0 + _cogl_winsys_egl_wayland_get_vtable@Base 6.6.3 _cogl_winsys_egl_xlib_get_vtable@Base 5.3.0 _cogl_winsys_error_quark@Base 5.3.0 _cogl_winsys_glx_get_vtable@Base 5.3.0 @@ -1573,9 +1573,9 @@ libmuffin-cogl-0.so.0 libmuffin0 #MINVER# cogl_dma_buf_handle_get_width@Base 6.6.0 cogl_dma_buf_handle_mmap@Base 6.6.0 cogl_dma_buf_handle_munmap@Base 6.6.0 + cogl_dma_buf_handle_new@Base 5.3.0 cogl_dma_buf_handle_sync_read_end@Base 6.6.0 cogl_dma_buf_handle_sync_read_start@Base 6.6.0 - cogl_dma_buf_handle_new@Base 5.3.0 cogl_egl_context_get_egl_display@Base 5.3.0 cogl_egl_texture_2d_new_from_image@Base 5.3.0 cogl_fence_closure_get_user_data@Base 5.3.0 @@ -1979,6 +1979,10 @@ libmuffin-cogl-0.so.0 libmuffin0 #MINVER# cogl_texture_set_region@Base 5.3.0 cogl_texture_set_region_from_bitmap@Base 5.3.0 cogl_wayland_display_set_compositor_display@Base 6.0.0 + cogl_wayland_onscreen_get_wl_surface@Base 6.6.3 + cogl_wayland_onscreen_resize@Base 6.6.3 + cogl_wayland_renderer_get_display@Base 6.6.3 + cogl_wayland_renderer_set_foreign_display@Base 6.6.3 cogl_x11_onscreen_get_window_xid@Base 5.3.0 cogl_xlib_renderer_add_filter@Base 5.3.0 cogl_xlib_renderer_get_display@Base 5.3.0 @@ -2734,8 +2738,8 @@ libmuffin.so.0 libmuffin0 #MINVER# meta_workspace_get_work_area_all_monitors@Base 5.3.0 meta_workspace_get_work_area_for_monitor@Base 5.3.0 meta_workspace_index@Base 5.3.0 - meta_workspace_list_windows@Base 5.3.0 meta_workspace_list_unobscured_windows@Base 6.6.0 + meta_workspace_list_windows@Base 5.3.0 meta_workspace_manager_append_new_workspace@Base 5.3.0 meta_workspace_manager_get_active_workspace@Base 5.3.0 meta_workspace_manager_get_active_workspace_index@Base 5.3.0 diff --git a/meson.build b/meson.build index aebefe68f..b7975f6cc 100644 --- a/meson.build +++ b/meson.build @@ -171,6 +171,20 @@ if have_wayland endif endif +have_wayland_client = get_option('wayland_client') + +if have_wayland_client + if not have_wayland + # Need wayland dependencies even if server support is not enabled + wayland_client_dep = dependency('wayland-client', version: wayland_server_req) + wayland_egl_dep = dependency('wayland-egl') + endif + + if not have_egl + error('Wayland client support requires EGL to be enabled') + endif +endif + have_libgudev = get_option('udev') if have_libgudev libudev_dep = dependency('libudev', version: udev_req) @@ -355,6 +369,7 @@ cdata.set_quoted('PACKAGE_VERSION', meson.project_version()) cdata.set('HAVE_EGL', have_egl) cdata.set('HAVE_WAYLAND', have_wayland) +cdata.set('HAVE_WAYLAND_CLIENT', have_wayland_client) cdata.set('HAVE_NATIVE_BACKEND', have_native_backend) cdata.set('HAVE_REMOTE_DESKTOP', have_remote_desktop) cdata.set('HAVE_EGL_DEVICE', have_egl_device) @@ -500,6 +515,7 @@ output = [ ' Options:', '', ' Wayland.......................... ' + have_wayland.to_string(), + ' Wayland Client................... ' + have_wayland_client.to_string(), ' Wayland EGLStream................ ' + have_wayland_eglstream.to_string(), ' Native Backend................... ' + have_native_backend.to_string(), ' EGL Device....................... ' + have_egl_device.to_string(), diff --git a/meson_options.txt b/meson_options.txt index 9b6fb1fb8..97340f5a5 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -39,6 +39,12 @@ option('wayland', description: 'Enable Wayland support' ) +option('wayland_client', + type: 'boolean', + value: true, + description: 'Enable Wayland client backend (for running as a Wayland client)' +) + option('native_backend', type: 'boolean', value: true, diff --git a/src/backends/meta-cursor-sprite-xcursor.c b/src/backends/meta-cursor-sprite-xcursor.c index e6209fb93..134f02387 100644 --- a/src/backends/meta-cursor-sprite-xcursor.c +++ b/src/backends/meta-cursor-sprite-xcursor.c @@ -111,7 +111,7 @@ meta_cursor_get_name (MetaCursor cursor) case META_CURSOR_ROW_RESIZE: return "v_double_arrow"; case META_CURSOR_ALL_SCROLL: - return "left_ptr"; + return "all-scroll"; case META_CURSOR_ZOOM_IN: return "left_ptr"; case META_CURSOR_ZOOM_OUT: @@ -119,7 +119,7 @@ meta_cursor_get_name (MetaCursor cursor) case META_CURSOR_DND_ASK: return "dnd-copy"; case META_CURSOR_ALL_RESIZE: - return "dnd-move"; + return "fleur"; case META_CURSOR_INVALID: case META_CURSOR_NONE: break; @@ -197,7 +197,7 @@ meta_cursor_get_legacy_name (MetaCursor cursor) case META_CURSOR_ROW_RESIZE: return "v_double_arrow"; case META_CURSOR_ALL_SCROLL: - return "left_ptr"; + return "fleur"; case META_CURSOR_ZOOM_IN: return "left_ptr"; case META_CURSOR_ZOOM_OUT: @@ -205,7 +205,7 @@ meta_cursor_get_legacy_name (MetaCursor cursor) case META_CURSOR_DND_ASK: return "dnd-copy"; case META_CURSOR_ALL_RESIZE: - return "dnd-move"; + return "fleur"; case META_CURSOR_INVALID: case META_CURSOR_NONE: break; diff --git a/src/backends/meta-cursor-tracker.c b/src/backends/meta-cursor-tracker.c index b37697280..4b99998ba 100644 --- a/src/backends/meta-cursor-tracker.c +++ b/src/backends/meta-cursor-tracker.c @@ -219,11 +219,19 @@ set_window_cursor (MetaCursorTracker *tracker, gboolean has_cursor, MetaCursorSprite *cursor_sprite) { - g_clear_object (&tracker->window_cursor); + g_autoptr (MetaCursorSprite) old_cursor = g_steal_pointer (&tracker->window_cursor); + if (cursor_sprite) tracker->window_cursor = g_object_ref (cursor_sprite); tracker->has_window_cursor = has_cursor; sync_cursor (tracker); + + /* old_cursor is released here, after sync_cursor has realized the new + * cursor sprite's GBM bo. This ensures the old GBM bo (and its DRM + * handle) stays alive while the new bo is allocated, preventing the + * kernel from recycling the same DRM handle and skipping the cursor + * plane update. + */ } gboolean diff --git a/src/compositor/compositor.c b/src/compositor/compositor.c index 66af08546..11d606c38 100644 --- a/src/compositor/compositor.c +++ b/src/compositor/compositor.c @@ -998,8 +998,13 @@ sync_actor_stacking (MetaCompositor *compositor) children = clutter_actor_get_children (priv->bottom_window_group); for (tmp = children; tmp != NULL; tmp = tmp->next) { - MetaWindowActor *child = tmp->data; - MetaWindow *mw = meta_window_actor_get_meta_window (child); + ClutterActor *child = tmp->data; + MetaWindow *mw; + + if (!META_IS_WINDOW_ACTOR (child)) + continue; + + mw = meta_window_actor_get_meta_window (META_WINDOW_ACTOR (child)); if (mw != NULL) { diff --git a/src/compositor/meta-surface-actor.c b/src/compositor/meta-surface-actor.c index 6cc3a6795..5ee141ad1 100644 --- a/src/compositor/meta-surface-actor.c +++ b/src/compositor/meta-surface-actor.c @@ -94,7 +94,7 @@ get_scaled_region (MetaSurfaceActor *surface_actor, float x, y; window_actor = meta_window_actor_from_actor (CLUTTER_ACTOR (surface_actor)); - geometry_scale = meta_window_actor_get_geometry_scale (window_actor); + geometry_scale = window_actor ? meta_window_actor_get_geometry_scale (window_actor) : 1; clutter_actor_get_position (CLUTTER_ACTOR (surface_actor), &x, &y); cairo_region_translate (region, x, y); @@ -331,7 +331,7 @@ meta_surface_actor_is_untransformed (MetaCullable *cullable) clutter_actor_get_abs_allocation_vertices (actor, verts); window_actor = meta_window_actor_from_actor (actor); - geometry_scale = meta_window_actor_get_geometry_scale (window_actor); + geometry_scale = window_actor ? meta_window_actor_get_geometry_scale (window_actor) : 1; return meta_actor_vertices_are_untransformed (verts, width * geometry_scale, diff --git a/src/core/constraints.c b/src/core/constraints.c index 420d878fe..6a9dbc2b5 100644 --- a/src/core/constraints.c +++ b/src/core/constraints.c @@ -853,7 +853,7 @@ constrain_custom_rule (MetaWindow *window, return TRUE; parent = meta_window_get_transient_for (window); - if (window->placement.state == META_PLACEMENT_STATE_CONSTRAINED_FINISHED) + if (parent && window->placement.state == META_PLACEMENT_STATE_CONSTRAINED_FINISHED) { placement_rule->parent_rect.x = parent->rect.x; placement_rule->parent_rect.y = parent->rect.y; @@ -880,8 +880,8 @@ constrain_custom_rule (MetaWindow *window, case META_PLACEMENT_STATE_CONSTRAINED_FINISHED: case META_PLACEMENT_STATE_INVALIDATED: temporary_rect = (MetaRectangle) { - .x = parent->rect.x + window->placement.current.rel_x, - .y = parent->rect.y + window->placement.current.rel_y, + .x = (parent ? parent->rect.x : parent_x) + window->placement.current.rel_x, + .y = (parent ? parent->rect.y : parent_y) + window->placement.current.rel_y, .width = info->current.width, .height = info->current.height, }; diff --git a/src/core/display.c b/src/core/display.c index f715ff4ec..d313459c3 100644 --- a/src/core/display.c +++ b/src/core/display.c @@ -1693,7 +1693,7 @@ meta_cursor_for_grab_op (MetaGrabOp op) case META_GRAB_OP_MOVING: case META_GRAB_OP_KEYBOARD_MOVING: case META_GRAB_OP_KEYBOARD_RESIZING_UNKNOWN: - return META_CURSOR_MOVE; + return META_CURSOR_ALL_RESIZE; break; default: break; diff --git a/src/core/frame.c b/src/core/frame.c index d1047c84c..bf73e0ab3 100644 --- a/src/core/frame.c +++ b/src/core/frame.c @@ -28,6 +28,7 @@ #include "backends/x11/meta-backend-x11.h" #include "core/bell.h" #include "core/keybindings-private.h" +#include "meta/display.h" #include "meta/meta-x11-errors.h" #include "x11/meta-x11-display-private.h" @@ -423,6 +424,9 @@ meta_frame_set_screen_cursor (MetaFrame *frame, XFlush (x11_display->xdisplay); XFreeCursor (x11_display->xdisplay, xcursor); } + + if (meta_is_wayland_compositor ()) + meta_display_set_cursor (frame->window->display, cursor); } Window diff --git a/src/core/window.c b/src/core/window.c index 09f920845..b16862032 100644 --- a/src/core/window.c +++ b/src/core/window.c @@ -1820,17 +1820,23 @@ gboolean meta_window_should_be_showing (MetaWindow *window) { MetaWorkspaceManager *workspace_manager = window->display->workspace_manager; + gboolean has_buffer, on_workspace, showing_on_workspace; #ifdef HAVE_WAYLAND if (window->client_type == META_WINDOW_CLIENT_TYPE_WAYLAND && !meta_wayland_surface_get_buffer (window->surface)) return FALSE; + has_buffer = TRUE; +#else + has_buffer = TRUE; #endif + on_workspace = meta_window_located_on_workspace (window, workspace_manager->active_workspace); + showing_on_workspace = meta_window_showing_on_its_workspace (window); + /* Windows should be showing if they're located on the * active workspace and they're showing on their own workspace. */ - return (meta_window_located_on_workspace (window, workspace_manager->active_workspace) && - meta_window_showing_on_its_workspace (window)); + return (on_workspace && showing_on_workspace); } static void @@ -4542,6 +4548,25 @@ meta_window_move_resize_internal (MetaWindow *window, constrained_rect = unconstrained_rect; temporary_rect = window->rect; + + /* If the window doesn't have a monitor yet but has a placement rule + * (e.g., for popups from layer-shell surfaces), try to determine the + * monitor from the placement rule's parent rect so constraints can be applied. */ + if (!window->monitor && window->placement.rule) + { + MetaBackend *backend = meta_get_backend (); + MetaMonitorManager *monitor_manager = + meta_backend_get_monitor_manager (backend); + MetaLogicalMonitor *logical_monitor; + MetaRectangle *parent_rect = &window->placement.rule->parent_rect; + + logical_monitor = + meta_monitor_manager_get_logical_monitor_from_rect (monitor_manager, + parent_rect); + if (logical_monitor) + window->monitor = logical_monitor; + } + if (flags & (META_MOVE_RESIZE_MOVE_ACTION | META_MOVE_RESIZE_RESIZE_ACTION) && !(flags & META_MOVE_RESIZE_WAYLAND_FINISH_MOVE_RESIZE) && window->monitor) diff --git a/src/core/workspace-private.h b/src/core/workspace-private.h index a58b2347d..606b6e8c7 100644 --- a/src/core/workspace-private.h +++ b/src/core/workspace-private.h @@ -62,6 +62,7 @@ struct _MetaWorkspace GList *screen_edges; GList *monitor_edges; GSList *builtin_struts; + GSList *layer_shell_struts; GSList *all_struts; guint work_areas_invalid : 1; @@ -88,6 +89,14 @@ void meta_workspace_get_work_area_for_logical_monitor (MetaWorkspace *works void meta_workspace_invalidate_work_area (MetaWorkspace *workspace); +void meta_workspace_set_layer_shell_struts (MetaWorkspace *workspace, + GSList *struts); + +void meta_workspace_get_work_area_for_logical_monitor_excluding_layer_shell ( + MetaWorkspace *workspace, + MetaLogicalMonitor *logical_monitor, + MetaRectangle *area); + GList* meta_workspace_get_onscreen_region (MetaWorkspace *workspace); GList * meta_workspace_get_onmonitor_region (MetaWorkspace *workspace, MetaLogicalMonitor *logical_monitor); diff --git a/src/core/workspace.c b/src/core/workspace.c index ffea1ab8f..371ccc3de 100644 --- a/src/core/workspace.c +++ b/src/core/workspace.c @@ -302,6 +302,22 @@ workspace_free_builtin_struts (MetaWorkspace *workspace) workspace->builtin_struts = NULL; } +/** + * workspace_free_layer_shell_struts: + * @workspace: The workspace. + * + * Frees the struts list set with meta_workspace_set_layer_shell_struts + */ +static void +workspace_free_layer_shell_struts (MetaWorkspace *workspace) +{ + if (workspace->layer_shell_struts == NULL) + return; + + g_slist_free_full (workspace->layer_shell_struts, g_free); + workspace->layer_shell_struts = NULL; +} + /* Ensure that the workspace is empty by making sure that * all of our windows are on-all-workspaces. */ static void @@ -333,6 +349,7 @@ meta_workspace_remove (MetaWorkspace *workspace) g_list_free (workspace->list_containing_self); workspace_free_builtin_struts (workspace); + workspace_free_layer_shell_struts (workspace); /* screen.c:update_num_workspaces(), which calls us, removes windows from * workspaces first, which can cause the workareas on the workspace to be @@ -899,6 +916,16 @@ ensure_work_areas_validated (MetaWorkspace *workspace) workspace->all_struts = copy_strut_list (workspace->builtin_struts); + /* Add layer-shell struts */ + { + GSList *s_iter; + for (s_iter = workspace->layer_shell_struts; s_iter != NULL; s_iter = s_iter->next) + { + workspace->all_struts = g_slist_prepend (workspace->all_struts, + copy_strut (s_iter->data)); + } + } + windows = meta_workspace_list_windows (workspace); for (tmp = windows; tmp != NULL; tmp = tmp->next) { @@ -1161,6 +1188,28 @@ meta_workspace_set_builtin_struts (MetaWorkspace *workspace, meta_workspace_invalidate_work_area (workspace); } +/** + * meta_workspace_set_layer_shell_struts: + * @workspace: a #MetaWorkspace + * @struts: (element-type Meta.Strut) (transfer none): list of #MetaStrut + * + * Sets a list of struts from layer-shell surfaces that will be used + * in addition to the builtin struts and window struts when computing + * the work area of the workspace. + */ +void +meta_workspace_set_layer_shell_struts (MetaWorkspace *workspace, + GSList *struts) +{ + if (strut_lists_equal (struts, workspace->layer_shell_struts)) + return; + + workspace_free_layer_shell_struts (workspace); + workspace->layer_shell_struts = copy_strut_list (struts); + + meta_workspace_invalidate_work_area (workspace); +} + void meta_workspace_get_work_area_for_logical_monitor (MetaWorkspace *workspace, MetaLogicalMonitor *logical_monitor, @@ -1171,6 +1220,56 @@ meta_workspace_get_work_area_for_logical_monitor (MetaWorkspace *workspace, area); } +/** + * meta_workspace_get_work_area_for_logical_monitor_excluding_layer_shell: + * @workspace: a #MetaWorkspace + * @logical_monitor: a #MetaLogicalMonitor + * @area: (out): location to store the work area + * + * Computes work area for @logical_monitor using only builtin struts, + * excluding layer-shell struts. This is used for positioning layer-shell + * surfaces so they don't get pushed by their own struts. + */ +void +meta_workspace_get_work_area_for_logical_monitor_excluding_layer_shell ( + MetaWorkspace *workspace, + MetaLogicalMonitor *logical_monitor, + MetaRectangle *area) +{ + GList *tmp; + GList *monitor_region; + GSList *struts_for_calculation = NULL; + GSList *s_iter; + + g_return_if_fail (logical_monitor != NULL); + g_return_if_fail (area != NULL); + + /* Start with builtin struts only */ + for (s_iter = workspace->builtin_struts; s_iter != NULL; s_iter = s_iter->next) + { + MetaStrut *strut = s_iter->data; + MetaStrut *copy = g_new0 (MetaStrut, 1); + *copy = *strut; + struts_for_calculation = g_slist_prepend (struts_for_calculation, copy); + } + + /* Calculate region for this monitor using only builtin struts */ + monitor_region = + meta_rectangle_get_minimal_spanning_set_for_region (&logical_monitor->rect, + struts_for_calculation); + + /* Find work area from the region */ + *area = logical_monitor->rect; + for (tmp = monitor_region; tmp != NULL; tmp = tmp->next) + { + MetaRectangle *rect = tmp->data; + meta_rectangle_intersect (area, rect, area); + } + + g_list_free_full (monitor_region, g_free); + g_slist_free_full (struts_for_calculation, g_free); +} + /** * meta_workspace_get_work_area_for_monitor: * @workspace: a #MetaWorkspace diff --git a/src/meson.build b/src/meson.build index e40d74c37..bb44618a3 100644 --- a/src/meson.build +++ b/src/meson.build @@ -518,8 +518,12 @@ if have_wayland 'wayland/meta-wayland-dma-buf.h', 'wayland/meta-wayland-dnd-surface.c', 'wayland/meta-wayland-dnd-surface.h', + 'wayland/meta-wayland-foreign-toplevel.c', + 'wayland/meta-wayland-foreign-toplevel.h', 'wayland/meta-wayland-gtk-shell.c', 'wayland/meta-wayland-gtk-shell.h', + 'wayland/meta-wayland-layer-shell.c', + 'wayland/meta-wayland-layer-shell.h', 'wayland/meta-wayland.h', 'wayland/meta-wayland-idle-inhibit.c', 'wayland/meta-wayland-idle-inhibit.h', @@ -808,6 +812,8 @@ if have_wayland wayland_protocols = [ ['cursor-shape-v1', 'private', ], ['gtk-shell', 'private', ], + ['wlr-foreign-toplevel-management-unstable-v1', 'private', ], + ['wlr-layer-shell-unstable-v1', 'private', ], ['idle-inhibit', 'unstable', 'v1', ], ['keyboard-shortcuts-inhibit', 'unstable', 'v1', ], ['linux-dmabuf', 'unstable', 'v1', ], diff --git a/src/wayland/meta-wayland-actor-surface.c b/src/wayland/meta-wayland-actor-surface.c index bf92b7c00..3b5f12756 100644 --- a/src/wayland/meta-wayland-actor-surface.c +++ b/src/wayland/meta-wayland-actor-surface.c @@ -182,6 +182,7 @@ meta_wayland_actor_surface_real_sync_actor_state (MetaWaylandActorSurface *actor stex = meta_surface_actor_get_texture (surface_actor); buffer = surface->buffer_ref.buffer; + if (buffer) { CoglSnippet *snippet; diff --git a/src/wayland/meta-wayland-foreign-toplevel.c b/src/wayland/meta-wayland-foreign-toplevel.c new file mode 100644 index 000000000..3136a2d93 --- /dev/null +++ b/src/wayland/meta-wayland-foreign-toplevel.c @@ -0,0 +1,939 @@ +/* + * Copyright (C) 2024 Linux Mint + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "wayland/meta-wayland-foreign-toplevel.h" + +#include "backends/meta-backend-private.h" +#include "backends/meta-logical-monitor.h" +#include "backends/meta-monitor-manager-private.h" +#include "core/display-private.h" +#include "core/window-private.h" +#include "wayland/meta-wayland-outputs.h" +#include "wayland/meta-wayland-private.h" +#include "wayland/meta-wayland-versions.h" + +#include "wlr-foreign-toplevel-management-unstable-v1-server-protocol.h" + +typedef struct _MetaForeignToplevelManager MetaForeignToplevelManager; +typedef struct _MetaForeignToplevelHandle MetaForeignToplevelHandle; + +struct _MetaForeignToplevelManager +{ + MetaWaylandCompositor *compositor; + GList *manager_resources; + GList *handles; + + gulong window_created_handler_id; + gulong window_entered_monitor_handler_id; + gulong window_left_monitor_handler_id; +}; + +struct _MetaForeignToplevelHandle +{ + MetaForeignToplevelManager *manager; + MetaWindow *window; + GList *handle_resources; + + gulong title_handler_id; + gulong wm_class_handler_id; + gulong minimized_handler_id; + gulong maximized_h_handler_id; + gulong maximized_v_handler_id; + gulong fullscreen_handler_id; + gulong appears_focused_handler_id; + gulong skip_taskbar_handler_id; + gulong unmanaging_handler_id; + + gboolean closed; +}; + +static struct wl_resource * +find_output_resource_for_client (MetaWaylandOutput *wayland_output, + struct wl_client *client) +{ + GList *l; + + if (!wayland_output) + return NULL; + + for (l = wayland_output->resources; l; l = l->next) + { + struct wl_resource *resource = l->data; + + if (wl_resource_get_client (resource) == client) + return resource; + } + + return NULL; +} + +static MetaWaylandOutput * +find_wayland_output_for_monitor (MetaWaylandCompositor *compositor, + int monitor_index) +{ + MetaBackend *backend = meta_get_backend (); + MetaMonitorManager *monitor_manager = meta_backend_get_monitor_manager (backend); + GList *logical_monitors; + GList *l; + + logical_monitors = meta_monitor_manager_get_logical_monitors (monitor_manager); + + for (l = logical_monitors; l; l = l->next) + { + MetaLogicalMonitor *logical_monitor = l->data; + + if (logical_monitor->number == monitor_index) + { + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, compositor->outputs); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + MetaWaylandOutput *wayland_output = value; + + if (wayland_output->logical_monitor == logical_monitor) + return wayland_output; + } + + break; + } + } + + return NULL; +} + +static void +send_state_to_resource (struct wl_resource *resource, + MetaForeignToplevelHandle *handle) +{ + MetaWindow *window = handle->window; + struct wl_array states; + uint32_t *s; + + wl_array_init (&states); + + if (META_WINDOW_MAXIMIZED (window)) + { + s = wl_array_add (&states, sizeof (uint32_t)); + *s = ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED; + } + + if (window->minimized) + { + s = wl_array_add (&states, sizeof (uint32_t)); + *s = ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED; + } + + if (meta_window_appears_focused (window)) + { + s = wl_array_add (&states, sizeof (uint32_t)); + *s = ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED; + } + + if (window->fullscreen) + { + s = wl_array_add (&states, sizeof (uint32_t)); + *s = ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN; + } + + zwlr_foreign_toplevel_handle_v1_send_state (resource, &states); + wl_array_release (&states); +} + +static void +send_title_to_resource (struct wl_resource *resource, + MetaWindow *window) +{ + const char *title = meta_window_get_title (window); + + if (title) + zwlr_foreign_toplevel_handle_v1_send_title (resource, title); +} + +static void +send_app_id_to_resource (struct wl_resource *resource, + MetaWindow *window) +{ + const char *wm_class = meta_window_get_wm_class (window); + const char *sandboxed_app_id; + + sandboxed_app_id = meta_window_get_sandboxed_app_id (window); + if (sandboxed_app_id) + zwlr_foreign_toplevel_handle_v1_send_app_id (resource, sandboxed_app_id); + else if (wm_class) + zwlr_foreign_toplevel_handle_v1_send_app_id (resource, wm_class); +} + +static void +send_done_to_resource (struct wl_resource *resource) +{ + zwlr_foreign_toplevel_handle_v1_send_done (resource); +} + +static void +send_output_enter (MetaForeignToplevelHandle *handle, + int monitor_index) +{ + MetaWaylandOutput *wayland_output; + GList *l; + + wayland_output = find_wayland_output_for_monitor (handle->manager->compositor, + monitor_index); + if (!wayland_output) + return; + + for (l = handle->handle_resources; l; l = l->next) + { + struct wl_resource *handle_resource = l->data; + struct wl_client *client = wl_resource_get_client (handle_resource); + struct wl_resource *output_resource; + + output_resource = find_output_resource_for_client (wayland_output, client); + if (output_resource) + zwlr_foreign_toplevel_handle_v1_send_output_enter (handle_resource, + output_resource); + } +} + +static void +send_output_leave (MetaForeignToplevelHandle *handle, + int monitor_index) +{ + MetaWaylandOutput *wayland_output; + GList *l; + + wayland_output = find_wayland_output_for_monitor (handle->manager->compositor, + monitor_index); + if (!wayland_output) + return; + + for (l = handle->handle_resources; l; l = l->next) + { + struct wl_resource *handle_resource = l->data; + struct wl_client *client = wl_resource_get_client (handle_resource); + struct wl_resource *output_resource; + + output_resource = find_output_resource_for_client (wayland_output, client); + if (output_resource) + zwlr_foreign_toplevel_handle_v1_send_output_leave (handle_resource, + output_resource); + } +} + +static void +on_title_changed (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + MetaForeignToplevelHandle *handle = user_data; + GList *l; + + if (handle->closed) + return; + + for (l = handle->handle_resources; l; l = l->next) + { + send_title_to_resource (l->data, handle->window); + send_done_to_resource (l->data); + } +} + +static void +on_wm_class_changed (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + MetaForeignToplevelHandle *handle = user_data; + GList *l; + + if (handle->closed) + return; + + for (l = handle->handle_resources; l; l = l->next) + { + send_app_id_to_resource (l->data, handle->window); + send_done_to_resource (l->data); + } +} + +static void +on_state_changed (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + MetaForeignToplevelHandle *handle = user_data; + GList *l; + + if (handle->closed) + return; + + for (l = handle->handle_resources; l; l = l->next) + { + send_state_to_resource (l->data, handle); + send_done_to_resource (l->data); + } +} + +static void +on_skip_taskbar_changed (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + MetaForeignToplevelHandle *handle = user_data; + MetaWindow *window = handle->window; + + if (handle->closed) + return; + + if (meta_window_is_skip_taskbar (window)) + { + GList *l; + + for (l = handle->handle_resources; l; l = l->next) + zwlr_foreign_toplevel_handle_v1_send_closed (l->data); + + handle->closed = TRUE; + } +} + +static void handle_destroy (MetaForeignToplevelHandle *handle); + +static void +on_unmanaging (MetaWindow *window, + gpointer user_data) +{ + MetaForeignToplevelHandle *handle = user_data; + GList *l; + + if (!handle->closed) + { + for (l = handle->handle_resources; l; l = l->next) + zwlr_foreign_toplevel_handle_v1_send_closed (l->data); + + handle->closed = TRUE; + } + + handle_destroy (handle); +} + +static void +disconnect_window_signals (MetaForeignToplevelHandle *handle) +{ + if (handle->window == NULL) + return; + + if (handle->title_handler_id) + g_signal_handler_disconnect (handle->window, handle->title_handler_id); + if (handle->wm_class_handler_id) + g_signal_handler_disconnect (handle->window, handle->wm_class_handler_id); + if (handle->minimized_handler_id) + g_signal_handler_disconnect (handle->window, handle->minimized_handler_id); + if (handle->maximized_h_handler_id) + g_signal_handler_disconnect (handle->window, handle->maximized_h_handler_id); + if (handle->maximized_v_handler_id) + g_signal_handler_disconnect (handle->window, handle->maximized_v_handler_id); + if (handle->fullscreen_handler_id) + g_signal_handler_disconnect (handle->window, handle->fullscreen_handler_id); + if (handle->appears_focused_handler_id) + g_signal_handler_disconnect (handle->window, handle->appears_focused_handler_id); + if (handle->skip_taskbar_handler_id) + g_signal_handler_disconnect (handle->window, handle->skip_taskbar_handler_id); + if (handle->unmanaging_handler_id) + g_signal_handler_disconnect (handle->window, handle->unmanaging_handler_id); + + handle->title_handler_id = 0; + handle->wm_class_handler_id = 0; + handle->minimized_handler_id = 0; + handle->maximized_h_handler_id = 0; + handle->maximized_v_handler_id = 0; + handle->fullscreen_handler_id = 0; + handle->appears_focused_handler_id = 0; + handle->skip_taskbar_handler_id = 0; + handle->unmanaging_handler_id = 0; + handle->window = NULL; +} + +static void +handle_destroy (MetaForeignToplevelHandle *handle) +{ + GList *l; + + disconnect_window_signals (handle); + + for (l = handle->handle_resources; l; l = l->next) + wl_resource_set_user_data (l->data, NULL); + + g_list_free (handle->handle_resources); + handle->handle_resources = NULL; + + if (handle->manager) + { + handle->manager->handles = g_list_remove (handle->manager->handles, + handle); + } + + g_free (handle); +} + +/* Handle requests from clients */ +static void +handle_set_maximized (struct wl_client *client, + struct wl_resource *resource) +{ + MetaForeignToplevelHandle *handle = wl_resource_get_user_data (resource); + + if (!handle || !handle->window) + return; + + if (!meta_window_can_maximize (handle->window)) + return; + + meta_window_maximize (handle->window, META_MAXIMIZE_BOTH); +} + +static void +handle_unset_maximized (struct wl_client *client, + struct wl_resource *resource) +{ + MetaForeignToplevelHandle *handle = wl_resource_get_user_data (resource); + + if (!handle || !handle->window) + return; + + meta_window_unmaximize (handle->window, META_MAXIMIZE_BOTH); +} + +static void +handle_set_minimized (struct wl_client *client, + struct wl_resource *resource) +{ + MetaForeignToplevelHandle *handle = wl_resource_get_user_data (resource); + + if (!handle || !handle->window) + return; + + if (!meta_window_can_minimize (handle->window)) + return; + + meta_window_minimize (handle->window); +} + +static void +handle_unset_minimized (struct wl_client *client, + struct wl_resource *resource) +{ + MetaForeignToplevelHandle *handle = wl_resource_get_user_data (resource); + + if (!handle || !handle->window) + return; + + meta_window_unminimize (handle->window); +} + +static void +handle_activate (struct wl_client *client, + struct wl_resource *resource, + struct wl_resource *seat_resource) +{ + MetaForeignToplevelHandle *handle = wl_resource_get_user_data (resource); + uint32_t timestamp; + + if (!handle || !handle->window) + return; + + timestamp = meta_display_get_current_time_roundtrip (meta_get_display ()); + meta_window_activate (handle->window, timestamp); +} + +static void +handle_close (struct wl_client *client, + struct wl_resource *resource) +{ + MetaForeignToplevelHandle *handle = wl_resource_get_user_data (resource); + + if (!handle || !handle->window) + return; + + if (!meta_window_can_close (handle->window)) + return; + + meta_window_delete (handle->window, META_CURRENT_TIME); +} + +static void +handle_set_rectangle (struct wl_client *client, + struct wl_resource *resource, + struct wl_resource *surface_resource, + int32_t x, + int32_t y, + int32_t width, + int32_t height) +{ + MetaForeignToplevelHandle *handle = wl_resource_get_user_data (resource); + + if (!handle || !handle->window) + return; + + if (width > 0 && height > 0) + { + MetaRectangle rect = { x, y, width, height }; + meta_window_set_icon_geometry (handle->window, &rect); + } + else + { + meta_window_set_icon_geometry (handle->window, NULL); + } +} + +static void +handle_resource_destroy (struct wl_client *client, + struct wl_resource *resource) +{ + wl_resource_destroy (resource); +} + +static void +handle_set_fullscreen (struct wl_client *client, + struct wl_resource *resource, + struct wl_resource *output_resource) +{ + MetaForeignToplevelHandle *handle = wl_resource_get_user_data (resource); + + if (!handle || !handle->window) + return; + + if (!handle->window->has_fullscreen_func) + return; + + meta_window_make_fullscreen (handle->window); +} + +static void +handle_unset_fullscreen (struct wl_client *client, + struct wl_resource *resource) +{ + MetaForeignToplevelHandle *handle = wl_resource_get_user_data (resource); + + if (!handle || !handle->window) + return; + + meta_window_unmake_fullscreen (handle->window); +} + +static const struct zwlr_foreign_toplevel_handle_v1_interface toplevel_handle_interface = { + handle_set_maximized, + handle_unset_maximized, + handle_set_minimized, + handle_unset_minimized, + handle_activate, + handle_close, + handle_set_rectangle, + handle_resource_destroy, + handle_set_fullscreen, + handle_unset_fullscreen, +}; + +static void +handle_resource_destroyed (struct wl_resource *resource) +{ + MetaForeignToplevelHandle *handle = wl_resource_get_user_data (resource); + + if (!handle) + return; + + handle->handle_resources = g_list_remove (handle->handle_resources, + resource); + + if (handle->handle_resources == NULL && handle->closed) + handle_destroy (handle); +} + +static MetaForeignToplevelHandle * +find_handle_for_window (MetaForeignToplevelManager *manager, + MetaWindow *window) +{ + GList *l; + + for (l = manager->handles; l; l = l->next) + { + MetaForeignToplevelHandle *handle = l->data; + + if (handle->window == window) + return handle; + } + + return NULL; +} + +static void +send_initial_state_for_resource (struct wl_resource *handle_resource, + MetaForeignToplevelHandle *handle) +{ + MetaWindow *window = handle->window; + MetaWaylandOutput *wayland_output; + struct wl_client *client; + struct wl_resource *output_resource; + int monitor; + + send_title_to_resource (handle_resource, window); + send_app_id_to_resource (handle_resource, window); + send_state_to_resource (handle_resource, handle); + + client = wl_resource_get_client (handle_resource); + monitor = meta_window_get_monitor (window); + if (monitor >= 0) + { + wayland_output = find_wayland_output_for_monitor (handle->manager->compositor, + monitor); + if (wayland_output) + { + output_resource = find_output_resource_for_client (wayland_output, + client); + if (output_resource) + zwlr_foreign_toplevel_handle_v1_send_output_enter (handle_resource, + output_resource); + } + } + + /* Send parent if version >= 3 */ + if (wl_resource_get_version (handle_resource) >= 3) + { + MetaWindow *transient_for = meta_window_get_transient_for (window); + + if (transient_for) + { + MetaForeignToplevelHandle *parent_handle; + parent_handle = find_handle_for_window (handle->manager, transient_for); + + if (parent_handle && parent_handle->handle_resources) + { + struct wl_resource *parent_resource = NULL; + GList *pl; + + for (pl = parent_handle->handle_resources; pl; pl = pl->next) + { + if (wl_resource_get_client (pl->data) == client) + { + parent_resource = pl->data; + break; + } + } + + if (parent_resource) + { + zwlr_foreign_toplevel_handle_v1_send_parent (handle_resource, + parent_resource); + } + } + } + else + { + zwlr_foreign_toplevel_handle_v1_send_parent (handle_resource, NULL); + } + } + + send_done_to_resource (handle_resource); +} + +static MetaForeignToplevelHandle * +create_handle_for_window (MetaForeignToplevelManager *manager, + MetaWindow *window) +{ + MetaForeignToplevelHandle *handle; + + handle = g_new0 (MetaForeignToplevelHandle, 1); + handle->manager = manager; + handle->window = window; + handle->closed = FALSE; + + handle->title_handler_id = + g_signal_connect (window, "notify::title", + G_CALLBACK (on_title_changed), handle); + handle->wm_class_handler_id = + g_signal_connect (window, "notify::wm-class", + G_CALLBACK (on_wm_class_changed), handle); + handle->minimized_handler_id = + g_signal_connect (window, "notify::minimized", + G_CALLBACK (on_state_changed), handle); + handle->maximized_h_handler_id = + g_signal_connect (window, "notify::maximized-horizontally", + G_CALLBACK (on_state_changed), handle); + handle->maximized_v_handler_id = + g_signal_connect (window, "notify::maximized-vertically", + G_CALLBACK (on_state_changed), handle); + handle->fullscreen_handler_id = + g_signal_connect (window, "notify::fullscreen", + G_CALLBACK (on_state_changed), handle); + handle->appears_focused_handler_id = + g_signal_connect (window, "notify::appears-focused", + G_CALLBACK (on_state_changed), handle); + handle->skip_taskbar_handler_id = + g_signal_connect (window, "notify::skip-taskbar", + G_CALLBACK (on_skip_taskbar_changed), handle); + handle->unmanaging_handler_id = + g_signal_connect (window, "unmanaging", + G_CALLBACK (on_unmanaging), handle); + + manager->handles = g_list_prepend (manager->handles, handle); + + return handle; +} + +static gboolean +should_expose_window (MetaWindow *window) +{ + MetaWindowType type = meta_window_get_window_type (window); + + if (meta_window_is_skip_taskbar (window)) + return FALSE; + + if (meta_window_is_override_redirect (window)) + return FALSE; + + switch (type) + { + case META_WINDOW_NORMAL: + case META_WINDOW_DIALOG: + case META_WINDOW_MODAL_DIALOG: + case META_WINDOW_UTILITY: + return TRUE; + default: + return FALSE; + } +} + +static void +create_handle_resource_for_manager_resource (MetaForeignToplevelHandle *handle, + struct wl_resource *manager_resource) +{ + struct wl_client *client = wl_resource_get_client (manager_resource); + uint32_t version = wl_resource_get_version (manager_resource); + struct wl_resource *handle_resource; + + handle_resource = wl_resource_create (client, + &zwlr_foreign_toplevel_handle_v1_interface, + version, 0); + + wl_resource_set_implementation (handle_resource, + &toplevel_handle_interface, + handle, + handle_resource_destroyed); + + handle->handle_resources = g_list_prepend (handle->handle_resources, + handle_resource); + + zwlr_foreign_toplevel_manager_v1_send_toplevel (manager_resource, + handle_resource); + send_initial_state_for_resource (handle_resource, handle); +} + +static void +on_window_created (MetaDisplay *display, + MetaWindow *window, + gpointer user_data) +{ + MetaForeignToplevelManager *manager = user_data; + MetaForeignToplevelHandle *handle; + GList *l; + + if (!should_expose_window (window)) + return; + + handle = create_handle_for_window (manager, window); + + for (l = manager->manager_resources; l; l = l->next) + create_handle_resource_for_manager_resource (handle, l->data); +} + +static void +on_window_entered_monitor (MetaDisplay *display, + int monitor_index, + MetaWindow *window, + gpointer user_data) +{ + MetaForeignToplevelManager *manager = user_data; + MetaForeignToplevelHandle *handle; + + handle = find_handle_for_window (manager, window); + if (!handle || handle->closed) + return; + + send_output_enter (handle, monitor_index); + + { + GList *l; + for (l = handle->handle_resources; l; l = l->next) + send_done_to_resource (l->data); + } +} + +static void +on_window_left_monitor (MetaDisplay *display, + int monitor_index, + MetaWindow *window, + gpointer user_data) +{ + MetaForeignToplevelManager *manager = user_data; + MetaForeignToplevelHandle *handle; + + handle = find_handle_for_window (manager, window); + if (!handle || handle->closed) + return; + + send_output_leave (handle, monitor_index); + + { + GList *l; + for (l = handle->handle_resources; l; l = l->next) + send_done_to_resource (l->data); + } +} + +/* Manager protocol requests */ +static void +manager_stop (struct wl_client *client, + struct wl_resource *resource) +{ + zwlr_foreign_toplevel_manager_v1_send_finished (resource); +} + +static const struct zwlr_foreign_toplevel_manager_v1_interface manager_interface = { + manager_stop, +}; + +static void +manager_resource_destroyed (struct wl_resource *resource) +{ + MetaForeignToplevelManager *manager = wl_resource_get_user_data (resource); + + if (manager) + { + manager->manager_resources = g_list_remove (manager->manager_resources, + resource); + } +} + +static void +send_existing_windows (MetaForeignToplevelManager *manager, + struct wl_resource *manager_resource) +{ + MetaDisplay *display = meta_get_display (); + GSList *windows; + GSList *l; + + if (!display) + return; + + windows = meta_display_list_windows (display, META_LIST_DEFAULT); + + for (l = windows; l; l = l->next) + { + MetaWindow *window = l->data; + MetaForeignToplevelHandle *handle; + + if (!should_expose_window (window)) + continue; + + handle = find_handle_for_window (manager, window); + if (!handle) + handle = create_handle_for_window (manager, window); + + create_handle_resource_for_manager_resource (handle, manager_resource); + } + + g_slist_free (windows); +} + +static void +ensure_display_signals_connected (MetaForeignToplevelManager *manager) +{ + MetaDisplay *display; + + if (manager->window_created_handler_id != 0) + return; + + display = meta_get_display (); + if (!display) + return; + + manager->window_created_handler_id = + g_signal_connect (display, "window-created", + G_CALLBACK (on_window_created), manager); + manager->window_entered_monitor_handler_id = + g_signal_connect (display, "window-entered-monitor", + G_CALLBACK (on_window_entered_monitor), manager); + manager->window_left_monitor_handler_id = + g_signal_connect (display, "window-left-monitor", + G_CALLBACK (on_window_left_monitor), manager); +} + +static void +bind_manager (struct wl_client *client, + void *data, + uint32_t version, + uint32_t id) +{ + MetaForeignToplevelManager *manager = data; + struct wl_resource *resource; + + resource = wl_resource_create (client, + &zwlr_foreign_toplevel_manager_v1_interface, + version, id); + + wl_resource_set_implementation (resource, + &manager_interface, + manager, + manager_resource_destroyed); + + manager->manager_resources = g_list_prepend (manager->manager_resources, + resource); + + ensure_display_signals_connected (manager); + send_existing_windows (manager, resource); +} + +void +meta_wayland_init_foreign_toplevel (MetaWaylandCompositor *compositor) +{ + MetaForeignToplevelManager *manager; + + manager = g_new0 (MetaForeignToplevelManager, 1); + manager->compositor = compositor; + + if (wl_global_create (compositor->wayland_display, + &zwlr_foreign_toplevel_manager_v1_interface, + META_ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_VERSION, + manager, bind_manager) == NULL) + { + g_warning ("Failed to register zwlr_foreign_toplevel_manager_v1 global"); + g_free (manager); + return; + } + + ensure_display_signals_connected (manager); + + g_object_set_data (G_OBJECT (compositor), "-meta-wayland-foreign-toplevel", + manager); + + g_debug ("Foreign toplevel management protocol initialized (version %d)", + META_ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_VERSION); +} diff --git a/src/wayland/meta-wayland-foreign-toplevel.h b/src/wayland/meta-wayland-foreign-toplevel.h new file mode 100644 index 000000000..60f3dd683 --- /dev/null +++ b/src/wayland/meta-wayland-foreign-toplevel.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 Linux Mint + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#ifndef META_WAYLAND_FOREIGN_TOPLEVEL_H +#define META_WAYLAND_FOREIGN_TOPLEVEL_H + +#include "wayland/meta-wayland.h" + +void meta_wayland_init_foreign_toplevel (MetaWaylandCompositor *compositor); + +#endif /* META_WAYLAND_FOREIGN_TOPLEVEL_H */ diff --git a/src/wayland/meta-wayland-layer-shell.c b/src/wayland/meta-wayland-layer-shell.c new file mode 100644 index 000000000..4873f72c2 --- /dev/null +++ b/src/wayland/meta-wayland-layer-shell.c @@ -0,0 +1,1406 @@ +/* + * Copyright (C) 2024 Linux Mint + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "wayland/meta-wayland-layer-shell.h" + +#include "backends/meta-logical-monitor.h" +#include "compositor/compositor-private.h" +#include "compositor/meta-surface-actor-wayland.h" +#include "core/meta-workspace-manager-private.h" +#include "core/workspace-private.h" +#include "meta/boxes.h" +#include "wayland/meta-wayland-data-device.h" +#include "wayland/meta-wayland-outputs.h" +#include "wayland/meta-wayland-private.h" +#include "wayland/meta-wayland-surface.h" +#include "wayland/meta-wayland-versions.h" +#include "wayland/meta-wayland-xdg-shell.h" + +#include "wlr-layer-shell-unstable-v1-server-protocol.h" + +/* Layer shell global */ +struct _MetaWaylandLayerShell +{ + GObject parent; + GList *shell_resources; + GList *layer_surfaces; + MetaWaylandCompositor *compositor; + gulong workareas_changed_handler_id; +}; + +G_DEFINE_TYPE (MetaWaylandLayerShell, meta_wayland_layer_shell, G_TYPE_OBJECT) + +/* Layer surface state (pending/current) */ +typedef struct +{ + uint32_t anchor; + int32_t exclusive_zone; + struct { + int32_t top; + int32_t right; + int32_t bottom; + int32_t left; + } margin; + uint32_t desired_width; + uint32_t desired_height; + MetaLayerShellLayer layer; + uint32_t keyboard_interactivity; +} MetaWaylandLayerSurfaceState; + +/* Layer surface */ +struct _MetaWaylandLayerSurface +{ + MetaWaylandActorSurface parent; + + struct wl_resource *resource; + MetaWaylandOutput *output; + char *namespace; + MetaLayerShellLayer initial_layer; + + MetaWaylandLayerSurfaceState current; + MetaWaylandLayerSurfaceState pending; + + uint32_t configure_serial; + gboolean configured; + gboolean mapped; +}; + +G_DEFINE_TYPE (MetaWaylandLayerSurface, meta_wayland_layer_surface, + META_TYPE_WAYLAND_ACTOR_SURFACE) + +enum +{ + PROP_0, + PROP_OUTPUT, + PROP_NAMESPACE, + PROP_INITIAL_LAYER, + N_PROPS +}; + +static GParamSpec *layer_surface_props[N_PROPS] = { NULL, }; + +static void meta_wayland_layer_surface_send_configure (MetaWaylandLayerSurface *layer_surface); + +static MetaWaylandLayerShell * +meta_wayland_layer_shell_from_compositor (MetaWaylandCompositor *compositor) +{ + return g_object_get_data (G_OBJECT (compositor), "-meta-wayland-layer-shell"); +} + +static void +on_workareas_changed (MetaDisplay *display, + MetaWaylandLayerShell *layer_shell) +{ + meta_wayland_layer_shell_on_workarea_changed (layer_shell->compositor); +} + +static void +meta_wayland_layer_shell_ensure_signal_connected (MetaWaylandLayerShell *layer_shell) +{ + MetaDisplay *display; + + if (layer_shell->workareas_changed_handler_id != 0) + return; + + display = meta_get_display (); + if (!display) + return; + + layer_shell->workareas_changed_handler_id = + g_signal_connect (display, "workareas-changed", + G_CALLBACK (on_workareas_changed), + layer_shell); +} + +static MetaSide +get_strut_side_from_anchor (uint32_t anchor) +{ + gboolean anchored_top = (anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP); + gboolean anchored_bottom = (anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM); + gboolean anchored_left = (anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); + gboolean anchored_right = (anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); + + if (anchored_top && !anchored_bottom) + return META_SIDE_TOP; + else if (anchored_bottom && !anchored_top) + return META_SIDE_BOTTOM; + else if (anchored_left && !anchored_right) + return META_SIDE_LEFT; + else if (anchored_right && !anchored_left) + return META_SIDE_RIGHT; + else + return -1; +} + +/** + * Calculate the total exclusive zone offset from OTHER layer surfaces + * on the same edge that were created before this surface. + * + * Surfaces are stored in reverse creation order (newest first), so + * surfaces that appear AFTER this one in the list were created earlier + * and should be positioned closer to the edge. + */ +static int +get_other_layer_surfaces_exclusive_offset (MetaWaylandLayerSurface *layer_surface, + MetaWaylandCompositor *compositor, + MetaSide side) +{ + MetaWaylandLayerShell *layer_shell; + GList *l; + int offset = 0; + gboolean found_self = FALSE; + + layer_shell = meta_wayland_layer_shell_from_compositor (compositor); + if (!layer_shell) + return 0; + + /* Iterate through all layer surfaces. Surfaces after this one in the list + * were created earlier and are "before" us for stacking purposes. */ + for (l = layer_shell->layer_surfaces; l; l = l->next) + { + MetaWaylandLayerSurface *other = l->data; + + if (other == layer_surface) + { + found_self = TRUE; + continue; + } + + /* Only count surfaces that come after us (created before us) */ + if (!found_self) + continue; + + /* Only count mapped surfaces with exclusive_zone > 0 on the same edge */ + if (!other->mapped || other->current.exclusive_zone <= 0) + continue; + + MetaSide other_side = get_strut_side_from_anchor (other->current.anchor); + if (other_side != side) + continue; + + /* Add this surface's exclusive zone (plus its margin on this edge) */ + switch (side) + { + case META_SIDE_TOP: + offset += other->current.exclusive_zone + other->current.margin.top; + break; + case META_SIDE_BOTTOM: + offset += other->current.exclusive_zone + other->current.margin.bottom; + break; + case META_SIDE_LEFT: + offset += other->current.exclusive_zone + other->current.margin.left; + break; + case META_SIDE_RIGHT: + offset += other->current.exclusive_zone + other->current.margin.right; + break; + default: + break; + } + } + + return offset; +} + +static gboolean +get_layer_surface_bounds (MetaWaylandLayerSurface *layer_surface, + MetaRectangle *output_rect, + MetaRectangle *usable_area) +{ + MetaWaylandLayerSurfaceState *state = &layer_surface->current; + MetaWaylandSurface *surface; + MetaLogicalMonitor *logical_monitor = NULL; + MetaRectangle monitor_rect = { 0, 0, 0, 0 }; + + surface = meta_wayland_surface_role_get_surface (META_WAYLAND_SURFACE_ROLE (layer_surface)); + + if (layer_surface->output && layer_surface->output->logical_monitor) + { + logical_monitor = layer_surface->output->logical_monitor; + monitor_rect = logical_monitor->rect; + } + else + { + MetaBackend *backend = meta_get_backend (); + MetaMonitorManager *monitor_manager = meta_backend_get_monitor_manager (backend); + MetaLogicalMonitor *primary = meta_monitor_manager_get_primary_logical_monitor (monitor_manager); + + if (primary) + { + logical_monitor = primary; + monitor_rect = primary->rect; + } + else + { + monitor_rect.width = 1920; + monitor_rect.height = 1080; + } + } + + *output_rect = monitor_rect; + + if (state->exclusive_zone == -1) + { + /* Full output, ignore all panels */ + *usable_area = monitor_rect; + } + else if (logical_monitor) + { + MetaDisplay *display = meta_get_display (); + MetaWorkspaceManager *workspace_manager; + MetaWorkspace *workspace; + + *usable_area = monitor_rect; + if (display) + { + workspace_manager = meta_display_get_workspace_manager (display); + if (workspace_manager) + { + workspace = meta_workspace_manager_get_active_workspace (workspace_manager); + if (workspace) + { + if (state->exclusive_zone > 0) + { + MetaSide side; + int other_surfaces_offset; + + /* For surfaces that claim exclusive space, use workarea + * excluding layer-shell struts to avoid circular dependency + * (surface's own strut affecting its position). */ + meta_workspace_get_work_area_for_logical_monitor_excluding_layer_shell ( + workspace, logical_monitor, usable_area); + + /* Also account for other layer surfaces on the same edge + * that were created before this one. */ + side = get_strut_side_from_anchor (state->anchor); + if (side != (MetaSide) -1 && surface && surface->compositor) + { + other_surfaces_offset = + get_other_layer_surfaces_exclusive_offset (layer_surface, + surface->compositor, + side); + switch (side) + { + case META_SIDE_TOP: + usable_area->y += other_surfaces_offset; + usable_area->height -= other_surfaces_offset; + break; + case META_SIDE_BOTTOM: + usable_area->height -= other_surfaces_offset; + break; + case META_SIDE_LEFT: + usable_area->x += other_surfaces_offset; + usable_area->width -= other_surfaces_offset; + break; + case META_SIDE_RIGHT: + usable_area->width -= other_surfaces_offset; + break; + default: + break; + } + } + } + else + { + /* For surfaces with exclusive_zone == 0, use full workarea + * (they respect all panels including other layer surfaces). */ + meta_workspace_get_work_area_for_logical_monitor (workspace, + logical_monitor, + usable_area); + } + return TRUE; + } + } + } + } + else + { + *usable_area = monitor_rect; + } + + return TRUE; +} + +static MetaStrut * +meta_wayland_layer_surface_create_strut (MetaWaylandLayerSurface *layer_surface) +{ + MetaWaylandLayerSurfaceState *state = &layer_surface->current; + MetaRectangle output_rect = { 0, 0, 0, 0 }; + MetaRectangle usable_area = { 0, 0, 0, 0 }; + MetaStrut *strut; + MetaSide side; + int offset_top, offset_bottom, offset_left, offset_right; + + if (state->exclusive_zone <= 0 || !layer_surface->mapped) + return NULL; + + side = get_strut_side_from_anchor (state->anchor); + if (side == (MetaSide) -1) + return NULL; + + get_layer_surface_bounds (layer_surface, &output_rect, &usable_area); + + /* Calculate how much the workarea is offset from output on each edge + * (this accounts for Cinnamon panels via builtin_struts). */ + offset_top = usable_area.y - output_rect.y; + offset_bottom = (output_rect.y + output_rect.height) - + (usable_area.y + usable_area.height); + offset_left = usable_area.x - output_rect.x; + offset_right = (output_rect.x + output_rect.width) - + (usable_area.x + usable_area.width); + + strut = g_new0 (MetaStrut, 1); + strut->side = side; + + /* Create strut from OUTPUT edge, extending to cover both the existing + * workarea offset (Cinnamon panels) AND this surface's exclusive zone. + * This matches how builtin_struts are processed. */ + switch (side) + { + case META_SIDE_TOP: + strut->rect.x = output_rect.x; + strut->rect.y = output_rect.y; + strut->rect.width = output_rect.width; + strut->rect.height = offset_top + state->exclusive_zone + state->margin.top; + break; + case META_SIDE_BOTTOM: + strut->rect.x = output_rect.x; + strut->rect.height = offset_bottom + state->exclusive_zone + state->margin.bottom; + strut->rect.y = output_rect.y + output_rect.height - strut->rect.height; + strut->rect.width = output_rect.width; + break; + case META_SIDE_LEFT: + strut->rect.x = output_rect.x; + strut->rect.y = output_rect.y; + strut->rect.width = offset_left + state->exclusive_zone + state->margin.left; + strut->rect.height = output_rect.height; + break; + case META_SIDE_RIGHT: + strut->rect.y = output_rect.y; + strut->rect.width = offset_right + state->exclusive_zone + state->margin.right; + strut->rect.x = output_rect.x + output_rect.width - strut->rect.width; + strut->rect.height = output_rect.height; + break; + default: + g_free (strut); + return NULL; + } + + return strut; +} + +static void +layer_surface_set_size (struct wl_client *client, + struct wl_resource *resource, + uint32_t width, + uint32_t height) +{ + MetaWaylandLayerSurface *layer_surface = wl_resource_get_user_data (resource); + + layer_surface->pending.desired_width = width; + layer_surface->pending.desired_height = height; +} + +static void +layer_surface_set_anchor (struct wl_client *client, + struct wl_resource *resource, + uint32_t anchor) +{ + MetaWaylandLayerSurface *layer_surface = wl_resource_get_user_data (resource); + + if (anchor > (ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | + ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT)) + { + wl_resource_post_error (resource, + ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_ANCHOR, + "Invalid anchor value"); + return; + } + + layer_surface->pending.anchor = anchor; +} + +static void +layer_surface_set_exclusive_zone (struct wl_client *client, + struct wl_resource *resource, + int32_t zone) +{ + MetaWaylandLayerSurface *layer_surface = wl_resource_get_user_data (resource); + + layer_surface->pending.exclusive_zone = zone; +} + +static void +layer_surface_set_margin (struct wl_client *client, + struct wl_resource *resource, + int32_t top, + int32_t right, + int32_t bottom, + int32_t left) +{ + MetaWaylandLayerSurface *layer_surface = wl_resource_get_user_data (resource); + + layer_surface->pending.margin.top = top; + layer_surface->pending.margin.right = right; + layer_surface->pending.margin.bottom = bottom; + layer_surface->pending.margin.left = left; +} + +static void +layer_surface_set_keyboard_interactivity (struct wl_client *client, + struct wl_resource *resource, + uint32_t keyboard_interactivity) +{ + MetaWaylandLayerSurface *layer_surface = wl_resource_get_user_data (resource); + + if (keyboard_interactivity > ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND) + { + wl_resource_post_error (resource, + ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_KEYBOARD_INTERACTIVITY, + "Invalid keyboard interactivity value"); + return; + } + + layer_surface->pending.keyboard_interactivity = keyboard_interactivity; +} + +static void +layer_surface_get_popup (struct wl_client *client, + struct wl_resource *resource, + struct wl_resource *popup_resource) +{ + MetaWaylandLayerSurface *layer_surface = wl_resource_get_user_data (resource); + MetaWaylandSurface *surface = + meta_wayland_surface_role_get_surface (META_WAYLAND_SURFACE_ROLE (layer_surface)); + MetaWaylandXdgPopup *xdg_popup; + + if (!popup_resource) + { + wl_resource_post_error (resource, + ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_SURFACE_STATE, + "popup resource is NULL"); + return; + } + + xdg_popup = wl_resource_get_user_data (popup_resource); + if (!xdg_popup || !META_IS_WAYLAND_XDG_POPUP (xdg_popup)) + { + wl_resource_post_error (resource, + ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_SURFACE_STATE, + "popup is not a valid xdg_popup"); + return; + } + + meta_wayland_xdg_popup_set_parent_surface (xdg_popup, surface); +} + +static void +layer_surface_ack_configure (struct wl_client *client, + struct wl_resource *resource, + uint32_t serial) +{ + MetaWaylandLayerSurface *layer_surface = wl_resource_get_user_data (resource); + + layer_surface->configure_serial = serial; + layer_surface->configured = TRUE; +} + +static void +layer_surface_destroy (struct wl_client *client, + struct wl_resource *resource) +{ + wl_resource_destroy (resource); +} + +static void +layer_surface_set_layer (struct wl_client *client, + struct wl_resource *resource, + uint32_t layer) +{ + MetaWaylandLayerSurface *layer_surface = wl_resource_get_user_data (resource); + + if (layer > ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY) + { + wl_resource_post_error (resource, + ZWLR_LAYER_SHELL_V1_ERROR_INVALID_LAYER, + "Invalid layer value"); + return; + } + + layer_surface->pending.layer = layer; +} + +static void +layer_surface_set_exclusive_edge (struct wl_client *client, + struct wl_resource *resource, + uint32_t edge) +{ + /* TODO: Implement exclusive edge */ +} + +static const struct zwlr_layer_surface_v1_interface layer_surface_interface = { + layer_surface_set_size, + layer_surface_set_anchor, + layer_surface_set_exclusive_zone, + layer_surface_set_margin, + layer_surface_set_keyboard_interactivity, + layer_surface_get_popup, + layer_surface_ack_configure, + layer_surface_destroy, + layer_surface_set_layer, + layer_surface_set_exclusive_edge, +}; + +static void +layer_surface_resource_destroyed (struct wl_resource *resource) +{ + MetaWaylandLayerSurface *layer_surface = wl_resource_get_user_data (resource); + + if (layer_surface) + { + layer_surface->resource = NULL; + g_free (layer_surface->namespace); + layer_surface->namespace = NULL; + } +} + +/* Calculate surface position based on anchor, margin, and output/workarea geometry */ +static void +calculate_surface_position (MetaWaylandLayerSurface *layer_surface, + int *out_x, + int *out_y) +{ + MetaRectangle output_rect = { 0, 0, 0, 0 }; + MetaRectangle usable_area = { 0, 0, 0, 0 }; + MetaRectangle *bounds; + MetaWaylandSurface *surface; + int width, height; + int x = 0, y = 0; + uint32_t anchor; + + surface = meta_wayland_surface_role_get_surface (META_WAYLAND_SURFACE_ROLE (layer_surface)); + if (!surface || !surface->buffer_ref.buffer) + { + *out_x = 0; + *out_y = 0; + return; + } + + get_layer_surface_bounds (layer_surface, &output_rect, &usable_area); + + /* Use appropriate bounds based on exclusive_zone: + * -1: use full output (extend under panels) + * 0 or >0: use workarea (respect builtin panels like Cinnamon's) */ + if (layer_surface->current.exclusive_zone == -1) + bounds = &output_rect; + else + bounds = &usable_area; + + width = meta_wayland_surface_get_width (surface); + height = meta_wayland_surface_get_height (surface); + anchor = layer_surface->current.anchor; + + /* Calculate X position */ + if ((anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT) && + (anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT)) + { + /* Horizontally centered, stretched */ + x = bounds->x + layer_surface->current.margin.left; + } + else if (anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT) + { + x = bounds->x + layer_surface->current.margin.left; + } + else if (anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT) + { + x = bounds->x + bounds->width - width - layer_surface->current.margin.right; + } + else + { + /* Horizontally centered */ + x = bounds->x + (bounds->width - width) / 2; + } + + /* Calculate Y position */ + if ((anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP) && + (anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM)) + { + /* Vertically centered, stretched */ + y = bounds->y + layer_surface->current.margin.top; + } + else if (anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP) + { + y = bounds->y + layer_surface->current.margin.top; + } + else if (anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM) + { + y = bounds->y + bounds->height - height - layer_surface->current.margin.bottom; + } + else + { + /* Vertically centered */ + y = bounds->y + (bounds->height - height) / 2; + } + + *out_x = x; + *out_y = y; +} + +static ClutterActor * +get_layer_container_for_layer (MetaWaylandLayerSurface *layer_surface) +{ + MetaDisplay *display; + ClutterActor *layer_container = NULL; + + display = meta_get_display (); + + switch (layer_surface->current.layer) + { + case META_LAYER_SHELL_LAYER_BACKGROUND: + case META_LAYER_SHELL_LAYER_BOTTOM: + /* Use bottom_window_group for background and bottom layers */ + layer_container = meta_get_bottom_window_group_for_display (display); + break; + case META_LAYER_SHELL_LAYER_TOP: + /* Use top_window_group for top layer */ + layer_container = meta_get_top_window_group_for_display (display); + break; + case META_LAYER_SHELL_LAYER_OVERLAY: + /* Use feedback_group for overlay (topmost) */ + layer_container = meta_get_feedback_group_for_display (display); + break; + } + + return layer_container; +} + +static void +meta_wayland_layer_surface_apply_state (MetaWaylandSurfaceRole *surface_role, + MetaWaylandSurfaceState *pending) +{ + MetaWaylandLayerSurface *layer_surface = META_WAYLAND_LAYER_SURFACE (surface_role); + MetaWaylandSurface *surface = + meta_wayland_surface_role_get_surface (surface_role); + MetaWaylandActorSurface *actor_surface = META_WAYLAND_ACTOR_SURFACE (surface_role); + MetaSurfaceActor *surface_actor; + ClutterActor *layer_container; + gboolean had_buffer; + gboolean has_buffer; + gboolean struts_changed = FALSE; + MetaWaylandLayerSurfaceState old_state; + int x, y; + + had_buffer = layer_surface->mapped; + has_buffer = surface->buffer_ref.buffer != NULL; + + /* Save old state for strut change detection */ + old_state = layer_surface->current; + + /* Copy pending state to current */ + layer_surface->current = layer_surface->pending; + + /* Chain up to handle frame callbacks */ + meta_wayland_actor_surface_queue_frame_callbacks (actor_surface, pending); + + /* If client committed without a buffer and hasn't been properly configured, + * send a configure with the calculated size based on their anchors. */ + if (!has_buffer && !layer_surface->configured) + { + meta_wayland_layer_surface_send_configure (layer_surface); + } + + surface_actor = meta_wayland_actor_surface_get_actor (actor_surface); + if (!surface_actor) + return; + + layer_container = get_layer_container_for_layer (layer_surface); + if (!layer_container) + return; + + if (has_buffer) + { + if (!had_buffer) + { + /* Surface is being mapped */ + ClutterActor *actor = CLUTTER_ACTOR (surface_actor); + + clutter_actor_set_reactive (actor, TRUE); + clutter_actor_add_child (layer_container, actor); + layer_surface->mapped = TRUE; + + /* Mapping may affect struts */ + if (layer_surface->current.exclusive_zone > 0) + struts_changed = TRUE; + + g_debug ("Layer surface mapped: namespace=%s layer=%d", + layer_surface->namespace ? layer_surface->namespace : "(null)", + layer_surface->current.layer); + } + else + { + /* Check if strut-affecting properties changed while mapped */ + if (layer_surface->current.exclusive_zone != old_state.exclusive_zone || + layer_surface->current.anchor != old_state.anchor || + layer_surface->current.margin.top != old_state.margin.top || + layer_surface->current.margin.bottom != old_state.margin.bottom || + layer_surface->current.margin.left != old_state.margin.left || + layer_surface->current.margin.right != old_state.margin.right) + { + if (layer_surface->current.exclusive_zone > 0 || + old_state.exclusive_zone > 0) + struts_changed = TRUE; + } + } + + /* Sync actor state */ + meta_wayland_actor_surface_sync_actor_state (actor_surface); + + /* Update position */ + calculate_surface_position (layer_surface, &x, &y); + clutter_actor_set_position (CLUTTER_ACTOR (surface_actor), x, y); + } + else if (had_buffer && !has_buffer) + { + /* Surface is being unmapped */ + ClutterActor *actor = CLUTTER_ACTOR (surface_actor); + + clutter_actor_set_reactive (actor, FALSE); + + if (clutter_actor_get_parent (actor)) + clutter_actor_remove_child (layer_container, actor); + + layer_surface->mapped = FALSE; + + /* Unmapping may affect struts */ + if (old_state.exclusive_zone > 0) + struts_changed = TRUE; + + g_debug ("Layer surface unmapped: namespace=%s", + layer_surface->namespace ? layer_surface->namespace : "(null)"); + } + + /* Update workspace struts if needed */ + if (struts_changed) + meta_wayland_layer_shell_update_struts (surface->compositor); +} + +static void +meta_wayland_layer_surface_send_configure (MetaWaylandLayerSurface *layer_surface) +{ + MetaWaylandSurface *surface = + meta_wayland_surface_role_get_surface (META_WAYLAND_SURFACE_ROLE (layer_surface)); + MetaWaylandLayerSurfaceState *state = &layer_surface->pending; + MetaRectangle output_rect = { 0, 0, 0, 0 }; + MetaRectangle usable_area = { 0, 0, 0, 0 }; + MetaRectangle *bounds; + MetaLogicalMonitor *logical_monitor = NULL; + uint32_t width, height; + uint32_t serial; + + if (!layer_surface->resource) + return; + + /* Get output and workarea geometry for configure */ + if (layer_surface->output && layer_surface->output->logical_monitor) + { + logical_monitor = layer_surface->output->logical_monitor; + output_rect = logical_monitor->rect; + } + else + { + MetaBackend *backend = meta_get_backend (); + MetaMonitorManager *monitor_manager = meta_backend_get_monitor_manager (backend); + MetaLogicalMonitor *primary = meta_monitor_manager_get_primary_logical_monitor (monitor_manager); + + if (primary) + { + logical_monitor = primary; + output_rect = primary->rect; + } + else + { + output_rect.width = 1920; + output_rect.height = 1080; + } + } + + /* Get workarea if needed */ + if (state->exclusive_zone == -1) + { + usable_area = output_rect; + } + else if (logical_monitor) + { + MetaDisplay *display = meta_get_display (); + MetaWorkspaceManager *workspace_manager; + MetaWorkspace *workspace; + + usable_area = output_rect; + if (display) + { + workspace_manager = meta_display_get_workspace_manager (display); + if (workspace_manager) + { + workspace = meta_workspace_manager_get_active_workspace (workspace_manager); + if (workspace) + { + if (state->exclusive_zone > 0) + { + MetaSide side; + int other_surfaces_offset; + + /* Use workarea excluding layer-shell struts */ + meta_workspace_get_work_area_for_logical_monitor_excluding_layer_shell ( + workspace, logical_monitor, &usable_area); + + /* Also account for other layer surfaces on the same edge */ + side = get_strut_side_from_anchor (state->anchor); + if (side != (MetaSide) -1 && surface && surface->compositor) + { + other_surfaces_offset = + get_other_layer_surfaces_exclusive_offset (layer_surface, + surface->compositor, + side); + switch (side) + { + case META_SIDE_TOP: + usable_area.y += other_surfaces_offset; + usable_area.height -= other_surfaces_offset; + break; + case META_SIDE_BOTTOM: + usable_area.height -= other_surfaces_offset; + break; + case META_SIDE_LEFT: + usable_area.x += other_surfaces_offset; + usable_area.width -= other_surfaces_offset; + break; + case META_SIDE_RIGHT: + usable_area.width -= other_surfaces_offset; + break; + default: + break; + } + } + } + else + { + /* Use full workarea for exclusive_zone == 0 */ + meta_workspace_get_work_area_for_logical_monitor (workspace, + logical_monitor, + &usable_area); + } + } + } + } + } + else + { + usable_area = output_rect; + } + + /* Use appropriate bounds based on exclusive_zone: + * -1: use full output (extend under panels) + * 0 or >0: use workarea (respect builtin panels like Cinnamon's) */ + if (state->exclusive_zone == -1) + bounds = &output_rect; + else + bounds = &usable_area; + + /* Calculate configure size based on anchors and desired size */ + if (state->desired_width != 0) + width = state->desired_width; + else if ((state->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT) && + (state->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT)) + width = bounds->width - + state->margin.left - + state->margin.right; + else + width = 0; + + if (state->desired_height != 0) + height = state->desired_height; + else if ((state->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP) && + (state->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM)) + height = bounds->height - + state->margin.top - + state->margin.bottom; + else + height = 0; + + serial = wl_display_next_serial (surface->compositor->wayland_display); + zwlr_layer_surface_v1_send_configure (layer_surface->resource, serial, width, height); + + g_debug ("Layer surface configured: serial=%u size=%ux%u", serial, width, height); +} + +static void +meta_wayland_layer_surface_dispose (GObject *object) +{ + MetaWaylandLayerSurface *layer_surface = META_WAYLAND_LAYER_SURFACE (object); + MetaWaylandActorSurface *actor_surface = META_WAYLAND_ACTOR_SURFACE (object); + MetaWaylandSurface *surface; + MetaSurfaceActor *surface_actor; + gboolean had_struts; + + had_struts = layer_surface->mapped && layer_surface->current.exclusive_zone > 0; + + /* Remove from layer container */ + surface_actor = meta_wayland_actor_surface_get_actor (actor_surface); + if (surface_actor) + { + ClutterActor *actor = CLUTTER_ACTOR (surface_actor); + ClutterActor *parent = clutter_actor_get_parent (actor); + + if (parent) + clutter_actor_remove_child (parent, actor); + } + + /* Remove from tracking list and update struts */ + surface = meta_wayland_surface_role_get_surface (META_WAYLAND_SURFACE_ROLE (layer_surface)); + if (surface && surface->compositor) + { + MetaWaylandLayerShell *layer_shell = + meta_wayland_layer_shell_from_compositor (surface->compositor); + + if (layer_shell) + { + layer_shell->layer_surfaces = g_list_remove (layer_shell->layer_surfaces, + layer_surface); + if (had_struts) + meta_wayland_layer_shell_update_struts (surface->compositor); + } + } + + g_clear_pointer (&layer_surface->namespace, g_free); + + G_OBJECT_CLASS (meta_wayland_layer_surface_parent_class)->dispose (object); +} + +static void +meta_wayland_layer_surface_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MetaWaylandLayerSurface *layer_surface = META_WAYLAND_LAYER_SURFACE (object); + + switch (prop_id) + { + case PROP_OUTPUT: + layer_surface->output = g_value_get_pointer (value); + break; + case PROP_NAMESPACE: + layer_surface->namespace = g_value_dup_string (value); + break; + case PROP_INITIAL_LAYER: + layer_surface->initial_layer = g_value_get_uint (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +meta_wayland_layer_surface_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MetaWaylandLayerSurface *layer_surface = META_WAYLAND_LAYER_SURFACE (object); + + switch (prop_id) + { + case PROP_OUTPUT: + g_value_set_pointer (value, layer_surface->output); + break; + case PROP_NAMESPACE: + g_value_set_string (value, layer_surface->namespace); + break; + case PROP_INITIAL_LAYER: + g_value_set_uint (value, layer_surface->initial_layer); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +meta_wayland_layer_surface_constructed (GObject *object) +{ + MetaWaylandLayerSurface *layer_surface = META_WAYLAND_LAYER_SURFACE (object); + + G_OBJECT_CLASS (meta_wayland_layer_surface_parent_class)->constructed (object); + + /* Apply the initial layer from construction property */ + layer_surface->pending.layer = layer_surface->initial_layer; +} + +static void +meta_wayland_layer_surface_init (MetaWaylandLayerSurface *layer_surface) +{ + /* Default state */ + layer_surface->pending.layer = META_LAYER_SHELL_LAYER_BACKGROUND; + layer_surface->pending.keyboard_interactivity = + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE; + layer_surface->pending.exclusive_zone = 0; + layer_surface->configured = FALSE; + layer_surface->mapped = FALSE; +} + +static void +meta_wayland_layer_surface_assigned (MetaWaylandSurfaceRole *surface_role) +{ + MetaWaylandSurfaceRoleClass *surface_role_class = + META_WAYLAND_SURFACE_ROLE_CLASS (meta_wayland_layer_surface_parent_class); + MetaWaylandSurface *surface = + meta_wayland_surface_role_get_surface (surface_role); + + surface->dnd.funcs = meta_wayland_data_device_get_drag_dest_funcs (); + + if (surface_role_class->assigned) + surface_role_class->assigned (surface_role); +} + +static void +meta_wayland_layer_surface_class_init (MetaWaylandLayerSurfaceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + MetaWaylandSurfaceRoleClass *surface_role_class = + META_WAYLAND_SURFACE_ROLE_CLASS (klass); + + object_class->constructed = meta_wayland_layer_surface_constructed; + object_class->dispose = meta_wayland_layer_surface_dispose; + object_class->set_property = meta_wayland_layer_surface_set_property; + object_class->get_property = meta_wayland_layer_surface_get_property; + + surface_role_class->assigned = meta_wayland_layer_surface_assigned; + surface_role_class->apply_state = meta_wayland_layer_surface_apply_state; + + layer_surface_props[PROP_OUTPUT] = + g_param_spec_pointer ("output", NULL, NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + layer_surface_props[PROP_NAMESPACE] = + g_param_spec_string ("namespace", NULL, NULL, NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + layer_surface_props[PROP_INITIAL_LAYER] = + g_param_spec_uint ("initial-layer", NULL, NULL, + 0, ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, + META_LAYER_SHELL_LAYER_BACKGROUND, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, N_PROPS, layer_surface_props); +} + +MetaLayerShellLayer +meta_wayland_layer_surface_get_layer (MetaWaylandLayerSurface *layer_surface) +{ + return layer_surface->current.layer; +} + +MetaWaylandOutput * +meta_wayland_layer_surface_get_output (MetaWaylandLayerSurface *layer_surface) +{ + return layer_surface->output; +} + +gboolean +meta_wayland_layer_surface_wants_keyboard_focus (MetaWaylandLayerSurface *layer_surface) +{ + return layer_surface->current.keyboard_interactivity != + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE; +} + +/* Layer shell protocol implementation */ +static void +layer_shell_get_layer_surface (struct wl_client *client, + struct wl_resource *resource, + uint32_t id, + struct wl_resource *surface_resource, + struct wl_resource *output_resource, + uint32_t layer, + const char *namespace) +{ + MetaWaylandSurface *surface = wl_resource_get_user_data (surface_resource); + MetaWaylandOutput *output = NULL; + MetaWaylandLayerSurface *layer_surface; + + /* Check if surface already has a role */ + if (surface->role) + { + wl_resource_post_error (resource, + ZWLR_LAYER_SHELL_V1_ERROR_ROLE, + "Surface already has a role"); + return; + } + + /* Validate layer value */ + if (layer > ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY) + { + wl_resource_post_error (resource, + ZWLR_LAYER_SHELL_V1_ERROR_INVALID_LAYER, + "Invalid layer value"); + return; + } + + /* Check if surface already has a buffer */ + if (surface->buffer_ref.buffer) + { + wl_resource_post_error (resource, + ZWLR_LAYER_SHELL_V1_ERROR_ALREADY_CONSTRUCTED, + "Surface has a buffer attached"); + return; + } + + if (output_resource) + output = wl_resource_get_user_data (output_resource); + + /* Create layer surface role via meta_wayland_surface_assign_role. + * This ensures the surface is associated with the role during construction, + * which is required by MetaWaylandActorSurface's constructed() handler. */ + if (!meta_wayland_surface_assign_role (surface, + META_TYPE_WAYLAND_LAYER_SURFACE, + "output", output, + "namespace", namespace, + "initial-layer", layer, + NULL)) + { + wl_resource_post_error (resource, + ZWLR_LAYER_SHELL_V1_ERROR_ROLE, + "wl_surface@%d already has a different role", + wl_resource_get_id (surface_resource)); + return; + } + + layer_surface = META_WAYLAND_LAYER_SURFACE (surface->role); + + layer_surface->resource = + wl_resource_create (client, + &zwlr_layer_surface_v1_interface, + wl_resource_get_version (resource), + id); + + wl_resource_set_implementation (layer_surface->resource, + &layer_surface_interface, + layer_surface, + layer_surface_resource_destroyed); + + /* Add to tracking list and ensure signal is connected */ + { + MetaWaylandLayerShell *layer_shell = wl_resource_get_user_data (resource); + layer_shell->layer_surfaces = g_list_prepend (layer_shell->layer_surfaces, + layer_surface); + meta_wayland_layer_shell_ensure_signal_connected (layer_shell); + } + + g_debug ("Layer surface created: namespace=%s layer=%d output=%p", + namespace ? namespace : "(null)", layer, output); + + /* Send initial configure now that resource is ready */ + meta_wayland_layer_surface_send_configure (layer_surface); +} + +static void +layer_shell_destroy (struct wl_client *client, + struct wl_resource *resource) +{ + wl_resource_destroy (resource); +} + +static const struct zwlr_layer_shell_v1_interface layer_shell_interface = { + layer_shell_get_layer_surface, + layer_shell_destroy, +}; + +static void +layer_shell_destructor (struct wl_resource *resource) +{ + MetaWaylandLayerShell *layer_shell = wl_resource_get_user_data (resource); + + layer_shell->shell_resources = g_list_remove (layer_shell->shell_resources, + resource); +} + +static void +bind_layer_shell (struct wl_client *client, + void *data, + uint32_t version, + uint32_t id) +{ + MetaWaylandLayerShell *layer_shell = data; + struct wl_resource *resource; + + resource = wl_resource_create (client, + &zwlr_layer_shell_v1_interface, + version, + id); + wl_resource_set_implementation (resource, + &layer_shell_interface, + layer_shell, + layer_shell_destructor); + + layer_shell->shell_resources = g_list_prepend (layer_shell->shell_resources, + resource); +} + +static void +meta_wayland_layer_shell_dispose (GObject *object) +{ + MetaWaylandLayerShell *layer_shell = META_WAYLAND_LAYER_SHELL (object); + + if (layer_shell->workareas_changed_handler_id != 0) + { + MetaDisplay *display = meta_get_display (); + if (display) + g_signal_handler_disconnect (display, layer_shell->workareas_changed_handler_id); + layer_shell->workareas_changed_handler_id = 0; + } + + g_clear_pointer (&layer_shell->layer_surfaces, g_list_free); + g_clear_pointer (&layer_shell->shell_resources, g_list_free); + + G_OBJECT_CLASS (meta_wayland_layer_shell_parent_class)->dispose (object); +} + +static void +meta_wayland_layer_shell_init (MetaWaylandLayerShell *layer_shell) +{ +} + +static void +meta_wayland_layer_shell_class_init (MetaWaylandLayerShellClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = meta_wayland_layer_shell_dispose; +} + +static MetaWaylandLayerShell * +meta_wayland_layer_shell_new (MetaWaylandCompositor *compositor) +{ + MetaWaylandLayerShell *layer_shell; + + layer_shell = g_object_new (META_TYPE_WAYLAND_LAYER_SHELL, NULL); + layer_shell->compositor = compositor; + + if (wl_global_create (compositor->wayland_display, + &zwlr_layer_shell_v1_interface, + META_ZWLR_LAYER_SHELL_V1_VERSION, + layer_shell, bind_layer_shell) == NULL) + { + g_warning ("Failed to register wlr_layer_shell_v1 global"); + g_object_unref (layer_shell); + return NULL; + } + + g_debug ("Layer shell protocol initialized (version %d)", META_ZWLR_LAYER_SHELL_V1_VERSION); + + return layer_shell; +} + +void +meta_wayland_layer_shell_update_struts (MetaWaylandCompositor *compositor) +{ + MetaWaylandLayerShell *layer_shell; + MetaDisplay *display; + MetaWorkspaceManager *workspace_manager; + GList *workspaces; + GList *l; + GSList *struts = NULL; + + layer_shell = meta_wayland_layer_shell_from_compositor (compositor); + if (!layer_shell) + return; + + display = meta_get_display (); + if (!display) + return; + + workspace_manager = meta_display_get_workspace_manager (display); + if (!workspace_manager) + return; + + for (l = layer_shell->layer_surfaces; l; l = l->next) + { + MetaWaylandLayerSurface *surface = l->data; + MetaStrut *strut = meta_wayland_layer_surface_create_strut (surface); + + if (strut) + struts = g_slist_prepend (struts, strut); + } + + workspaces = meta_workspace_manager_get_workspaces (workspace_manager); + for (l = workspaces; l; l = l->next) + { + MetaWorkspace *workspace = l->data; + meta_workspace_set_layer_shell_struts (workspace, struts); + } + + g_slist_free_full (struts, g_free); +} + +void +meta_wayland_layer_shell_on_workarea_changed (MetaWaylandCompositor *compositor) +{ + MetaWaylandLayerShell *layer_shell; + GList *l; + + layer_shell = meta_wayland_layer_shell_from_compositor (compositor); + if (!layer_shell) + return; + + for (l = layer_shell->layer_surfaces; l; l = l->next) + { + MetaWaylandLayerSurface *surface = l->data; + + /* Surfaces with exclusive_zone != -1 use workarea bounds and need + * repositioning when workarea changes. Surfaces with exclusive_zone == -1 + * use full output and aren't affected. */ + if (surface->current.exclusive_zone != -1 && surface->mapped) + { + MetaWaylandActorSurface *actor_surface = META_WAYLAND_ACTOR_SURFACE (surface); + MetaSurfaceActor *surface_actor = meta_wayland_actor_surface_get_actor (actor_surface); + + if (surface_actor) + { + int x, y; + calculate_surface_position (surface, &x, &y); + clutter_actor_set_position (CLUTTER_ACTOR (surface_actor), x, y); + } + + /* Also send configure in case size changed */ + meta_wayland_layer_surface_send_configure (surface); + } + } + + /* Recalculate layer-shell struts since surface positions changed */ + meta_wayland_layer_shell_update_struts (compositor); +} + +void +meta_wayland_init_layer_shell (MetaWaylandCompositor *compositor) +{ + g_object_set_data_full (G_OBJECT (compositor), "-meta-wayland-layer-shell", + meta_wayland_layer_shell_new (compositor), + g_object_unref); +} diff --git a/src/wayland/meta-wayland-layer-shell.h b/src/wayland/meta-wayland-layer-shell.h new file mode 100644 index 000000000..10d5f2a37 --- /dev/null +++ b/src/wayland/meta-wayland-layer-shell.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 Linux Mint + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#ifndef META_WAYLAND_LAYER_SHELL_H +#define META_WAYLAND_LAYER_SHELL_H + +#include "wayland/meta-wayland.h" +#include "wayland/meta-wayland-actor-surface.h" + +/* Layer shell layer values - matches protocol enum */ +typedef enum +{ + META_LAYER_SHELL_LAYER_BACKGROUND = 0, + META_LAYER_SHELL_LAYER_BOTTOM = 1, + META_LAYER_SHELL_LAYER_TOP = 2, + META_LAYER_SHELL_LAYER_OVERLAY = 3, +} MetaLayerShellLayer; + +#define META_TYPE_WAYLAND_LAYER_SHELL (meta_wayland_layer_shell_get_type ()) +G_DECLARE_FINAL_TYPE (MetaWaylandLayerShell, meta_wayland_layer_shell, + META, WAYLAND_LAYER_SHELL, GObject) + +#define META_TYPE_WAYLAND_LAYER_SURFACE (meta_wayland_layer_surface_get_type ()) +G_DECLARE_FINAL_TYPE (MetaWaylandLayerSurface, meta_wayland_layer_surface, + META, WAYLAND_LAYER_SURFACE, MetaWaylandActorSurface) + +void meta_wayland_init_layer_shell (MetaWaylandCompositor *compositor); + +MetaLayerShellLayer meta_wayland_layer_surface_get_layer (MetaWaylandLayerSurface *layer_surface); +MetaWaylandOutput * meta_wayland_layer_surface_get_output (MetaWaylandLayerSurface *layer_surface); +gboolean meta_wayland_layer_surface_wants_keyboard_focus (MetaWaylandLayerSurface *layer_surface); + +void meta_wayland_layer_shell_update_struts (MetaWaylandCompositor *compositor); +void meta_wayland_layer_shell_on_workarea_changed (MetaWaylandCompositor *compositor); + +#endif /* META_WAYLAND_LAYER_SHELL_H */ diff --git a/src/wayland/meta-wayland-pointer.c b/src/wayland/meta-wayland-pointer.c index 2016a930f..f4e4692a4 100644 --- a/src/wayland/meta-wayland-pointer.c +++ b/src/wayland/meta-wayland-pointer.c @@ -62,6 +62,7 @@ #include "wayland/meta-wayland-private.h" #include "wayland/meta-wayland-seat.h" #include "wayland/meta-wayland-surface.h" +#include "wayland/meta-wayland-layer-shell.h" #include "wayland/meta-xwayland.h" #ifdef HAVE_NATIVE_BACKEND @@ -485,6 +486,23 @@ default_grab_button (MetaWaylandPointerGrab *grab, MetaWaylandPointer *pointer = grab->pointer; meta_wayland_pointer_send_button (pointer, event); + + if (clutter_event_type (event) == CLUTTER_BUTTON_PRESS && + pointer->focus_surface) + { + MetaWaylandSurfaceRole *role = pointer->focus_surface->role; + + if (role && META_IS_WAYLAND_LAYER_SURFACE (role) && + meta_wayland_layer_surface_wants_keyboard_focus ( + META_WAYLAND_LAYER_SURFACE (role))) + { + MetaWaylandSeat *seat = meta_wayland_pointer_get_seat (pointer); + + if (meta_wayland_seat_has_keyboard (seat)) + meta_wayland_keyboard_set_focus (seat->keyboard, + pointer->focus_surface); + } + } } static const MetaWaylandPointerGrabInterface default_pointer_grab_interface = { diff --git a/src/wayland/meta-wayland-shell-surface.c b/src/wayland/meta-wayland-shell-surface.c index a1f5f36af..df8894907 100644 --- a/src/wayland/meta-wayland-shell-surface.c +++ b/src/wayland/meta-wayland-shell-surface.c @@ -244,6 +244,7 @@ meta_wayland_shell_surface_surface_pre_apply_state (MetaWaylandSurfaceRole *sur MetaWaylandSurface *surface = meta_wayland_surface_role_get_surface (surface_role); + /* Queue calc_showing when buffer is detached (unmap) */ if (pending->newly_attached && !surface->buffer_ref.buffer && priv->window) @@ -285,6 +286,10 @@ meta_wayland_shell_surface_surface_apply_state (MetaWaylandSurfaceRole *surface meta_wayland_surface_get_width (surface) * geometry_scale; window->buffer_rect.height = meta_wayland_surface_get_height (surface) * geometry_scale; + + /* Queue calc_showing when buffer is newly attached - needed for window to become visible */ + if (pending->newly_attached) + meta_window_queue (window, META_QUEUE_CALC_SHOWING); } static MetaWindow * @@ -342,9 +347,14 @@ meta_wayland_shell_surface_sync_actor_state (MetaWaylandActorSurface *actor_surf MetaWaylandActorSurfaceClass *actor_surface_class = META_WAYLAND_ACTOR_SURFACE_CLASS (meta_wayland_shell_surface_parent_class); MetaWindow *toplevel_window; + MetaWindow *window; toplevel_window = meta_wayland_surface_get_toplevel_window (surface); - if (!toplevel_window) + window = meta_wayland_surface_get_window (surface); + + /* For popups parented to layer surfaces, there's no toplevel window, + * but the popup itself has a window. Allow sync in that case. */ + if (!toplevel_window && !window) return; actor_surface_class->sync_actor_state (actor_surface); diff --git a/src/wayland/meta-wayland-surface.c b/src/wayland/meta-wayland-surface.c index 38e41fadb..746f619f6 100644 --- a/src/wayland/meta-wayland-surface.c +++ b/src/wayland/meta-wayland-surface.c @@ -42,6 +42,8 @@ #include "wayland/meta-wayland-buffer.h" #include "wayland/meta-wayland-data-device.h" #include "wayland/meta-wayland-gtk-shell.h" +#include "wayland/meta-wayland-foreign-toplevel.h" +#include "wayland/meta-wayland-layer-shell.h" #include "wayland/meta-wayland-keyboard.h" #include "wayland/meta-wayland-outputs.h" #include "wayland/meta-wayland-pointer.h" @@ -1280,7 +1282,8 @@ update_surface_output_state (gpointer key, gpointer value, gpointer user_data) MetaLogicalMonitor *logical_monitor; gboolean is_on_logical_monitor; - g_assert (surface->role); + if (!surface->role) + return; logical_monitor = wayland_output->logical_monitor; if (!logical_monitor) @@ -1456,6 +1459,8 @@ meta_wayland_shell_init (MetaWaylandCompositor *compositor) { meta_wayland_xdg_shell_init (compositor); meta_wayland_init_gtk_shell (compositor); + meta_wayland_init_layer_shell (compositor); + meta_wayland_init_foreign_toplevel (compositor); meta_wayland_init_viewporter (compositor); } diff --git a/src/wayland/meta-wayland-versions.h b/src/wayland/meta-wayland-versions.h index 666501229..82f89d926 100644 --- a/src/wayland/meta-wayland-versions.h +++ b/src/wayland/meta-wayland-versions.h @@ -60,5 +60,7 @@ #define META_ZWP_PRIMARY_SELECTION_V1_VERSION 1 #define META_XDG_TOPLEVEL_TAG_V1_VERSION 1 #define META_WP_CURSOR_SHAPE_VERSION 2 +#define META_ZWLR_LAYER_SHELL_V1_VERSION 4 +#define META_ZWLR_FOREIGN_TOPLEVEL_MANAGER_V1_VERSION 3 #endif diff --git a/src/wayland/meta-wayland-xdg-shell.c b/src/wayland/meta-wayland-xdg-shell.c index 80a41cc46..2243f454c 100644 --- a/src/wayland/meta-wayland-xdg-shell.c +++ b/src/wayland/meta-wayland-xdg-shell.c @@ -26,8 +26,11 @@ #include "wayland/meta-wayland-xdg-shell.h" #include "backends/meta-logical-monitor.h" +#include "compositor/meta-surface-actor-wayland.h" +#include "compositor/meta-window-actor-private.h" #include "core/boxes-private.h" #include "core/window-private.h" +#include "wayland/meta-wayland-layer-shell.h" #include "wayland/meta-wayland-outputs.h" #include "wayland/meta-wayland-popup.h" #include "wayland/meta-wayland-private.h" @@ -136,6 +139,16 @@ struct _MetaWaylandXdgPopup */ MetaPlacementRule placement_rule; + /* Stored positioner data for when parent is set later (e.g., layer-shell) */ + MetaRectangle anchor_rect; + int32_t width; + int32_t height; + uint32_t gravity; + uint32_t anchor; + uint32_t constraint_adjustment; + int32_t offset_x; + int32_t offset_y; + MetaWaylandSeat *grab_seat; uint32_t grab_serial; } setup; @@ -1095,7 +1108,9 @@ finish_popup_setup (MetaWaylandXdgPopup *xdg_popup) xdg_popup->setup.grab_seat = NULL; xdg_popup->dismissed_by_client = FALSE; - if (!meta_wayland_surface_get_window (parent_surface)) + /* Allow both regular window parents and layer surface parents */ + if (!meta_wayland_surface_get_window (parent_surface) && + !META_IS_WAYLAND_LAYER_SURFACE (parent_surface->role)) { xdg_popup_send_popup_done (xdg_popup->resource); return; @@ -1267,14 +1282,19 @@ meta_wayland_xdg_popup_post_apply_state (MetaWaylandSurfaceRole *surface_role, } parent_window = meta_wayland_surface_get_window (xdg_popup->parent_surface); - meta_window_get_buffer_rect (window, &buffer_rect); - meta_window_get_buffer_rect (parent_window, &parent_buffer_rect); - if (!meta_rectangle_overlap (&buffer_rect, &parent_buffer_rect) && - !meta_rectangle_is_adjacent_to (&buffer_rect, &parent_buffer_rect)) + + /* Skip overlap check for layer surface parents (no parent window) */ + if (parent_window) { - g_warning ("Buggy client caused popup to be placed outside of " - "parent window"); - dismiss_invalid_popup (xdg_popup); + meta_window_get_buffer_rect (window, &buffer_rect); + meta_window_get_buffer_rect (parent_window, &parent_buffer_rect); + if (!meta_rectangle_overlap (&buffer_rect, &parent_buffer_rect) && + !meta_rectangle_is_adjacent_to (&buffer_rect, &parent_buffer_rect)) + { + g_warning ("Buggy client caused popup to be placed outside of " + "parent window"); + dismiss_invalid_popup (xdg_popup); + } } } @@ -1324,11 +1344,18 @@ meta_wayland_xdg_popup_configure (MetaWaylandShellSurface *shell_surface, * * FIXME: Could maybe add a signal that is emitted before the window is * created so that we can avoid incorrect intermediate foci. + * + * However, layer-shell surfaces don't have a MetaWindow, so we still need + * to send configure for popups parented to layer surfaces. */ - if (!parent_window) + if (!parent_window && !META_IS_WAYLAND_LAYER_SURFACE (xdg_popup->parent_surface->role)) return; - geometry_scale = meta_window_wayland_get_geometry_scale (parent_window); + if (parent_window) + geometry_scale = meta_window_wayland_get_geometry_scale (parent_window); + else + geometry_scale = 1; /* Layer surfaces use scale 1 */ + x = configuration->rel_x / geometry_scale; y = configuration->rel_y / geometry_scale; if (xdg_popup->pending_repositioned) @@ -1971,32 +1998,31 @@ xdg_surface_constructor_get_popup (struct wl_client *client, MetaWaylandXdgPopup *xdg_popup; MetaWaylandXdgSurface *xdg_surface; - if (!parent_resource) + if (parent_resource) { - wl_resource_post_error (xdg_wm_base_resource, - XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT, - "Parent surface is null but Mutter does not yet " - "support specifying parent surfaces via other " - "protocols"); - return; - } + parent_surface = surface_from_xdg_surface_resource (parent_resource); + if (!parent_surface || !META_IS_WAYLAND_XDG_SURFACE (parent_surface->role)) + { + wl_resource_post_error (xdg_wm_base_resource, + XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT, + "Invalid popup parent role"); + return; + } - parent_surface = surface_from_xdg_surface_resource (parent_resource); - if (!parent_surface || !META_IS_WAYLAND_XDG_SURFACE (parent_surface->role)) - { - wl_resource_post_error (xdg_wm_base_resource, - XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT, - "Invalid popup parent role"); - return; + parent_window = meta_wayland_surface_get_window (parent_surface); + if (!parent_window) + { + wl_resource_post_error (xdg_wm_base_resource, + XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT, + "Invalid popup parent window"); + return; + } } - - parent_window = meta_wayland_surface_get_window (parent_surface); - if (!parent_window) + else { - wl_resource_post_error (xdg_wm_base_resource, - XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT, - "Invalid popup parent window"); - return; + /* Parent will be set later via another protocol (e.g., wlr-layer-shell) */ + parent_surface = NULL; + parent_window = NULL; } if (!meta_wayland_surface_assign_role (surface, @@ -2026,8 +2052,23 @@ xdg_surface_constructor_get_popup (struct wl_client *client, meta_wayland_xdg_surface_constructor_finalize (constructor, xdg_surface); xdg_positioner = wl_resource_get_user_data (positioner_resource); - xdg_popup->setup.placement_rule = - meta_wayland_xdg_positioner_to_placement (xdg_positioner, parent_window); + if (parent_window) + { + xdg_popup->setup.placement_rule = + meta_wayland_xdg_positioner_to_placement (xdg_positioner, parent_window); + } + else + { + /* Store positioner data for later use (e.g., with layer-shell parent) */ + xdg_popup->setup.anchor_rect = xdg_positioner->anchor_rect; + xdg_popup->setup.width = xdg_positioner->width; + xdg_popup->setup.height = xdg_positioner->height; + xdg_popup->setup.gravity = xdg_positioner->gravity; + xdg_popup->setup.anchor = xdg_positioner->anchor; + xdg_popup->setup.constraint_adjustment = xdg_positioner->constraint_adjustment; + xdg_popup->setup.offset_x = xdg_positioner->offset_x; + xdg_popup->setup.offset_y = xdg_positioner->offset_y; + } xdg_popup->setup.parent_surface = parent_surface; } @@ -2529,6 +2570,50 @@ bind_xdg_wm_base (struct wl_client *client, shell_client, xdg_wm_base_destructor); } +void +meta_wayland_xdg_popup_set_parent_surface (MetaWaylandXdgPopup *xdg_popup, + MetaWaylandSurface *parent_surface) +{ + g_return_if_fail (META_IS_WAYLAND_XDG_POPUP (xdg_popup)); + + xdg_popup->setup.parent_surface = parent_surface; + + /* If parent is a layer surface, compute placement rule from stored positioner data */ + if (META_IS_WAYLAND_LAYER_SURFACE (parent_surface->role)) + { + MetaSurfaceActor *surface_actor; + ClutterActor *actor; + float parent_x, parent_y; + float parent_width, parent_height; + MetaPlacementRule *rule = &xdg_popup->setup.placement_rule; + + surface_actor = meta_wayland_surface_get_actor (parent_surface); + actor = CLUTTER_ACTOR (surface_actor); + clutter_actor_get_position (actor, &parent_x, &parent_y); + clutter_actor_get_size (actor, &parent_width, &parent_height); + + /* Build placement rule from stored positioner data + layer surface position */ + rule->anchor_rect.x = xdg_popup->setup.anchor_rect.x + (int)parent_x; + rule->anchor_rect.y = xdg_popup->setup.anchor_rect.y + (int)parent_y; + rule->anchor_rect.width = xdg_popup->setup.anchor_rect.width; + rule->anchor_rect.height = xdg_popup->setup.anchor_rect.height; + rule->width = xdg_popup->setup.width; + rule->height = xdg_popup->setup.height; + rule->anchor = positioner_anchor_to_placement_anchor (xdg_popup->setup.anchor); + rule->gravity = positioner_gravity_to_placement_gravity (xdg_popup->setup.gravity); + rule->constraint_adjustment = xdg_popup->setup.constraint_adjustment; + rule->offset_x = xdg_popup->setup.offset_x; + rule->offset_y = xdg_popup->setup.offset_y; + rule->is_reactive = FALSE; + + /* Set parent_rect to the layer surface bounds */ + rule->parent_rect.x = (int)parent_x; + rule->parent_rect.y = (int)parent_y; + rule->parent_rect.width = (int)parent_width; + rule->parent_rect.height = (int)parent_height; + } +} + void meta_wayland_xdg_shell_init (MetaWaylandCompositor *compositor) { diff --git a/src/wayland/meta-wayland-xdg-shell.h b/src/wayland/meta-wayland-xdg-shell.h index f90e29bea..5631aed44 100644 --- a/src/wayland/meta-wayland-xdg-shell.h +++ b/src/wayland/meta-wayland-xdg-shell.h @@ -50,4 +50,7 @@ G_DECLARE_FINAL_TYPE (MetaWaylandXdgPopup, void meta_wayland_xdg_shell_init (MetaWaylandCompositor *compositor); +void meta_wayland_xdg_popup_set_parent_surface (MetaWaylandXdgPopup *xdg_popup, + MetaWaylandSurface *parent_surface); + #endif /* META_WAYLAND_XDG_SHELL_H */ diff --git a/src/wayland/meta-window-wayland.c b/src/wayland/meta-window-wayland.c index 10d7f0127..bd8c90a50 100644 --- a/src/wayland/meta-window-wayland.c +++ b/src/wayland/meta-window-wayland.c @@ -483,7 +483,7 @@ meta_window_wayland_update_main_monitor (MetaWindow *window, /* If the window is not a toplevel window (i.e. it's a popup window) just use * the monitor of the toplevel. */ toplevel_window = meta_wayland_surface_get_toplevel_window (window->surface); - if (toplevel_window != window) + if (toplevel_window && toplevel_window != window) { meta_window_update_monitor (toplevel_window, flags); window->monitor = toplevel_window->monitor; @@ -976,8 +976,17 @@ meta_window_wayland_finish_move_resize (MetaWindow *window, MetaWindow *parent; parent = meta_window_get_transient_for (window); - rect.x = parent->rect.x + acked_configuration->rel_x; - rect.y = parent->rect.y + acked_configuration->rel_y; + if (parent) + { + rect.x = parent->rect.x + acked_configuration->rel_x; + rect.y = parent->rect.y + acked_configuration->rel_y; + } + else + { + /* Layer-shell popups have no parent MetaWindow, use placement rule's parent_rect */ + rect.x = window->placement.rule->parent_rect.x + acked_configuration->rel_x; + rect.y = window->placement.rule->parent_rect.y + acked_configuration->rel_y; + } } else if (acked_configuration->has_position) { @@ -1059,6 +1068,9 @@ meta_window_place_with_placement_rule (MetaWindow *window, META_GRAVITY_NORTH_WEST, window->unconstrained_rect); window->calc_placement = FALSE; + + /* Mark as placed so meta_window_force_placement won't override our position */ + window->placed = TRUE; } void diff --git a/src/wayland/protocol/wlr-foreign-toplevel-management-unstable-v1.xml b/src/wayland/protocol/wlr-foreign-toplevel-management-unstable-v1.xml new file mode 100644 index 000000000..ad6ce1f33 --- /dev/null +++ b/src/wayland/protocol/wlr-foreign-toplevel-management-unstable-v1.xml @@ -0,0 +1,270 @@ + + + + Copyright © 2018 Ilia Bozhinov + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + The purpose of this protocol is to enable the creation of taskbars + and docks by providing them with a list of opened applications and + letting them request certain actions on them, like maximizing, etc. + + After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + toplevel window will be sent via the toplevel event + + + + + This event is emitted whenever a new toplevel window is created. It + is emitted for all toplevels, regardless of the app that has created + them. + + All initial details of the toplevel(title, app_id, states, etc.) will + be sent immediately after this event via the corresponding events in + zwlr_foreign_toplevel_handle_v1. + + + + + + + Indicates the client no longer wishes to receive events for new toplevels. + However the compositor may emit further toplevel_created events, until + the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending events to the + zwlr_foreign_toplevel_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + + A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + window. Each app may have multiple opened toplevels. + + Each toplevel has a list of outputs it is visible on, conveyed to the + client with the output_enter and output_leave events. + + + + + This event is emitted whenever the title of the toplevel changes. + + + + + + + This event is emitted whenever the app-id of the toplevel changes. + + + + + + + This event is emitted whenever the toplevel becomes visible on + the given output. A toplevel may be visible on multiple outputs. + + + + + + + This event is emitted whenever the toplevel stops being visible on + the given output. It is guaranteed that an entered-output event + with the same output has been emitted before this event. + + + + + + + Requests that the toplevel be maximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unmaximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be minimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unminimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Request that this toplevel be activated on the given seat. + There is no guarantee the toplevel will be actually activated. + + + + + + + The different states that a toplevel can have. These have the same meaning + as the states with the same names defined in xdg-toplevel + + + + + + + + + + + This event is emitted immediately after the zwlr_foreign_toplevel_handle_v1 + is created and each time the toplevel state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + + This event is sent after all changes in the toplevel state have been + sent. + + This allows changes to the zwlr_foreign_toplevel_handle_v1 properties + to be seen as atomic, even if they happen via multiple events. + + + + + + Send a request to the toplevel to close itself. The compositor would + typically use a shell-specific method to carry out this request, for + example by sending the xdg_toplevel.close event. However, this gives + no guarantees the toplevel will actually be destroyed. If and when + this happens, the zwlr_foreign_toplevel_handle_v1.closed event will + be emitted. + + + + + + The rectangle of the surface specified in this request corresponds to + the place where the app using this protocol represents the given toplevel. + It can be used by the compositor as a hint for some operations, e.g + minimizing. The client is however not required to set this, in which + case the compositor is free to decide some default value. + + If the client specifies more than one rectangle, only the last one is + considered. + + The dimensions are given in surface-local coordinates. + Setting width=height=0 removes the already-set rectangle. + + + + + + + + + + + + + + + + This event means the toplevel has been destroyed. It is guaranteed there + won't be any more events for this zwlr_foreign_toplevel_handle_v1. The + toplevel itself becomes inert so any requests will be ignored except the + destroy request. + + + + + + Destroys the zwlr_foreign_toplevel_handle_v1 object. + + This request should be called either when the client does not want to + use the toplevel anymore or after the closed event to finalize the + destruction of the object. + + + + + + + + Requests that the toplevel be fullscreened on the given output. If the + fullscreen state and/or the outputs the toplevel is visible on actually + change, this will be indicated by the state and output_enter/leave + events. + + The output parameter is only a hint to the compositor. Also, if output + is NULL, the compositor should decide which output the toplevel will be + fullscreened on, if at all. + + + + + + + Requests that the toplevel be unfullscreened. If the fullscreen state + actually changes, this will be indicated by the state event. + + + + + + + + This event is emitted whenever the parent of the toplevel changes. + + No event is emitted when the parent handle is destroyed by the client. + + + + + diff --git a/src/wayland/protocol/wlr-layer-shell-unstable-v1.xml b/src/wayland/protocol/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 000000000..6998081ec --- /dev/null +++ b/src/wayland/protocol/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,406 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Types of keyboard interaction possible for layer shell surfaces. The + rationale for this is twofold: (1) some applications are not interested + in keyboard events and not allowing them to be focused can improve the + desktop experience; (2) some applications will want to take exclusive + keyboard focus. + + + + + This value indicates that this surface is not interested in keyboard + events and the compositor should never assign it the keyboard focus. + + This is the default value, set for newly created layer shell surfaces. + + This is useful for e.g. desktop widgets that display information or + only have interaction with non-keyboard input devices. + + + + + Request exclusive keyboard focus if this surface is above the shell surface layer. + + For the top and overlay layers, the seat will always give + exclusive keyboard focus to the top-most layer which has keyboard + interactivity set to exclusive. If this layer contains multiple + surfaces with keyboard interactivity set to exclusive, the compositor + determines the one receiving keyboard events in an implementation- + defined manner. In this case, no guarantee is made when this surface + will receive keyboard focus (if ever). + + For the bottom and background layers, the compositor is allowed to use + normal focus semantics. + + This setting is mainly intended for applications that need to ensure + they receive all keyboard events, such as a lock screen or a password + prompt. + + + + + This requests the compositor to allow this surface to be focused and + unfocused by the user in an implementation-defined manner. The user + should be able to unfocus this surface even regardless of the layer + it is on. + + Typically, the compositor will want to use its normal mechanism to + manage keyboard focus between layer shell surfaces with this setting + and regular toplevels on the desktop layer (e.g. click to focus). + Nevertheless, it is possible for a compositor to require a special + interaction to focus or unfocus layer shell surfaces (e.g. requiring + a click even if focus follows the mouse normally, or providing a + keybinding to switch focus between layers). + + This setting is mainly intended for desktop shell components (e.g. + panels) that allow keyboard interaction. Using this option can allow + implementing a desktop shell that can be fully usable without the + mouse. + + + + + + + Set how keyboard events are delivered to this surface. By default, + layer shell surfaces do not receive keyboard events; this request can + be used to change this. + + This setting is inherited by child surfaces set by the get_popup + request. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Keyboard interactivity is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + + + + + + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + + + + + + + + + Requests an edge for the exclusive zone to apply. The exclusive + edge will be automatically deduced from anchor points when possible, + but when the surface is anchored to a corner, it will be necessary + to set it explicitly to disambiguate, as it is not possible to deduce + which one of the two corner edges should be used. + + The edge must be one the surface is anchored to, otherwise the + invalid_exclusive_edge protocol error will be raised. + + + + + diff --git a/src/wayland/tests/test-foreign-toplevel.py b/src/wayland/tests/test-foreign-toplevel.py new file mode 100755 index 000000000..8407cc6b3 --- /dev/null +++ b/src/wayland/tests/test-foreign-toplevel.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +""" +Test client for the wlr-foreign-toplevel-management protocol. + +Connects to the Wayland compositor, binds the zwlr_foreign_toplevel_manager_v1 +global, and displays a GTK window listing all toplevel windows with action +buttons (maximize, minimize, activate, close, fullscreen). + +Requires: python3, python3-gi, GTK3, wl_framework + (clone https://codeberg.org/Consolatis/wl_framework) + +Usage: PYTHONPATH=/path/to/wl_framework WAYLAND_DISPLAY=wayland-0 python3 test-foreign-toplevel.py +""" + +import sys +import os + +# Allow running from the dev tree +WL_FRAMEWORK_PATH = os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', 'wl_framework') +if os.path.isdir(WL_FRAMEWORK_PATH): + sys.path.insert(0, os.path.abspath(WL_FRAMEWORK_PATH)) + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, GLib, GObject, Pango + +from wl_framework.loop_integrations import GLibIntegration +from wl_framework.network.connection import WaylandConnection +from wl_framework.protocols.foreign_toplevel import ForeignTopLevel + + +class ToplevelManager(ForeignTopLevel): + def __init__(self, wl_connection, context): + super().__init__(wl_connection) + self.context = context + + def on_toplevel_created(self, toplevel): + self.context.emit('toplevel-new', toplevel) + + def on_toplevel_synced(self, toplevel): + self.context.emit('toplevel-synced', toplevel) + + def on_toplevel_closed(self, toplevel): + self.context.emit('toplevel-closed', toplevel) + + +class Context(GObject.Object, WaylandConnection): + __gsignals__ = { + 'toplevel-new': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)), + 'toplevel-synced': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)), + 'toplevel-closed': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)), + 'wl-ready': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_PYOBJECT, ()), + } + + def __init__(self): + GObject.Object.__init__(self) + WaylandConnection.__init__(self, eventloop_integration=GLibIntegration()) + self.seat = None + self.manager = None + + def on_initial_sync(self, data): + super().on_initial_sync(data) + self.seat = self.display.seat + self.manager = ToplevelManager(self, self) + self.emit('wl-ready') + + +class PanelUI: + def __init__(self, context): + self.context = context + self.rows = {} + + context.connect('toplevel-new', self._on_new) + context.connect('toplevel-synced', self._on_synced) + context.connect('toplevel-closed', self._on_closed) + context.connect('wl-ready', self._on_ready) + + self._build_ui() + + def _build_ui(self): + self.window = Gtk.Window(title="Foreign Toplevel Test") + self.window.set_default_size(750, 500) + self.window.connect("destroy", Gtk.main_quit) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + vbox.set_margin_start(10) + vbox.set_margin_end(10) + vbox.set_margin_top(10) + vbox.set_margin_bottom(10) + self.window.add(vbox) + + self.status_label = Gtk.Label(label="Connecting to Wayland...") + self.status_label.set_xalign(0) + vbox.pack_start(self.status_label, False, False, 0) + + vbox.pack_start(Gtk.Separator(), False, False, 0) + + scroll = Gtk.ScrolledWindow() + scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + vbox.pack_start(scroll, True, True, 0) + + self.listbox = Gtk.ListBox() + self.listbox.set_selection_mode(Gtk.SelectionMode.NONE) + scroll.add(self.listbox) + + self.window.show_all() + + def _on_ready(self, context): + self.status_label.set_text("Connected - bound foreign-toplevel-manager v%d" % + context.manager.version) + + def _on_new(self, context, toplevel): + pass + + def _on_synced(self, context, toplevel): + old_row = self.rows.get(toplevel.obj_id) + if old_row: + self.listbox.remove(old_row) + + row = self._create_row(toplevel) + self.rows[toplevel.obj_id] = row + self.listbox.add(row) + + def _on_closed(self, context, toplevel): + row = self.rows.pop(toplevel.obj_id, None) + if row: + self.listbox.remove(row) + + def _state_str(self, toplevel): + return ", ".join(toplevel.states) if toplevel.states else "normal" + + def _create_row(self, toplevel): + row = Gtk.ListBoxRow() + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + hbox.set_margin_start(6) + hbox.set_margin_end(6) + hbox.set_margin_top(4) + hbox.set_margin_bottom(4) + row.add(hbox) + + info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) + hbox.pack_start(info_box, True, True, 0) + + title_label = Gtk.Label() + title_label.set_xalign(0) + title_label.set_ellipsize(Pango.EllipsizeMode.END) + title_label.set_markup( + "%s" % GLib.markup_escape_text(toplevel.title or "(no title)")) + info_box.pack_start(title_label, False, False, 0) + + detail_label = Gtk.Label() + detail_label.set_xalign(0) + detail_label.set_markup( + "app_id: %s | state: %s | handle: %d" % ( + GLib.markup_escape_text(toplevel.app_id or "(none)"), + self._state_str(toplevel), + toplevel.obj_id)) + info_box.pack_start(detail_label, False, False, 0) + + btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) + hbox.pack_end(btn_box, False, False, 0) + + self._add_button(btn_box, "Activate", + lambda b: toplevel.activate(self.context.seat)) + + if 'maximized' in toplevel.states: + self._add_button(btn_box, "Unmax", + lambda b: toplevel.set_maximize(False)) + else: + self._add_button(btn_box, "Max", + lambda b: toplevel.set_maximize(True)) + + if 'minimized' in toplevel.states: + self._add_button(btn_box, "Unmin", + lambda b: toplevel.set_minimize(False)) + else: + self._add_button(btn_box, "Min", + lambda b: toplevel.set_minimize(True)) + + if 'fullscreen' in toplevel.states: + self._add_button(btn_box, "Unfs", + lambda b: toplevel.set_fullscreen(False)) + else: + self._add_button(btn_box, "Full", + lambda b: toplevel.set_fullscreen(True)) + + btn_close = self._add_button(btn_box, "Close", + lambda b: toplevel.close()) + btn_close.get_style_context().add_class("destructive-action") + + row.show_all() + return row + + def _add_button(self, box, label, callback): + btn = Gtk.Button(label=label) + btn.connect("clicked", callback) + box.pack_start(btn, False, False, 0) + return btn + + +def main(): + try: + context = Context() + except RuntimeError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + ui = PanelUI(context) + + try: + Gtk.main() + except KeyboardInterrupt: + print() + + +if __name__ == '__main__': + main() diff --git a/tools/nested-session.py b/tools/nested-session.py new file mode 100755 index 000000000..ba4a85bda --- /dev/null +++ b/tools/nested-session.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 + +import os +import signal +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') +from gi.repository import Gtk, Gdk, GLib + +PRESETS = [ + "Full workarea", + "1920x1080", + "1280x720", + "2560x1440", + "Custom", +] + +DEFAULT_COMMAND = "cinnamon --nested --wayland" +TITLEBAR_HEIGHT = 32 + + +class NestedSessionLauncher(Gtk.Window): + def __init__(self): + super().__init__(title="Nested Cinnamon Session") + self.set_default_size(380, -1) + self.set_resizable(False) + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + box.set_margin_top(16) + box.set_margin_bottom(16) + box.set_margin_start(16) + box.set_margin_end(16) + self.add(box) + + # Resolution selector + res_label = Gtk.Label(label="Resolution", xalign=0) + box.pack_start(res_label, False, False, 0) + + self.resolution_combo = Gtk.ComboBoxText() + for preset in PRESETS: + self.resolution_combo.append_text(preset) + self.resolution_combo.set_active(0) + self.resolution_combo.connect("changed", self._on_resolution_changed) + box.pack_start(self.resolution_combo, False, False, 0) + + # Custom resolution spin buttons + self.custom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + self.width_spin = Gtk.SpinButton.new_with_range(640, 7680, 1) + self.width_spin.set_value(1920) + self.height_spin = Gtk.SpinButton.new_with_range(480, 4320, 1) + self.height_spin.set_value(1080) + self.custom_box.pack_start(self.width_spin, True, True, 0) + self.custom_box.pack_start(Gtk.Label(label="x"), False, False, 0) + self.custom_box.pack_start(self.height_spin, True, True, 0) + box.pack_start(self.custom_box, False, False, 0) + self.custom_box.set_no_show_all(True) + + # Memory gsettings toggle + self.memory_backend_check = Gtk.CheckButton(label="Use memory gsettings backend") + self.memory_backend_check.set_active(True) + box.pack_start(self.memory_backend_check, False, False, 0) + + # Command entry + cmd_label = Gtk.Label(label="Command", xalign=0) + box.pack_start(cmd_label, False, False, 0) + + self.command_entry = Gtk.Entry() + self.command_entry.set_text(DEFAULT_COMMAND) + box.pack_start(self.command_entry, False, False, 0) + + # Launch button + self.launch_button = Gtk.Button(label="Launch") + self.launch_button.connect("clicked", self._on_launch_clicked) + box.pack_start(self.launch_button, False, False, 0) + + def _on_resolution_changed(self, combo): + is_custom = combo.get_active_text() == "Custom" + if is_custom: + self.custom_box.show_all() + else: + self.custom_box.hide() + + def _get_resolution(self): + text = self.resolution_combo.get_active_text() + + if text == "Full workarea": + display = Gdk.Display.get_default() + window = self.get_window() + monitor = display.get_monitor_at_window(window) + workarea = monitor.get_workarea() + return workarea.width, workarea.height - TITLEBAR_HEIGHT + + if text == "Custom": + return int(self.width_spin.get_value()), int(self.height_spin.get_value()) + + # Preset like "1920x1080" + w, h = text.split("x") + return int(w), int(h) + + def _on_launch_clicked(self, button): + width, height = self._get_resolution() + command = self.command_entry.get_text().strip() + if not command: + return + + env = os.environ.copy() + env["MUFFIN_DEBUG_DUMMY_MODE_SPECS"] = f"{width}x{height}" + + if self.memory_backend_check.get_active(): + env["GSETTINGS_BACKEND"] = "memory" + + argv = ["dbus-run-session", "--"] + command.split() + + try: + flags = GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD + envlist = [f"{k}={v}" for k, v in env.items()] + + pid, _, _, _ = GLib.spawn_async( + argv=argv, + envp=envlist, + flags=flags, + ) + GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid, self._on_child_exit) + except GLib.Error as e: + dialog = Gtk.MessageDialog( + transient_for=self, + modal=True, + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.CLOSE, + text=f"Failed to launch: {e.message}", + ) + dialog.run() + dialog.destroy() + + def _on_child_exit(self, pid, status): + GLib.spawn_close_pid(pid) + + +def main(): + signal.signal(signal.SIGINT, signal.SIG_DFL) + win = NestedSessionLauncher() + win.connect("destroy", Gtk.main_quit) + win.show_all() + Gtk.main() + + +if __name__ == "__main__": + main()