diff --git a/0001-window-Add-a-map-inhibit-API.patch b/0001-window-Add-a-map-inhibit-API.patch new file mode 100644 index 0000000..9c31254 --- /dev/null +++ b/0001-window-Add-a-map-inhibit-API.patch @@ -0,0 +1,124 @@ +From 362fe3dd06395405160114d76b35099ba9e79e2d Mon Sep 17 00:00:00 2001 +From: Olivier Fourdan +Date: Tue, 30 Sep 2025 14:35:37 +0200 +Subject: [PATCH 1/7] window: Add a map inhibit API + +A compositor (such as GNOME Kiosk) may need to prevent windows from +showing on screen for various reasons, yet there is no appropriate API +to tell mutter that a window that could be mapped on screen should not +actually show up. + +To solve that problem, add a new API that allows to caller to inhibit +mapping windows. + +Part-of: +(cherry picked from commit 6f77df353a48dc394e1921d29bfd1175ffd09575) +--- + src/core/window.c | 55 ++++++++++++++++++++++++++++++++++++++++++++++- + src/meta/window.h | 9 ++++++++ + 2 files changed, 63 insertions(+), 1 deletion(-) + +diff --git a/src/core/window.c b/src/core/window.c +index 8f44f60d22..b7b62fbda5 100644 +--- a/src/core/window.c ++++ b/src/core/window.c +@@ -191,6 +191,8 @@ typedef struct _MetaWindowPrivate + gboolean is_queued; + guint idle_handle_id; + } auto_maximize; ++ ++ unsigned int mapped_inhibit_count; + } MetaWindowPrivate; + + G_DEFINE_ABSTRACT_TYPE_WITH_CODE (MetaWindow, meta_window, G_TYPE_OBJECT, +@@ -1757,6 +1759,7 @@ meta_window_showing_on_its_workspace (MetaWindow *window) + gboolean showing; + gboolean is_desktop_or_dock; + MetaWorkspace *workspace_of_window; ++ MetaWindowPrivate *priv = meta_window_get_instance_private (window); + + showing = TRUE; + +@@ -1764,7 +1767,11 @@ meta_window_showing_on_its_workspace (MetaWindow *window) + if (window->minimized) + showing = FALSE; + +- /* 2. See if we're in "show desktop" mode */ ++ /* 2. See if mapping state is inhibited */ ++ if (priv->mapped_inhibit_count > 0) ++ showing = FALSE; ++ ++ /* 3. See if we're in "show desktop" mode */ + is_desktop_or_dock = FALSE; + is_desktop_or_dock_foreach (window, + &is_desktop_or_dock); +@@ -8700,3 +8707,49 @@ meta_window_show_in_window_list (MetaWindow *window) + window->skip_from_window_list = FALSE; + meta_window_recalc_features (window); + } ++ ++/** ++ * meta_window_inhibit_mapped ++ * @window: A #MetaWindow ++ * ++ * Inhibits the mapped state of the window. ++ */ ++void ++meta_window_inhibit_mapped (MetaWindow *window) ++{ ++ MetaWindowPrivate *priv = meta_window_get_instance_private (window); ++ ++ if (++priv->mapped_inhibit_count == 1) ++ meta_window_queue (window, META_QUEUE_CALC_SHOWING); ++} ++ ++/** ++ * meta_window_uninhibit_mapped ++ * @window: A #MetaWindow ++ * ++ * Uninhibits the mapped state of the window. ++ */ ++void ++meta_window_uninhibit_mapped (MetaWindow *window) ++{ ++ MetaWindowPrivate *priv = meta_window_get_instance_private (window); ++ ++ g_return_if_fail (priv->mapped_inhibit_count > 0); ++ ++ if (--priv->mapped_inhibit_count == 0) ++ meta_window_queue (window, META_QUEUE_CALC_SHOWING); ++} ++ ++/** ++ * meta_window_is_mapped_inhibited ++ * @window: A #MetaWindow ++ * ++ * Returns whether the mapped state of the window is inhibited. ++ */ ++gboolean ++meta_window_is_mapped_inhibited (MetaWindow *window) ++{ ++ MetaWindowPrivate *priv = meta_window_get_instance_private (window); ++ ++ return priv->mapped_inhibit_count > 0; ++} +diff --git a/src/meta/window.h b/src/meta/window.h +index 9670987843..4f6390f0a1 100644 +--- a/src/meta/window.h ++++ b/src/meta/window.h +@@ -460,3 +460,12 @@ void meta_window_set_maximize_flags (MetaWindow *window, + META_EXPORT + void meta_window_set_unmaximize_flags (MetaWindow *window, + MetaMaximizeFlags directions); ++ ++META_EXPORT ++void meta_window_inhibit_mapped (MetaWindow *window); ++ ++META_EXPORT ++void meta_window_uninhibit_mapped (MetaWindow *window); ++ ++META_EXPORT ++gboolean meta_window_is_mapped_inhibited (MetaWindow *window); +-- +2.53.0 + diff --git a/0002-tests-Add-tests-for-map-inhibit.patch b/0002-tests-Add-tests-for-map-inhibit.patch new file mode 100644 index 0000000..6b1e5a9 --- /dev/null +++ b/0002-tests-Add-tests-for-map-inhibit.patch @@ -0,0 +1,326 @@ +From faf8b0a563b4078a990337811e1c97b0de9f1971 Mon Sep 17 00:00:00 2001 +From: Olivier Fourdan +Date: Tue, 30 Sep 2025 17:48:51 +0200 +Subject: [PATCH 2/7] tests: Add tests for map inhibit + +Test basic functionality of the API. + +Part-of: +(cherry picked from commit 92b21b8c0119df74ccd1f01737b9be3ea9fa223a) +--- + src/tests/meson.build | 8 + + src/tests/window-mapped-inhibit-tests.c | 283 ++++++++++++++++++++++++ + 2 files changed, 291 insertions(+) + create mode 100644 src/tests/window-mapped-inhibit-tests.c + +diff --git a/src/tests/meson.build b/src/tests/meson.build +index 8c3773033f..8c824b5752 100644 +--- a/src/tests/meson.build ++++ b/src/tests/meson.build +@@ -955,6 +955,14 @@ wayland_test_cases = [ + wayland_test_utils, + ], + }, ++ { ++ 'name': 'window-mapped-inhibit', ++ 'suite': 'wayland', ++ 'sources': [ ++ 'window-mapped-inhibit-tests.c', ++ wayland_test_utils, ++ ], ++ }, + { + 'name': 'viewport-tests', + 'suite': 'wayland', +diff --git a/src/tests/window-mapped-inhibit-tests.c b/src/tests/window-mapped-inhibit-tests.c +new file mode 100644 +index 0000000000..0b297e94ce +--- /dev/null ++++ b/src/tests/window-mapped-inhibit-tests.c +@@ -0,0 +1,283 @@ ++/* ++ * Copyright (C) 2025 Red Hat Inc. ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License as ++ * published by the Free Software Foundation; either version 2 of the ++ * License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, but ++ * WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program; if not, see . ++ * ++ */ ++ ++#include "config.h" ++ ++#include ++ ++#include "core/window-private.h" ++#include "meta-test/meta-context-test.h" ++#include "tests/meta-test-utils.h" ++#include "tests/meta-wayland-test-driver.h" ++#include "tests/meta-wayland-test-utils.h" ++#include "wayland/meta-wayland.h" ++ ++static MetaContext *test_context; ++static MetaWaylandTestDriver *test_driver; ++static MetaVirtualMonitor *virtual_monitor; ++ ++#define TEST_CLIENT_TITLE "window-mapped-inhibit-test-window" ++ ++static void ++wait_for_condition (gboolean (*condition_func) (gpointer), ++ gpointer user_data) ++{ ++ while (!condition_func (user_data)) ++ g_main_context_iteration (NULL, TRUE); ++} ++ ++static gboolean ++window_is_not_hidden (gpointer user_data) ++{ ++ MetaWindow *window = (MetaWindow *) user_data; ++ return !meta_window_is_hidden (window); ++} ++ ++static gboolean ++window_is_hidden (gpointer user_data) ++{ ++ MetaWindow *window = (MetaWindow *) user_data; ++ return meta_window_is_hidden (window); ++} ++ ++static void ++test_window_mapped_inhibit (MetaWindowClientType client_type) ++{ ++ g_autoptr (GError) error = NULL; ++ MetaTestClient *test_client; ++ MetaWindow *window; ++ ++ g_debug ("Starting window mapped inhibit test"); ++ ++ test_client = meta_test_client_new (test_context, ++ TEST_CLIENT_TITLE, ++ client_type, ++ &error); ++ g_assert_no_error (error); ++ ++ meta_test_client_run (test_client, ++ "create " TEST_CLIENT_TITLE " csd\n" ++ "show " TEST_CLIENT_TITLE "\n"); ++ ++ /* Wait for the window to be created */ ++ while (!(window = meta_test_client_find_window (test_client, ++ TEST_CLIENT_TITLE, ++ NULL))) ++ g_main_context_iteration (NULL, TRUE); ++ g_object_add_weak_pointer (G_OBJECT (window), (gpointer *) &window); ++ ++ /* Wait for the window to be properly shown */ ++ wait_for_condition (window_is_not_hidden, window); ++ ++ /* Initially, the window should not be mapped inhibited */ ++ g_assert_false (meta_window_is_mapped_inhibited (window)); ++ ++ /* The window should be showing normally */ ++ g_assert_false (meta_window_is_hidden (window)); ++ ++ /* Inhibit the window mapping */ ++ meta_window_inhibit_mapped (window); ++ ++ /* Now the window should be mapped inhibited */ ++ g_assert_true (meta_window_is_mapped_inhibited (window)); ++ ++ /* Wait for the window to be processed and hidden */ ++ wait_for_condition (window_is_hidden, window); ++ ++ /* The window should now be hidden due to the inhibit */ ++ g_assert_true (meta_window_is_hidden (window)); ++ ++ /* Uninhibit the window mapping */ ++ meta_window_uninhibit_mapped (window); ++ ++ /* The window should no longer be mapped inhibited */ ++ g_assert_false (meta_window_is_mapped_inhibited (window)); ++ ++ /* Wait for the window to be processed and shown again */ ++ wait_for_condition (window_is_not_hidden, window); ++ ++ /* The window should be showing again */ ++ g_assert_false (meta_window_is_hidden (window)); ++ ++ g_debug ("Window mapped inhibit test passed"); ++ ++ meta_test_client_destroy (test_client); ++ ++ /* Wait for the window to be removed */ ++ while (window) ++ g_main_context_iteration (NULL, TRUE); ++} ++ ++static void ++test_window_mapped_inhibit_multiple (MetaWindowClientType client_type) ++{ ++ g_autoptr (GError) error = NULL; ++ MetaTestClient *test_client; ++ MetaWindow *window; ++ ++ g_debug ("Starting multiple window mapped inhibit test"); ++ ++ test_client = meta_test_client_new (test_context, ++ TEST_CLIENT_TITLE, ++ client_type, ++ &error); ++ g_assert_no_error (error); ++ ++ meta_test_client_run (test_client, ++ "create " TEST_CLIENT_TITLE " csd\n" ++ "show " TEST_CLIENT_TITLE "\n"); ++ ++ /* Wait for the window to be created */ ++ while (!(window = meta_test_client_find_window (test_client, ++ TEST_CLIENT_TITLE, ++ NULL))) ++ g_main_context_iteration (NULL, TRUE); ++ g_object_add_weak_pointer (G_OBJECT (window), (gpointer *) &window); ++ ++ /* Wait for the window to be properly shown */ ++ wait_for_condition (window_is_not_hidden, window); ++ ++ /* Initially, the window should not be mapped inhibited */ ++ g_assert_false (meta_window_is_mapped_inhibited (window)); ++ ++ /* Inhibit the window mapping multiple times */ ++ meta_window_inhibit_mapped (window); ++ g_assert_true (meta_window_is_mapped_inhibited (window)); ++ wait_for_condition (window_is_hidden, window); ++ ++ meta_window_inhibit_mapped (window); ++ g_assert_true (meta_window_is_mapped_inhibited (window)); ++ g_assert_true (meta_window_is_hidden (window)); ++ ++ meta_window_inhibit_mapped (window); ++ g_assert_true (meta_window_is_mapped_inhibited (window)); ++ g_assert_true (meta_window_is_hidden (window)); ++ ++ /* Uninhibit once - should still be inhibited */ ++ meta_window_uninhibit_mapped (window); ++ g_assert_true (meta_window_is_mapped_inhibited (window)); ++ g_assert_true (meta_window_is_hidden (window)); ++ ++ /* Uninhibit again - should still be inhibited */ ++ meta_window_uninhibit_mapped (window); ++ g_assert_true (meta_window_is_mapped_inhibited (window)); ++ g_assert_true (meta_window_is_hidden (window)); ++ ++ /* Uninhibit the final time - should no longer be inhibited */ ++ meta_window_uninhibit_mapped (window); ++ g_assert_false (meta_window_is_mapped_inhibited (window)); ++ wait_for_condition (window_is_not_hidden, window); ++ g_assert_false (meta_window_is_hidden (window)); ++ ++ g_debug ("Multiple window mapped inhibit test passed"); ++ ++ meta_test_client_destroy (test_client); ++ ++ /* Wait for the window to be removed */ ++ while (window) ++ g_main_context_iteration (NULL, TRUE); ++} ++ ++static void ++test_window_mapped_inhibit_wayland (void) ++{ ++ test_window_mapped_inhibit (META_WINDOW_CLIENT_TYPE_WAYLAND); ++} ++ ++static void ++test_window_mapped_inhibit_x11 (void) ++{ ++#ifdef MUTTER_PRIVILEGED_TEST ++ g_test_skip ("Running Xwayland in CI KVM doesn't work currently"); ++#else ++ test_window_mapped_inhibit (META_WINDOW_CLIENT_TYPE_X11); ++#endif ++} ++ ++static void ++test_window_mapped_inhibit_multiple_wayland (void) ++{ ++ test_window_mapped_inhibit_multiple (META_WINDOW_CLIENT_TYPE_WAYLAND); ++} ++ ++static void ++test_window_mapped_inhibit_multiple_x11 (void) ++{ ++#ifdef MUTTER_PRIVILEGED_TEST ++ g_test_skip ("Running Xwayland in CI KVM doesn't work currently"); ++#else ++ test_window_mapped_inhibit_multiple (META_WINDOW_CLIENT_TYPE_X11); ++#endif ++} ++ ++static void ++on_before_tests (void) ++{ ++ MetaWaylandCompositor *compositor = ++ meta_context_get_wayland_compositor (test_context); ++ ++ test_driver = meta_wayland_test_driver_new (compositor); ++ virtual_monitor = meta_create_test_monitor (test_context, ++ 400, 400, 60.0); ++} ++ ++static void ++on_after_tests (void) ++{ ++ g_clear_object (&test_driver); ++ g_clear_object (&virtual_monitor); ++} ++ ++static void ++init_tests (void) ++{ ++ g_test_add_func ("/window/mapped-inhibit", ++ test_window_mapped_inhibit_wayland); ++ g_test_add_func ("/window/mapped-inhibit/x11", ++ test_window_mapped_inhibit_x11); ++ g_test_add_func ("/window/mapped-inhibit/multiple", ++ test_window_mapped_inhibit_multiple_wayland); ++ g_test_add_func ("/window/mapped-inhibit/multiple/x11", ++ test_window_mapped_inhibit_multiple_x11); ++} ++ ++int ++main (int argc, ++ char **argv) ++{ ++ g_autoptr (MetaContext) context = NULL; ++ ++ context = meta_create_test_context (META_CONTEXT_TEST_TYPE_HEADLESS, ++#ifdef MUTTER_PRIVILEGED_TEST ++ META_CONTEXT_TEST_FLAG_NO_X11 | ++#endif ++ META_CONTEXT_TEST_FLAG_TEST_CLIENT); ++ g_assert_true (meta_context_configure (context, &argc, &argv, NULL)); ++ ++ test_context = context; ++ ++ init_tests (); ++ ++ g_signal_connect (context, "before-tests", ++ G_CALLBACK (on_before_tests), NULL); ++ g_signal_connect (context, "after-tests", ++ G_CALLBACK (on_after_tests), NULL); ++ ++ return meta_context_test_run_tests (META_CONTEXT_TEST (context), ++ META_TEST_RUN_FLAG_CAN_SKIP); ++} +-- +2.53.0 + diff --git a/0003-window-Add-MetaExternalConstraintFlags.patch b/0003-window-Add-MetaExternalConstraintFlags.patch new file mode 100644 index 0000000..7ed0afa --- /dev/null +++ b/0003-window-Add-MetaExternalConstraintFlags.patch @@ -0,0 +1,40 @@ +From c2af2bec1618fbac784c61722b073a0cb4637067 Mon Sep 17 00:00:00 2001 +From: Olivier Fourdan +Date: Wed, 5 Nov 2025 17:05:25 +0100 +Subject: [PATCH 3/7] window: Add MetaExternalConstraintFlags + +The plan is to allow plugins to implement their own constraint, while +not exposing all of the internal MetaMoveResizeFlags. + +Part-of: +(cherry picked from commit 3c9155a395c3c07f8aeb928a676e83bd82b7d30e) +--- + src/meta/meta-enums.h | 15 +++++++++++++++ + 1 file changed, 15 insertions(+) + +diff --git a/src/meta/meta-enums.h b/src/meta/meta-enums.h +index e0a2fc50ea..79294d7c15 100644 +--- a/src/meta/meta-enums.h ++++ b/src/meta/meta-enums.h +@@ -418,3 +418,18 @@ typedef enum + META_A11Y_STICKY_KEYS_BEEP = 1 << 12, + META_A11Y_FEATURE_STATE_CHANGE_BEEP = 1 << 13, + } MetaKeyboardA11yFlags; ++ ++/** ++ * MetaMoveResizeFlags: ++ * @META_EXTERNAL_CONSTRAINT_FLAGS_NONE: No operation ++ * @META_EXTERNAL_CONSTRAINT_FLAGS_MOVE: Move operation ++ * @META_EXTERNAL_CONSTRAINT_FLAGS_RESIZE: Resize operation ++ * ++ * Flags for external constraint operations. ++ */ ++typedef enum _MetaExternalConstraintFlags ++{ ++ META_EXTERNAL_CONSTRAINT_FLAGS_NONE = 0, ++ META_EXTERNAL_CONSTRAINT_FLAGS_MOVE = 1 << 0, ++ META_EXTERNAL_CONSTRAINT_FLAGS_RESIZE = 1 << 1, ++} MetaExternalConstraintFlags; +-- +2.53.0 + diff --git a/0004-constraints-Add-external-constraints-interface.patch b/0004-constraints-Add-external-constraints-interface.patch new file mode 100644 index 0000000..4548d11 --- /dev/null +++ b/0004-constraints-Add-external-constraints-interface.patch @@ -0,0 +1,212 @@ +From e0e3307984eaa4ac716ee6431b9d469d890e42be Mon Sep 17 00:00:00 2001 +From: Olivier Fourdan +Date: Fri, 14 Nov 2025 14:04:00 +0100 +Subject: [PATCH 4/7] constraints: Add external constraints interface + +This adds a new type interface for constraints. + +Part-of: +(cherry picked from commit 17b6a69868a38240a9e179239310dc55ae9202d5) +--- + src/core/meta-external-constraint.c | 58 +++++++++++++++++++ + src/meson.build | 1 + + src/meta/meson.build | 1 + + src/meta/meta-external-constraint.h | 87 +++++++++++++++++++++++++++++ + src/meta/types.h | 1 + + 5 files changed, 148 insertions(+) + create mode 100644 src/core/meta-external-constraint.c + create mode 100644 src/meta/meta-external-constraint.h + +diff --git a/src/core/meta-external-constraint.c b/src/core/meta-external-constraint.c +new file mode 100644 +index 0000000000..bc84f8bf97 +--- /dev/null ++++ b/src/core/meta-external-constraint.c +@@ -0,0 +1,58 @@ ++/* ++ * Copyright (C) 2025 Red Hat, Inc. ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License as ++ * published by the Free Software Foundation; either version 2 of the ++ * License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, but ++ * WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program; if not, see . ++ */ ++ ++#include "config.h" ++ ++#include "meta/meta-external-constraint.h" ++ ++G_DEFINE_INTERFACE (MetaExternalConstraint, meta_external_constraint, G_TYPE_OBJECT) ++ ++static void ++meta_external_constraint_default_init (MetaExternalConstraintInterface *iface) ++{ ++} ++ ++/** ++ * meta_external_constraint_constrain: ++ * @constraint: a #MetaExternalConstraint ++ * @window: the #MetaWindow being constrained ++ * @info: a #MetaExternalConstraintInfo with constraint parameters ++ * ++ * Applies external constraints to a window's position and size. ++ * ++ * Returns: %TRUE if the constraint has fully constrained the window, ++ %FALSE otherwise. ++ */ ++gboolean ++meta_external_constraint_constrain (MetaExternalConstraint *constraint, ++ MetaWindow *window, ++ MetaExternalConstraintInfo *info) ++{ ++ MetaExternalConstraintInterface *iface; ++ ++ g_return_val_if_fail (META_IS_EXTERNAL_CONSTRAINT (constraint), TRUE); ++ g_return_val_if_fail (META_IS_WINDOW (window), TRUE); ++ g_return_val_if_fail (info != NULL, TRUE); ++ g_return_val_if_fail (info->new_rect != NULL, TRUE); ++ ++ iface = META_EXTERNAL_CONSTRAINT_GET_IFACE (constraint); ++ ++ if (iface->constrain) ++ return iface->constrain (constraint, window, info); ++ ++ return TRUE; ++} +diff --git a/src/meson.build b/src/meson.build +index 508c3416da..744f941e6e 100644 +--- a/src/meson.build ++++ b/src/meson.build +@@ -360,6 +360,7 @@ mutter_sources = [ + 'core/meta-context.c', + 'core/meta-debug-control.c', + 'core/meta-debug-control-private.h', ++ 'core/meta-external-constraint.c', + 'core/meta-fraction.c', + 'core/meta-fraction.h', + 'core/meta-gesture-tracker.c', +diff --git a/src/meta/meson.build b/src/meta/meson.build +index bc42b56174..f34ace7439 100644 +--- a/src/meta/meson.build ++++ b/src/meta/meson.build +@@ -21,6 +21,7 @@ mutter_public_headers = [ + 'meta-debug-control.h', + 'meta-dnd.h', + 'meta-enums.h', ++ 'meta-external-constraint.h', + 'meta-idle-monitor.h', + 'meta-inhibit-shortcuts-dialog.h', + 'meta-launch-context.h', +diff --git a/src/meta/meta-external-constraint.h b/src/meta/meta-external-constraint.h +new file mode 100644 +index 0000000000..a2d5cd6176 +--- /dev/null ++++ b/src/meta/meta-external-constraint.h +@@ -0,0 +1,87 @@ ++/* ++ * Copyright (C) 2025 Red Hat, Inc. ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License as ++ * published by the Free Software Foundation; either version 2 of the ++ * License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, but ++ * WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program; if not, see . ++ */ ++ ++#pragma once ++ ++#include ++ ++#include "meta/types.h" ++#include "meta/window.h" ++ ++G_BEGIN_DECLS ++ ++#define META_TYPE_EXTERNAL_CONSTRAINT (meta_external_constraint_get_type ()) ++ ++META_EXPORT ++G_DECLARE_INTERFACE (MetaExternalConstraint, ++ meta_external_constraint, ++ META, ++ EXTERNAL_CONSTRAINT, ++ GObject) ++ ++/** ++ * MetaExternalConstraintInfo: ++ * @new_rect: (inout): the proposed new window rectangle ++ * @flags: the constraint flags for this operation ++ * @resize_gravity: the gravity for resizing ++ * ++ * Structure containing parameters for external window constraints. ++ */ ++typedef struct _MetaExternalConstraintInfo MetaExternalConstraintInfo; ++struct _MetaExternalConstraintInfo ++{ ++ MtkRectangle *new_rect; ++ MetaExternalConstraintFlags flags; ++ MetaGravity resize_gravity; ++}; ++ ++/** ++ * MetaExternalConstraintInterface: ++ * @constrain: virtual function for applying external constraints ++ * ++ * Interface for objects that can apply external window constraints. ++ */ ++struct _MetaExternalConstraintInterface ++{ ++ GTypeInterface parent_iface; ++ ++ /** ++ * MetaExternalConstraintInterface::constrain: ++ * @constraint: a #MetaExternalConstraint ++ * @window: the #MetaWindow being constrained ++ * @info: a #MetaExternalConstraintInfo with constraint parameters ++ * ++ * Virtual function called with other window constraints processing. ++ * This allows external components to implement custom window positioning ++ * and sizing constraints. ++ * ++ * The implementation can modify @info->new_rect to enforce its own constraints. ++ * ++ * Returns: %TRUE if the constraint has fully constrained the window, ++ %FALSE otherwise. ++ */ ++ gboolean (*constrain) (MetaExternalConstraint *constraint, ++ MetaWindow *window, ++ MetaExternalConstraintInfo *info); ++}; ++ ++META_EXPORT ++gboolean meta_external_constraint_constrain (MetaExternalConstraint *constraint, ++ MetaWindow *window, ++ MetaExternalConstraintInfo *info); ++ ++G_END_DECLS +diff --git a/src/meta/types.h b/src/meta/types.h +index cf7432ec48..944043d854 100644 +--- a/src/meta/types.h ++++ b/src/meta/types.h +@@ -37,3 +37,4 @@ typedef struct _MetaWorkspaceManager MetaWorkspaceManager; + typedef struct _MetaSelection MetaSelection; + typedef struct _MetaDebugControl MetaDebugControl; + typedef struct _MetaWindowConfig MetaWindowConfig; ++typedef struct _MetaExternalConstraint MetaExternalConstraint; +-- +2.53.0 + diff --git a/0005-window-Add-external-constraints-support.patch b/0005-window-Add-external-constraints-support.patch new file mode 100644 index 0000000..5aa8933 --- /dev/null +++ b/0005-window-Add-external-constraints-support.patch @@ -0,0 +1,176 @@ +From 80ff12ecbb492a6f87b0f22e47977b26b018f4d9 Mon Sep 17 00:00:00 2001 +From: Olivier Fourdan +Date: Fri, 14 Nov 2025 15:16:30 +0100 +Subject: [PATCH 5/7] window: Add external constraints support + +This allows to keep a list of external constraints with the window. + +The caller can add/remove constraint objects to each window. + +This is preparation work for the following commit that will call into +this to apply external window constraints along the rest of the +constraints. + +Part-of: +(cherry picked from commit 0eaf17634ecda569b519ab73cf79f21be1f631f2) +--- + src/core/window-private.h | 5 +++ + src/core/window.c | 87 +++++++++++++++++++++++++++++++++++++++ + src/meta/window.h | 8 ++++ + 3 files changed, 100 insertions(+) + +diff --git a/src/core/window-private.h b/src/core/window-private.h +index 01f6679ac3..e35bc66a82 100644 +--- a/src/core/window-private.h ++++ b/src/core/window-private.h +@@ -937,3 +937,8 @@ void meta_window_set_tag (MetaWindow *window, + + META_EXPORT_TEST + GPtrArray * meta_window_get_transient_children (MetaWindow *window); ++ ++gboolean meta_window_apply_external_constraints (MetaWindow *window, ++ MetaGravity resize_gravity, ++ MtkRectangle *constrained_rect, ++ MetaExternalConstraintFlags constraint_flags); +diff --git a/src/core/window.c b/src/core/window.c +index b7b62fbda5..5ea7c1bc2a 100644 +--- a/src/core/window.c ++++ b/src/core/window.c +@@ -80,6 +80,7 @@ + #include "core/workspace-private.h" + #include "meta/meta-cursor-tracker.h" + #include "meta/meta-enum-types.h" ++#include "meta/meta-external-constraint.h" + #include "meta/prefs.h" + #include "meta/meta-window-config.h" + +@@ -193,6 +194,8 @@ typedef struct _MetaWindowPrivate + } auto_maximize; + + unsigned int mapped_inhibit_count; ++ ++ GHashTable *external_constraints; + } MetaWindowPrivate; + + G_DEFINE_ABSTRACT_TYPE_WITH_CODE (MetaWindow, meta_window, G_TYPE_OBJECT, +@@ -415,6 +418,8 @@ meta_window_finalize (GObject *object) + g_free (window->placement.rule); + g_free (window->tag); + ++ g_clear_pointer (&priv->external_constraints, g_hash_table_destroy); ++ + G_OBJECT_CLASS (meta_window_parent_class)->finalize (object); + } + +@@ -837,6 +842,8 @@ meta_window_init (MetaWindow *window) + window->stamp = next_window_stamp++; + meta_prefs_add_listener (prefs_changed_callback, window); + window->is_alive = TRUE; ++ priv->external_constraints = ++ g_hash_table_new_full (NULL, NULL, g_object_unref, NULL); + } + + static gboolean +@@ -8753,3 +8760,83 @@ meta_window_is_mapped_inhibited (MetaWindow *window) + + return priv->mapped_inhibit_count > 0; + } ++ ++/** ++ * meta_window_add_external_constraint: ++ * @window: a #MetaWindow ++ * @constraint: a #MetaExternalConstraint ++ * ++ * Adds an external constraint to the window. ++ * ++ * The constraint object is referenced by the window, so the caller should ++ * release its own reference when no longer needed. ++ */ ++void ++meta_window_add_external_constraint (MetaWindow *window, ++ MetaExternalConstraint *constraint) ++{ ++ MetaWindowPrivate *priv; ++ ++ g_return_if_fail (META_IS_WINDOW (window)); ++ g_return_if_fail (META_IS_EXTERNAL_CONSTRAINT (constraint)); ++ ++ priv = meta_window_get_instance_private (window); ++ if (g_hash_table_contains (priv->external_constraints, constraint)) ++ { ++ g_warning ("Not adding external window constraint, already present"); ++ return; ++ } ++ ++ g_hash_table_add (priv->external_constraints, g_object_ref (constraint)); ++} ++ ++/** ++ * meta_window_remove_external_constraint: ++ * @window: a #MetaWindow ++ * @constraint: a #MetaExternalConstraint ++ * ++ * Removes a previously added external constraint from the window. ++ */ ++void ++meta_window_remove_external_constraint (MetaWindow *window, ++ MetaExternalConstraint *constraint) ++{ ++ MetaWindowPrivate *priv; ++ ++ g_return_if_fail (META_IS_WINDOW (window)); ++ g_return_if_fail (META_IS_EXTERNAL_CONSTRAINT (constraint)); ++ ++ priv = meta_window_get_instance_private (window); ++ g_hash_table_remove (priv->external_constraints, constraint); ++} ++ ++gboolean ++meta_window_apply_external_constraints (MetaWindow *window, ++ MetaGravity resize_gravity, ++ MtkRectangle *constrained_rect, ++ MetaExternalConstraintFlags constraint_flags) ++{ ++ MetaWindowPrivate *priv = meta_window_get_instance_private (window); ++ GHashTableIter iter; ++ gpointer constraint_ptr; ++ gboolean constraint_satisfied = TRUE; ++ ++ g_return_val_if_fail (META_IS_WINDOW (window), TRUE); ++ ++ g_hash_table_iter_init (&iter, priv->external_constraints); ++ while (g_hash_table_iter_next (&iter, &constraint_ptr, NULL)) ++ { ++ MetaExternalConstraint *constraint = META_EXTERNAL_CONSTRAINT (constraint_ptr); ++ MetaExternalConstraintInfo constraint_info = { ++ .new_rect = constrained_rect, ++ .flags = constraint_flags, ++ .resize_gravity = resize_gravity, ++ }; ++ ++ constraint_satisfied |= meta_external_constraint_constrain (constraint, ++ window, ++ &constraint_info); ++ } ++ ++ return constraint_satisfied; ++} +diff --git a/src/meta/window.h b/src/meta/window.h +index 4f6390f0a1..a677122716 100644 +--- a/src/meta/window.h ++++ b/src/meta/window.h +@@ -469,3 +469,11 @@ void meta_window_uninhibit_mapped (MetaWindow *window); + + META_EXPORT + gboolean meta_window_is_mapped_inhibited (MetaWindow *window); ++ ++META_EXPORT ++void meta_window_add_external_constraint (MetaWindow *window, ++ MetaExternalConstraint *constraint); ++ ++META_EXPORT ++void meta_window_remove_external_constraint (MetaWindow *window, ++ MetaExternalConstraint *constraint); +-- +2.53.0 + diff --git a/0006-constraints-Apply-window-external-constraints.patch b/0006-constraints-Apply-window-external-constraints.patch new file mode 100644 index 0000000..f833cf0 --- /dev/null +++ b/0006-constraints-Apply-window-external-constraints.patch @@ -0,0 +1,103 @@ +From 9b0f9f6346c9ffa6422f34d1232dcb1905794828 Mon Sep 17 00:00:00 2001 +From: Olivier Fourdan +Date: Wed, 5 Nov 2025 17:11:47 +0100 +Subject: [PATCH 6/7] constraints: Apply window external constraints + +Apply the window external constraints last, yet at the highest priority. + +Part-of: +(cherry picked from commit d7f0f7a7a477bfd452614f5c35c78a5743f9cec4) +--- + src/core/constraints.c | 48 +++++++++++++++++++++++++++++++++++++++++- + 1 file changed, 47 insertions(+), 1 deletion(-) + +diff --git a/src/core/constraints.c b/src/core/constraints.c +index fe4f6b3de3..bdea6b5c2b 100644 +--- a/src/core/constraints.c ++++ b/src/core/constraints.c +@@ -37,6 +37,7 @@ + #include "core/meta-workspace-manager-private.h" + #include "core/place.h" + #include "core/workspace-private.h" ++#include "meta/meta-external-constraint.h" + #include "meta/prefs.h" + + #ifdef HAVE_X11_CLIENT +@@ -115,7 +116,8 @@ typedef enum + PRIORITY_TITLEBAR_VISIBLE = 4, + PRIORITY_PARTIALLY_VISIBLE_ON_WORKAREA = 4, + PRIORITY_CUSTOM_RULE = 4, +- PRIORITY_MAXIMUM = 4 /* Dummy value used for loop end = max(all priorities) */ ++ PRIORITY_EXTERNAL_CONSTRAINT = 5, /* External constraints - highest priority */ ++ PRIORITY_MAXIMUM = 5 /* Dummy value used for loop end = max(all priorities) */ + } ConstraintPriority; + + typedef enum +@@ -164,6 +166,10 @@ static gboolean do_screen_and_monitor_relative_constraints (MetaWindow *wind + GList *region_spanning_rectangles, + ConstraintInfo *info, + gboolean check_only); ++static gboolean constrain_external (MetaWindow *window, ++ ConstraintInfo *info, ++ ConstraintPriority priority, ++ gboolean check_only); + static gboolean constrain_custom_rule (MetaWindow *window, + ConstraintInfo *info, + ConstraintPriority priority, +@@ -249,6 +255,7 @@ static const Constraint all_constraints[] = { + {constrain_fully_onscreen, "constrain_fully_onscreen"}, + {constrain_titlebar_visible, "constrain_titlebar_visible"}, + {constrain_partially_onscreen, "constrain_partially_onscreen"}, ++ {constrain_external, "constrain_external"}, + {NULL, NULL} + }; + +@@ -877,6 +884,45 @@ is_custom_rule_satisfied (MtkRectangle *rect, + return TRUE; + } + ++static MetaExternalConstraintFlags ++get_external_constraint_flags (ConstraintInfo *info) ++{ ++ MetaExternalConstraintFlags flags = META_EXTERNAL_CONSTRAINT_FLAGS_NONE; ++ ++ if (info->flags & META_MOVE_RESIZE_MOVE_ACTION) ++ flags |= META_EXTERNAL_CONSTRAINT_FLAGS_MOVE; ++ if (info->flags & META_MOVE_RESIZE_RESIZE_ACTION) ++ flags |= META_EXTERNAL_CONSTRAINT_FLAGS_RESIZE; ++ ++ return flags; ++} ++ ++static gboolean ++constrain_external (MetaWindow *window, ++ ConstraintInfo *info, ++ ConstraintPriority priority, ++ gboolean check_only) ++{ ++ MtkRectangle current_rect = info->current; ++ gboolean constraint_satisfied; ++ ++ if (priority > PRIORITY_EXTERNAL_CONSTRAINT) ++ return TRUE; ++ ++ constraint_satisfied = ++ meta_window_apply_external_constraints (window, ++ info->resize_gravity, ++ ¤t_rect, ++ get_external_constraint_flags (info)); ++ ++ if (check_only) ++ return constraint_satisfied; ++ ++ info->current = current_rect; ++ ++ return constraint_satisfied; ++} ++ + static gboolean + constrain_custom_rule (MetaWindow *window, + ConstraintInfo *info, +-- +2.53.0 + diff --git a/0007-tests-Add-test-for-external-constraints.patch b/0007-tests-Add-test-for-external-constraints.patch new file mode 100644 index 0000000..8095f05 --- /dev/null +++ b/0007-tests-Add-test-for-external-constraints.patch @@ -0,0 +1,564 @@ +From f4b835ff3c34dded177e51419a09da105089c085 Mon Sep 17 00:00:00 2001 +From: Olivier Fourdan +Date: Wed, 5 Nov 2025 17:19:33 +0100 +Subject: [PATCH 7/7] tests: Add test for external constraints + +Test the new external constraints, by checking that the size and +location set through external constraints get enforced on initial +placement, moving, resizing, maximizing and fullscreening the window. + +Part-of: +(cherry picked from commit aa803d6fe1f177eae5039913564499eeb2b31f55) +--- + src/tests/external-constraint-tests.c | 519 ++++++++++++++++++++++++++ + src/tests/meson.build | 8 + + 2 files changed, 527 insertions(+) + create mode 100644 src/tests/external-constraint-tests.c + +diff --git a/src/tests/external-constraint-tests.c b/src/tests/external-constraint-tests.c +new file mode 100644 +index 0000000000..46bf2461c4 +--- /dev/null ++++ b/src/tests/external-constraint-tests.c +@@ -0,0 +1,519 @@ ++/* ++ * Copyright (C) 2025 Red Hat, Inc. ++ * ++ * This program is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU General Public License as ++ * published by the Free Software Foundation; either version 2 of the ++ * License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, but ++ * WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program; if not, see . ++ */ ++ ++#include "config.h" ++ ++#include "backends/meta-virtual-monitor.h" ++#include "compositor/meta-window-actor-private.h" ++#include "core/window-private.h" ++#include "meta/display.h" ++#include "meta/meta-external-constraint.h" ++#include "meta-test/meta-context-test.h" ++#include "tests/meta-test-shell.h" ++#include "tests/meta-test-utils.h" ++#include "tests/meta-wayland-test-driver.h" ++ ++#define TEST_CLIENT_TITLE "external-constraint-test-window" ++ ++static MetaContext *test_context; ++static MetaWaylandTestDriver *test_driver; ++static MetaVirtualMonitor *virtual_monitor; ++ ++/* Global state for constraint testing */ ++static struct { ++ gboolean enabled; ++ MtkRectangle target_rect; ++ gboolean was_called; ++ MetaWindow *expected_window; ++} constraint_state; ++ ++/* Define our test constraint type */ ++#define TEST_TYPE_CONSTRAINT (test_constraint_get_type ()) ++G_DECLARE_FINAL_TYPE (TestConstraint, test_constraint, ++ TEST, CONSTRAINT, GObject) ++ ++struct _TestConstraint ++{ ++ GObject parent_instance; ++}; ++ ++static void test_constraint_external_constraint_iface_init (MetaExternalConstraintInterface *iface); ++ ++G_DEFINE_TYPE_WITH_CODE (TestConstraint, test_constraint, G_TYPE_OBJECT, ++ G_IMPLEMENT_INTERFACE (META_TYPE_EXTERNAL_CONSTRAINT, ++ test_constraint_external_constraint_iface_init)) ++ ++/* Our test constraint function */ ++static gboolean ++test_constraint_constrain (MetaExternalConstraint *constraint, ++ MetaWindow *window, ++ MetaExternalConstraintInfo *info) ++{ ++ if (!constraint_state.enabled) ++ return FALSE; ++ ++ /* Apply our test constraint */ ++ *info->new_rect = constraint_state.target_rect; ++ constraint_state.was_called = TRUE; ++ constraint_state.expected_window = window; ++ ++ return TRUE; /* Skip other constraints */ ++} ++ ++static void ++test_constraint_external_constraint_iface_init (MetaExternalConstraintInterface *iface) ++{ ++ iface->constrain = test_constraint_constrain; ++} ++ ++static void ++test_constraint_class_init (TestConstraintClass *klass) ++{ ++} ++ ++static void ++test_constraint_init (TestConstraint *constraint) ++{ ++} ++ ++static void ++on_effects_completed (MetaWindowActor *window_actor, ++ gboolean *done) ++{ ++ *done = TRUE; ++} ++ ++static void ++wait_for_window_added (MetaWindow *window) ++{ ++ MetaWindowActor *window_actor; ++ gboolean done = FALSE; ++ gulong handler_id; ++ ++ window_actor = meta_window_actor_from_window (window); ++ handler_id = g_signal_connect (window_actor, "effects-completed", ++ G_CALLBACK (on_effects_completed), &done); ++ ++ while (!done) ++ g_main_context_iteration (NULL, TRUE); ++ ++ g_signal_handler_disconnect (window_actor, handler_id); ++} ++ ++/* Test: Basic external constraint on window creation */ ++static void ++test_external_constraint_basic (void) ++{ ++ g_autoptr (GError) error = NULL; ++ MetaTestClient *test_client; ++ MetaWindow *window = NULL; ++ MtkRectangle frame_rect; ++ ++ /* Configure constraint */ ++ constraint_state.enabled = TRUE; ++ constraint_state.target_rect = (MtkRectangle) { 100, 150, 300, 200 }; ++ constraint_state.was_called = FALSE; ++ ++ /* Create test window */ ++ test_client = meta_test_client_new (test_context, ++ "external-constraint-test-client", ++ META_WINDOW_CLIENT_TYPE_WAYLAND, ++ &error); ++ g_assert_nonnull (test_client); ++ g_assert_no_error (error); ++ ++ meta_test_client_run (test_client, ++ "create " TEST_CLIENT_TITLE " csd\n" ++ "show " TEST_CLIENT_TITLE "\n"); ++ ++ while (!(window = meta_test_client_find_window (test_client, ++ TEST_CLIENT_TITLE, ++ NULL))) ++ g_main_context_iteration (NULL, TRUE); ++ ++ g_object_add_weak_pointer (G_OBJECT (window), (gpointer *) &window); ++ wait_for_window_added (window); ++ ++ /* Verify constraint was called */ ++ g_assert_true (constraint_state.was_called); ++ g_assert_true (constraint_state.expected_window == window); ++ ++ /* Verify window position matches constraint */ ++ meta_window_get_frame_rect (window, &frame_rect); ++ ++ g_assert_cmpint (frame_rect.x, ==, constraint_state.target_rect.x); ++ g_assert_cmpint (frame_rect.y, ==, constraint_state.target_rect.y); ++ g_assert_cmpint (frame_rect.width, ==, constraint_state.target_rect.width); ++ g_assert_cmpint (frame_rect.height, ==, constraint_state.target_rect.height); ++ ++ /* Cleanup */ ++ meta_test_client_destroy (test_client); ++ while (window) ++ g_main_context_iteration (NULL, TRUE); ++ ++ constraint_state.enabled = FALSE; ++ constraint_state.expected_window = NULL; ++} ++ ++/* Test: External constraint prevents window move */ ++static void ++test_external_constraint_move (void) ++{ ++ g_autoptr (GError) error = NULL; ++ MetaTestClient *test_client; ++ MetaWindow *window = NULL; ++ MtkRectangle frame_rect; ++ ++ /* Configure constraint to fixed position */ ++ constraint_state.enabled = TRUE; ++ constraint_state.target_rect = (MtkRectangle) { 200, 100, 400, 300 }; ++ constraint_state.was_called = FALSE; ++ ++ /* Create window */ ++ test_client = meta_test_client_new (test_context, ++ "external-constraint-test-client", ++ META_WINDOW_CLIENT_TYPE_WAYLAND, ++ &error); ++ g_assert_nonnull (test_client); ++ g_assert_no_error (error); ++ ++ meta_test_client_run (test_client, ++ "create " TEST_CLIENT_TITLE " csd\n" ++ "show " TEST_CLIENT_TITLE "\n"); ++ ++ while (!(window = meta_test_client_find_window (test_client, ++ TEST_CLIENT_TITLE, ++ NULL))) ++ g_main_context_iteration (NULL, TRUE); ++ ++ g_object_add_weak_pointer (G_OBJECT (window), (gpointer *) &window); ++ wait_for_window_added (window); ++ ++ /* Verify initial position */ ++ meta_window_get_frame_rect (window, &frame_rect); ++ ++ /* Try to move window */ ++ constraint_state.was_called = FALSE; ++ ++ meta_window_move_frame (window, TRUE, 500, 500); ++ ++ while (g_main_context_pending (NULL)) ++ g_main_context_iteration (NULL, FALSE); ++ ++ /* Verify constraint prevented the move */ ++ g_assert_true (constraint_state.was_called); ++ ++ meta_window_get_frame_rect (window, &frame_rect); ++ ++ g_assert_cmpint (frame_rect.x, ==, constraint_state.target_rect.x); ++ g_assert_cmpint (frame_rect.y, ==, constraint_state.target_rect.y); ++ ++ /* Cleanup */ ++ meta_test_client_destroy (test_client); ++ while (window) ++ g_main_context_iteration (NULL, TRUE); ++ ++ constraint_state.enabled = FALSE; ++ constraint_state.expected_window = NULL; ++} ++ ++/* Test: External constraint limits maximized window size */ ++static void ++test_external_constraint_maximized (void) ++{ ++ g_autoptr (GError) error = NULL; ++ MetaTestClient *test_client; ++ MetaWindow *window = NULL; ++ MtkRectangle frame_rect; ++ ++ /* Configure constraint to a smaller size than monitor */ ++ constraint_state.enabled = TRUE; ++ constraint_state.target_rect = (MtkRectangle) { 50, 50, 400, 300 }; ++ constraint_state.was_called = FALSE; ++ ++ /* Create window */ ++ test_client = meta_test_client_new (test_context, ++ "external-constraint-test-client", ++ META_WINDOW_CLIENT_TYPE_WAYLAND, ++ &error); ++ g_assert_nonnull (test_client); ++ g_assert_no_error (error); ++ ++ meta_test_client_run (test_client, ++ "create " TEST_CLIENT_TITLE " csd\n" ++ "show " TEST_CLIENT_TITLE "\n"); ++ ++ while (!(window = meta_test_client_find_window (test_client, ++ TEST_CLIENT_TITLE, ++ NULL))) ++ g_main_context_iteration (NULL, TRUE); ++ ++ g_object_add_weak_pointer (G_OBJECT (window), (gpointer *) &window); ++ wait_for_window_added (window); ++ ++ /* Maximize the window */ ++ constraint_state.was_called = FALSE; ++ meta_window_maximize (window); ++ ++ while (g_main_context_pending (NULL)) ++ g_main_context_iteration (NULL, FALSE); ++ ++ /* Verify constraint was called during maximize */ ++ g_assert_true (constraint_state.was_called); ++ ++ /* Verify maximized window respects constraint size */ ++ meta_window_get_frame_rect (window, &frame_rect); ++ ++ g_assert_cmpint (frame_rect.x, ==, constraint_state.target_rect.x); ++ g_assert_cmpint (frame_rect.y, ==, constraint_state.target_rect.y); ++ g_assert_cmpint (frame_rect.width, ==, constraint_state.target_rect.width); ++ g_assert_cmpint (frame_rect.height, ==, constraint_state.target_rect.height); ++ ++ /* Cleanup */ ++ meta_test_client_destroy (test_client); ++ while (window) ++ g_main_context_iteration (NULL, TRUE); ++ ++ constraint_state.enabled = FALSE; ++ constraint_state.expected_window = NULL; ++} ++ ++/* Test: External constraint limits fullscreen window size */ ++static void ++test_external_constraint_fullscreen (void) ++{ ++ g_autoptr (GError) error = NULL; ++ MetaTestClient *test_client; ++ MetaWindow *window = NULL; ++ MtkRectangle frame_rect; ++ ++ /* Configure constraint to a smaller size than monitor */ ++ constraint_state.enabled = TRUE; ++ constraint_state.target_rect = (MtkRectangle) { 100, 80, 500, 350 }; ++ constraint_state.was_called = FALSE; ++ ++ /* Create window */ ++ test_client = meta_test_client_new (test_context, ++ "external-constraint-test-client", ++ META_WINDOW_CLIENT_TYPE_WAYLAND, ++ &error); ++ g_assert_nonnull (test_client); ++ g_assert_no_error (error); ++ ++ meta_test_client_run (test_client, ++ "create " TEST_CLIENT_TITLE " csd\n" ++ "show " TEST_CLIENT_TITLE "\n"); ++ ++ while (!(window = meta_test_client_find_window (test_client, ++ TEST_CLIENT_TITLE, ++ NULL))) ++ g_main_context_iteration (NULL, TRUE); ++ ++ g_object_add_weak_pointer (G_OBJECT (window), (gpointer *) &window); ++ wait_for_window_added (window); ++ ++ /* Make the window fullscreen */ ++ constraint_state.was_called = FALSE; ++ meta_window_make_fullscreen (window); ++ ++ while (g_main_context_pending (NULL)) ++ g_main_context_iteration (NULL, FALSE); ++ ++ /* Verify constraint was called during fullscreen */ ++ g_assert_true (constraint_state.was_called); ++ ++ /* Verify fullscreen window respects constraint size */ ++ meta_window_get_frame_rect (window, &frame_rect); ++ ++ g_assert_cmpint (frame_rect.x, ==, constraint_state.target_rect.x); ++ g_assert_cmpint (frame_rect.y, ==, constraint_state.target_rect.y); ++ g_assert_cmpint (frame_rect.width, ==, constraint_state.target_rect.width); ++ g_assert_cmpint (frame_rect.height, ==, constraint_state.target_rect.height); ++ ++ /* Cleanup */ ++ meta_test_client_destroy (test_client); ++ while (window) ++ g_main_context_iteration (NULL, TRUE); ++ ++ constraint_state.enabled = FALSE; ++ constraint_state.expected_window = NULL; ++} ++ ++/* Test: External constraint applies on window resize */ ++static void ++test_external_constraint_resize (void) ++{ ++ g_autoptr (GError) error = NULL; ++ MetaTestClient *test_client; ++ MetaWindow *window = NULL; ++ MtkRectangle frame_rect; ++ ++ /* Configure constraint to limit window size */ ++ constraint_state.enabled = TRUE; ++ constraint_state.target_rect = (MtkRectangle) { 150, 100, 350, 250 }; ++ constraint_state.was_called = FALSE; ++ ++ /* Create window */ ++ test_client = meta_test_client_new (test_context, ++ "external-constraint-test-client", ++ META_WINDOW_CLIENT_TYPE_WAYLAND, ++ &error); ++ g_assert_nonnull (test_client); ++ g_assert_no_error (error); ++ ++ meta_test_client_run (test_client, ++ "create " TEST_CLIENT_TITLE " csd\n" ++ "show " TEST_CLIENT_TITLE "\n"); ++ ++ while (!(window = meta_test_client_find_window (test_client, ++ TEST_CLIENT_TITLE, ++ NULL))) ++ g_main_context_iteration (NULL, TRUE); ++ ++ g_object_add_weak_pointer (G_OBJECT (window), (gpointer *) &window); ++ wait_for_window_added (window); ++ ++ /* Verify initial constraint was applied */ ++ meta_window_get_frame_rect (window, &frame_rect); ++ g_assert_cmpint (frame_rect.width, ==, constraint_state.target_rect.width); ++ g_assert_cmpint (frame_rect.height, ==, constraint_state.target_rect.height); ++ ++ /* Try to resize window to a larger size via the client */ ++ constraint_state.was_called = FALSE; ++ meta_window_move_resize_frame (window, TRUE, 10, 10, 500, 400); ++ ++ /* Wait for the resize to be processed */ ++ while (g_main_context_pending (NULL)) ++ g_main_context_iteration (NULL, FALSE); ++ ++ /* Give it a bit more time to ensure all constraint processing completes */ ++ g_usleep (50000); /* 50ms */ ++ ++ while (g_main_context_pending (NULL)) ++ g_main_context_iteration (NULL, FALSE); ++ ++ /* Verify constraint was called during client-initiated resize */ ++ g_assert_true (constraint_state.was_called); ++ ++ /* Verify window size is constrained to our limit, not the requested 500x400 */ ++ meta_window_get_frame_rect (window, &frame_rect); ++ ++ g_assert_cmpint (frame_rect.x, >=, constraint_state.target_rect.x); ++ g_assert_cmpint (frame_rect.y, >=, constraint_state.target_rect.y); ++ g_assert_cmpint (frame_rect.width, <=, constraint_state.target_rect.width); ++ g_assert_cmpint (frame_rect.height, <=, constraint_state.target_rect.height); ++ ++ /* Cleanup */ ++ meta_test_client_destroy (test_client); ++ while (window) ++ g_main_context_iteration (NULL, TRUE); ++ ++ constraint_state.enabled = FALSE; ++ constraint_state.expected_window = NULL; ++} ++ ++static MetaExternalConstraint *test_constraint = NULL; ++ ++static void ++on_window_created (MetaDisplay *display, ++ MetaWindow *window, ++ gpointer user_data) ++{ ++ /* Add constraint to every newly created window */ ++ if (test_constraint) ++ meta_window_add_external_constraint (window, test_constraint); ++} ++ ++static void ++on_before_tests (void) ++{ ++ MetaWaylandCompositor *compositor; ++ MetaDisplay *display; ++ ++ /* Create our constraint */ ++ test_constraint = g_object_new (TEST_TYPE_CONSTRAINT, NULL); ++ ++ /* Connect to window-created signal to add constraint to new windows */ ++ display = meta_context_get_display (test_context); ++ g_signal_connect (display, "window-created", ++ G_CALLBACK (on_window_created), NULL); ++ ++ /* Setup test environment */ ++ compositor = meta_context_get_wayland_compositor (test_context); ++ test_driver = meta_wayland_test_driver_new (compositor); ++ virtual_monitor = meta_create_test_monitor (test_context, 640, 480, 60.0); ++} ++ ++static void ++on_after_tests (void) ++{ ++ MetaDisplay *display; ++ ++ /* Disconnect signal handler */ ++ display = meta_context_get_display (test_context); ++ g_signal_handlers_disconnect_by_func (display, ++ G_CALLBACK (on_window_created), ++ NULL); ++ ++ /* Cleanup constraint */ ++ g_clear_object (&test_constraint); ++ ++ g_clear_object (&test_driver); ++ g_clear_object (&virtual_monitor); ++} ++ ++static void ++init_tests (void) ++{ ++ g_test_add_func ("/backends/external-constraints/basic", ++ test_external_constraint_basic); ++ g_test_add_func ("/backends/external-constraints/move", ++ test_external_constraint_move); ++ g_test_add_func ("/backends/external-constraints/resize", ++ test_external_constraint_resize); ++ g_test_add_func ("/backends/external-constraints/maximized", ++ test_external_constraint_maximized); ++ g_test_add_func ("/backends/external-constraints/fullscreen", ++ test_external_constraint_fullscreen); ++} ++ ++int ++main (int argc, ++ char **argv) ++{ ++ g_autoptr (MetaContext) context = NULL; ++ g_autoptr (GError) error = NULL; ++ ++ context = meta_create_test_context (META_CONTEXT_TEST_TYPE_HEADLESS, ++#ifdef MUTTER_PRIVILEGED_TEST ++ META_CONTEXT_TEST_FLAG_NO_X11 | ++#endif ++ META_CONTEXT_TEST_FLAG_TEST_CLIENT); ++ g_assert_true (meta_context_configure (context, &argc, &argv, NULL)); ++ ++ test_context = context; ++ ++ init_tests (); ++ ++ g_signal_connect (context, "before-tests", ++ G_CALLBACK (on_before_tests), NULL); ++ g_signal_connect (context, "after-tests", ++ G_CALLBACK (on_after_tests), NULL); ++ ++ return meta_context_test_run_tests (META_CONTEXT_TEST (context), ++ META_TEST_RUN_FLAG_NONE); ++} +diff --git a/src/tests/meson.build b/src/tests/meson.build +index 8c824b5752..9b1a1edf01 100644 +--- a/src/tests/meson.build ++++ b/src/tests/meson.build +@@ -955,6 +955,14 @@ wayland_test_cases = [ + wayland_test_utils, + ], + }, ++ { ++ 'name': 'external-constraint', ++ 'suite': 'wayland', ++ 'sources': [ ++ 'external-constraint-tests.c', ++ wayland_test_utils, ++ ], ++ }, + { + 'name': 'window-mapped-inhibit', + 'suite': 'wayland', +-- +2.53.0 + diff --git a/mutter.spec b/mutter.spec index d4aa77a..6f6c36a 100644 --- a/mutter.spec +++ b/mutter.spec @@ -46,6 +46,16 @@ Patch: 0001-Revert-tests-monitor-backlight-Add-tests-for-the-sys.patch Patch: 0001-mtk-Take-explicit-reference-for-mtk_extrapolate_next.patch Patch: 0001-tests-test-runner-Do-a-client-roundtrip-after-popup-.patch +# Locking windows on monitor - https://issues.redhat.com/browse/RHEL-155377 +# https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4692 +Patch: 0001-window-Add-a-map-inhibit-API.patch +Patch: 0002-tests-Add-tests-for-map-inhibit.patch +Patch: 0003-window-Add-MetaExternalConstraintFlags.patch +Patch: 0004-constraints-Add-external-constraints-interface.patch +Patch: 0005-window-Add-external-constraints-support.patch +Patch: 0006-constraints-Apply-window-external-constraints.patch +Patch: 0007-tests-Add-test-for-external-constraints.patch + BuildRequires: pkgconfig(gobject-introspection-1.0) >= 1.41.0 BuildRequires: pkgconfig(sm) BuildRequires: pkgconfig(libadwaita-1)