Locking windows on monitor

Resolves: https://issues.redhat.com/browse/RHEL-155379
This commit is contained in:
Olivier Fourdan 2026-03-12 17:10:10 +01:00
parent d7457fe9e0
commit 91299a5f68
8 changed files with 1555 additions and 0 deletions

View File

@ -0,0 +1,124 @@
From 362fe3dd06395405160114d76b35099ba9e79e2d Mon Sep 17 00:00:00 2001
From: Olivier Fourdan <ofourdan@redhat.com>
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: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4692>
(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

View File

@ -0,0 +1,326 @@
From faf8b0a563b4078a990337811e1c97b0de9f1971 Mon Sep 17 00:00:00 2001
From: Olivier Fourdan <ofourdan@redhat.com>
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: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4692>
(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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+
+#include <glib.h>
+
+#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

View File

@ -0,0 +1,40 @@
From c2af2bec1618fbac784c61722b073a0cb4637067 Mon Sep 17 00:00:00 2001
From: Olivier Fourdan <ofourdan@redhat.com>
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: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4692>
(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

View File

@ -0,0 +1,212 @@
From e0e3307984eaa4ac716ee6431b9d469d890e42be Mon Sep 17 00:00:00 2001
From: Olivier Fourdan <ofourdan@redhat.com>
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: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4692>
(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 <http://www.gnu.org/licenses/>.
+ */
+
+#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 <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#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

View File

@ -0,0 +1,176 @@
From 80ff12ecbb492a6f87b0f22e47977b26b018f4d9 Mon Sep 17 00:00:00 2001
From: Olivier Fourdan <ofourdan@redhat.com>
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: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4692>
(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

View File

@ -0,0 +1,103 @@
From 9b0f9f6346c9ffa6422f34d1232dcb1905794828 Mon Sep 17 00:00:00 2001
From: Olivier Fourdan <ofourdan@redhat.com>
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: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4692>
(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,
+ &current_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

View File

@ -0,0 +1,564 @@
From f4b835ff3c34dded177e51419a09da105089c085 Mon Sep 17 00:00:00 2001
From: Olivier Fourdan <ofourdan@redhat.com>
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: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4692>
(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 <http://www.gnu.org/licenses/>.
+ */
+
+#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

View File

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