From 2135c36cc8ee98ad15df940f759ac2b6f9c1eb6a Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Thu, 15 Jan 2026 23:39:19 -0500 Subject: [PATCH 1/3] Implement wlr-layer-shell. Very WIP, experimenting mainly with nemo-desktop for now. --- src/compositor/compositor.c | 9 +- src/compositor/meta-surface-actor.c | 4 +- src/core/constraints.c | 6 +- src/core/window.c | 29 +- src/core/workspace-private.h | 9 + src/core/workspace.c | 99 ++ src/meson.build | 3 + src/wayland/meta-wayland-actor-surface.c | 1 + src/wayland/meta-wayland-layer-shell.c | 1399 +++++++++++++++++ src/wayland/meta-wayland-layer-shell.h | 51 + src/wayland/meta-wayland-shell-surface.c | 12 +- src/wayland/meta-wayland-surface.c | 5 +- src/wayland/meta-wayland-versions.h | 1 + src/wayland/meta-wayland-xdg-shell.c | 153 +- src/wayland/meta-wayland-xdg-shell.h | 3 + src/wayland/meta-window-wayland.c | 18 +- .../protocol/wlr-layer-shell-unstable-v1.xml | 406 +++++ 17 files changed, 2160 insertions(+), 48 deletions(-) create mode 100644 src/wayland/meta-wayland-layer-shell.c create mode 100644 src/wayland/meta-wayland-layer-shell.h create mode 100644 src/wayland/protocol/wlr-layer-shell-unstable-v1.xml 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/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..b36982175 100644 --- a/src/meson.build +++ b/src/meson.build @@ -520,6 +520,8 @@ if have_wayland 'wayland/meta-wayland-dnd-surface.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 +810,7 @@ if have_wayland wayland_protocols = [ ['cursor-shape-v1', 'private', ], ['gtk-shell', '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-layer-shell.c b/src/wayland/meta-wayland-layer-shell.c new file mode 100644 index 000000000..3a56cfb64 --- /dev/null +++ b/src/wayland/meta-wayland-layer-shell.c @@ -0,0 +1,1399 @@ +/* + * 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; +} + +/* 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..b468b085b --- /dev/null +++ b/src/wayland/meta-wayland-layer-shell.h @@ -0,0 +1,51 @@ +/* + * 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); + +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-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..3227cf196 100644 --- a/src/wayland/meta-wayland-surface.c +++ b/src/wayland/meta-wayland-surface.c @@ -42,6 +42,7 @@ #include "wayland/meta-wayland-buffer.h" #include "wayland/meta-wayland-data-device.h" #include "wayland/meta-wayland-gtk-shell.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 +1281,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 +1458,7 @@ 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_viewporter (compositor); } diff --git a/src/wayland/meta-wayland-versions.h b/src/wayland/meta-wayland-versions.h index 666501229..2f84bf080 100644 --- a/src/wayland/meta-wayland-versions.h +++ b/src/wayland/meta-wayland-versions.h @@ -60,5 +60,6 @@ #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 #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-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. + + + + + From c012839cea375238c635281ceb96dbedbc9af4b3 Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Tue, 3 Feb 2026 00:12:57 -0500 Subject: [PATCH 2/3] Implement a wayland backend for clutter. Allows standalone processes to use Clutter as a wayland client directly. --- clutter/clutter/clutter-backend.c | 6 + clutter/clutter/clutter-build-config.h.meson | 3 + clutter/clutter/meson.build | 62 ++ .../clutter-backend-wayland-client.c | 448 +++++++++++++++ .../clutter-backend-wayland-client.h | 91 +++ .../clutter-keymap-wayland-client.c | 70 +++ .../clutter-keymap-wayland-client.h | 59 ++ .../clutter-seat-wayland-client.c | 173 ++++++ .../clutter-seat-wayland-client.h | 61 ++ .../clutter-stage-wayland-client.c | 543 ++++++++++++++++++ .../clutter-stage-wayland-client.h | 109 ++++ .../wlr-layer-shell-unstable-v1.xml | 406 +++++++++++++ cogl/cogl/cogl-defines.h.meson | 1 + cogl/cogl/cogl-renderer-private.h | 4 + cogl/cogl/cogl-renderer.c | 28 + cogl/cogl/cogl-renderer.h | 1 + cogl/cogl/cogl-wayland-client.h | 132 +++++ cogl/cogl/meson.build | 12 + .../winsys/cogl-winsys-egl-wayland-private.h | 40 ++ cogl/cogl/winsys/cogl-winsys-egl-wayland.c | 540 +++++++++++++++++ cogl/meson.build | 7 + debian/libmuffin0.symbols | 12 +- meson.build | 16 + meson_options.txt | 6 + 24 files changed, 2826 insertions(+), 4 deletions(-) create mode 100644 clutter/clutter/wayland-client/clutter-backend-wayland-client.c create mode 100644 clutter/clutter/wayland-client/clutter-backend-wayland-client.h create mode 100644 clutter/clutter/wayland-client/clutter-keymap-wayland-client.c create mode 100644 clutter/clutter/wayland-client/clutter-keymap-wayland-client.h create mode 100644 clutter/clutter/wayland-client/clutter-seat-wayland-client.c create mode 100644 clutter/clutter/wayland-client/clutter-seat-wayland-client.h create mode 100644 clutter/clutter/wayland-client/clutter-stage-wayland-client.c create mode 100644 clutter/clutter/wayland-client/clutter-stage-wayland-client.h create mode 100644 clutter/clutter/wayland-client/wlr-layer-shell-unstable-v1.xml create mode 100644 cogl/cogl/cogl-wayland-client.h create mode 100644 cogl/cogl/winsys/cogl-winsys-egl-wayland-private.h create mode 100644 cogl/cogl/winsys/cogl-winsys-egl-wayland.c 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/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..87e0fc2ea --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-backend-wayland-client.c @@ -0,0 +1,448 @@ +/* + * 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 "clutter-backend-wayland-client.h" +#include "clutter-stage-wayland-client.h" +#include "clutter-seat-wayland-client.h" +#include "wlr-layer-shell-unstable-v1-client-protocol.h" + +#include "clutter-backend.h" +#include "clutter-debug.h" +#include "clutter-main.h" +#include "clutter-private.h" +#include "clutter-settings-private.h" +#include "clutter-stage-private.h" + +G_DEFINE_TYPE (ClutterBackendWaylandClient, + clutter_backend_wayland_client, + CLUTTER_TYPE_BACKEND) + +/* 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)); + } + 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, +}; + +/* 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; + + *timeout = -1; + + if (wl_source->backend->wl_display) + wl_display_flush (wl_source->backend->wl_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_IN) + { + if (wl_display_dispatch (display) == -1) + { + g_warning ("Wayland display dispatch failed: %s", strerror (errno)); + return G_SOURCE_REMOVE; + } + } + + if (wl_source->pfd.revents & (G_IO_ERR | G_IO_HUP)) + { + g_warning ("Wayland display error"); + 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); + + CLUTTER_NOTE (BACKEND, "Initializing Wayland input events"); + + /* Create stub seat for now - full input handling will come later */ + backend_wl->seat = clutter_seat_wayland_client_new (); +} + +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); + } + + if (backend_wl->layer_shell) + zwlr_layer_shell_v1_destroy (backend_wl->layer_shell); + + 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..51d2ceda1 --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-backend-wayland-client.h @@ -0,0 +1,91 @@ +/* + * 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 "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; + + /* 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); + +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..b2770c7f5 --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-keymap-wayland-client.c @@ -0,0 +1,70 @@ +/* + * 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) +{ + return FALSE; +} + +static gboolean +clutter_keymap_wayland_client_get_caps_lock_state (ClutterKeymap *keymap) +{ + 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); +} 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..569f5279d --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-keymap-wayland-client.h @@ -0,0 +1,59 @@ +/* + * 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 + +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 _ClutterKeymapWaylandClientClass +{ + ClutterKeymapClass parent_class; +}; + +GType clutter_keymap_wayland_client_get_type (void) G_GNUC_CONST; + +ClutterKeymap * clutter_keymap_wayland_client_new (void); + +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..f7dc82e0d --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-seat-wayland-client.c @@ -0,0 +1,173 @@ +/* + * 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" + +G_DEFINE_TYPE (ClutterSeatWaylandClient, + clutter_seat_wayland_client, + CLUTTER_TYPE_SEAT) + +static ClutterInputDevice * +clutter_seat_wayland_client_get_pointer (ClutterSeat *seat) +{ + /* No pointer device yet - input handling not implemented */ + return NULL; +} + +static ClutterInputDevice * +clutter_seat_wayland_client_get_keyboard (ClutterSeat *seat) +{ + /* No keyboard device yet - input handling not implemented */ + return NULL; +} + +static GList * +clutter_seat_wayland_client_list_devices (ClutterSeat *seat) +{ + /* No devices yet - input handling not implemented */ + return NULL; +} + +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->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 (void) +{ + return g_object_new (CLUTTER_TYPE_SEAT_WAYLAND_CLIENT, NULL); +} 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..a01fb3093 --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-seat-wayland-client.h @@ -0,0 +1,61 @@ +/* + * 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 + +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; + + ClutterKeymap *keymap; +}; + +struct _ClutterSeatWaylandClientClass +{ + ClutterSeatClass parent_class; +}; + +GType clutter_seat_wayland_client_get_type (void) G_GNUC_CONST; + +ClutterSeat * clutter_seat_wayland_client_new (void); + +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..b4ccb065e --- /dev/null +++ b/clutter/clutter/wayland-client/clutter-stage-wayland-client.c @@ -0,0 +1,543 @@ +/* + * 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; + } + + /* 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); + + /* 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); + + /* 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/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, From f4b7b5eda3042573c5d08cd9a0b24727eda89dad Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Fri, 6 Feb 2026 16:25:38 -0500 Subject: [PATCH 3/3] Work on wlr-foreign-toplevel-management-v1. Not sure we'll keep this in the end but it gives me boxes to check for now towards splitting up our Cinnamon process. There are definitely gaps in support versus the current single-process model but it allows us to begin to identify them. --- src/meson.build | 3 + src/wayland/meta-wayland-foreign-toplevel.c | 939 ++++++++++++++++++ src/wayland/meta-wayland-foreign-toplevel.h | 27 + src/wayland/meta-wayland-surface.c | 2 + src/wayland/meta-wayland-versions.h | 1 + ...oreign-toplevel-management-unstable-v1.xml | 270 +++++ src/wayland/tests/test-foreign-toplevel.py | 217 ++++ 7 files changed, 1459 insertions(+) create mode 100644 src/wayland/meta-wayland-foreign-toplevel.c create mode 100644 src/wayland/meta-wayland-foreign-toplevel.h create mode 100644 src/wayland/protocol/wlr-foreign-toplevel-management-unstable-v1.xml create mode 100755 src/wayland/tests/test-foreign-toplevel.py diff --git a/src/meson.build b/src/meson.build index b36982175..bb44618a3 100644 --- a/src/meson.build +++ b/src/meson.build @@ -518,6 +518,8 @@ 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', @@ -810,6 +812,7 @@ 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', ], 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-surface.c b/src/wayland/meta-wayland-surface.c index 3227cf196..746f619f6 100644 --- a/src/wayland/meta-wayland-surface.c +++ b/src/wayland/meta-wayland-surface.c @@ -42,6 +42,7 @@ #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" @@ -1459,6 +1460,7 @@ 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 2f84bf080..82f89d926 100644 --- a/src/wayland/meta-wayland-versions.h +++ b/src/wayland/meta-wayland-versions.h @@ -61,5 +61,6 @@ #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/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/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()