diff --git a/meson.build b/meson.build
index 6a310376e..709759f96 100644
--- a/meson.build
+++ b/meson.build
@@ -20,6 +20,7 @@ glib_req = '>= 2.69.0'
gi_req = '>= 0.9.5'
graphene_req = '>= 1.10.2'
gtk3_req = '>= 3.19.8'
+gtk4_req = '>= 4.0.0'
gdk_pixbuf_req = '>= 2.0'
uprof_req = '>= 0.3'
pango_req = '>= 1.46.0'
@@ -105,6 +106,7 @@ mutter_installed_tests_libexecdir = join_paths(
m_dep = cc.find_library('m', required: true)
graphene_dep = dependency('graphene-gobject-1.0', version: graphene_req)
gtk3_dep = dependency('gtk+-3.0', version: gtk3_req)
+gtk4_dep = dependency('gtk4', version: gtk4_req)
gdk_pixbuf_dep = dependency('gdk-pixbuf-2.0')
pango_dep = dependency('pango', version: pango_req)
cairo_dep = dependency('cairo', version: cairo_req)
diff --git a/src/frames/main.c b/src/frames/main.c
new file mode 100644
index 000000000..b6e429b4d
--- /dev/null
+++ b/src/frames/main.c
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2022 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 .
+ *
+ * Author: Carlos Garnacho
+ */
+
+#include "config.h"
+
+#include "meta-window-tracker.h"
+
+#include
+#include
+#include
+
+static gboolean
+on_sigterm (gpointer user_data)
+{
+ GMainLoop *main_loop = user_data;
+
+ g_main_loop_quit (main_loop);
+
+ return G_SOURCE_REMOVE;
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ g_autoptr (MetaWindowTracker) window_tracker = NULL;
+ GdkDisplay *display;
+ GMainLoop *loop;
+ Display *xdisplay;
+
+ /* We do know the desired GDK backend, don't let
+ * anyone tell us otherwise.
+ */
+ g_unsetenv ("GDK_BACKEND");
+
+ gdk_set_allowed_backends ("x11");
+
+ g_set_prgname ("mutter-x11-frames");
+
+ gtk_init ();
+
+ display = gdk_display_get_default ();
+
+ xdisplay = gdk_x11_display_get_xdisplay (display);
+ XFixesSetClientDisconnectMode (xdisplay,
+ XFixesClientDisconnectFlagTerminate);
+
+ window_tracker = meta_window_tracker_new (display);
+
+ loop = g_main_loop_new (NULL, FALSE);
+ g_unix_signal_add (SIGTERM, on_sigterm, loop);
+ g_main_loop_run (loop);
+ g_main_loop_unref (loop);
+
+ return 0;
+}
diff --git a/src/frames/meson.build b/src/frames/meson.build
new file mode 100644
index 000000000..1ddccff23
--- /dev/null
+++ b/src/frames/meson.build
@@ -0,0 +1,24 @@
+x11_frames_sources = [
+ 'main.c',
+ 'meta-frame.c',
+ 'meta-frame-content.c',
+ 'meta-frame-header.c',
+ 'meta-window-tracker.c',
+]
+
+x11_frames = executable('mutter-x11-frames',
+ sources: x11_frames_sources,
+ dependencies: [
+ gtk4_dep,
+ x11_dep,
+ xext_dep,
+ xfixes_dep,
+ xi_dep,
+ ],
+ c_args: [
+ '-DG_LOG_DOMAIN="mutter-x11-frames"',
+ ],
+ include_directories: top_includepath,
+ install: true,
+ install_dir: get_option('libexecdir'),
+)
diff --git a/src/frames/meta-frame-content.c b/src/frames/meta-frame-content.c
new file mode 100644
index 000000000..bd9965179
--- /dev/null
+++ b/src/frames/meta-frame-content.c
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2022 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 .
+ *
+ * Author: Carlos Garnacho
+ */
+
+#include "config.h"
+
+#include "meta-frame-content.h"
+
+struct _MetaFrameContent
+{
+ GtkWidget parent_instance;
+ Window window;
+ GtkBorder border;
+};
+
+enum {
+ PROP_0,
+ PROP_XWINDOW,
+ PROP_BORDER,
+ N_PROPS
+};
+
+static GParamSpec *props[N_PROPS] = { 0, };
+
+G_DEFINE_TYPE (MetaFrameContent, meta_frame_content, GTK_TYPE_WIDGET)
+
+static void
+meta_frame_content_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ MetaFrameContent *frame_content = META_FRAME_CONTENT (object);
+
+ switch (prop_id)
+ {
+ case PROP_XWINDOW:
+ frame_content->window = (Window) g_value_get_ulong (value);
+ break;
+ case PROP_BORDER:
+ frame_content->border = *(GtkBorder*) g_value_get_boxed (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+meta_frame_content_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ MetaFrameContent *frame_content = META_FRAME_CONTENT (object);
+
+ switch (prop_id)
+ {
+ case PROP_XWINDOW:
+ g_value_set_ulong (value, (gulong) frame_content->window);
+ break;
+ case PROP_BORDER:
+ g_value_set_boxed (value, &frame_content->border);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+meta_frame_content_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ *minimum_baseline = *natural_baseline = -1;
+ *minimum = *natural = 1;
+}
+
+static void
+meta_frame_content_update_border (MetaFrameContent *content,
+ GtkBorder border)
+{
+ if (content->border.left == border.left &&
+ content->border.right == border.right &&
+ content->border.top == border.top &&
+ content->border.bottom == border.bottom)
+ return;
+
+ content->border = border;
+ g_object_notify (G_OBJECT (content), "border");
+}
+
+static void
+meta_frame_content_size_allocate (GtkWidget *widget,
+ int width,
+ int height,
+ int baseline)
+{
+ MetaFrameContent *content = META_FRAME_CONTENT (widget);
+ GtkWindow *window = GTK_WINDOW (gtk_widget_get_root (widget));
+ double x = 0, y = 0, scale;
+
+ gtk_widget_translate_coordinates (widget,
+ GTK_WIDGET (window),
+ x, y,
+ &x, &y);
+
+ scale = gdk_surface_get_scale_factor (gtk_native_get_surface (GTK_NATIVE (window)));
+
+ meta_frame_content_update_border (content,
+ /* FIXME: right/bottom are broken, if they
+ * are ever other than 0.
+ */
+ (GtkBorder) {
+ x * scale, 0,
+ y * scale, 0,
+ });
+}
+
+static void
+meta_frame_content_class_init (MetaFrameContentClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->set_property = meta_frame_content_set_property;
+ object_class->get_property = meta_frame_content_get_property;
+
+ widget_class->measure = meta_frame_content_measure;
+ widget_class->size_allocate = meta_frame_content_size_allocate;
+
+ props[PROP_XWINDOW] = g_param_spec_ulong ("xwindow",
+ "X window",
+ "X window",
+ 0, G_MAXULONG, 0,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_NAME |
+ G_PARAM_STATIC_NICK |
+ G_PARAM_STATIC_BLURB);
+ props[PROP_BORDER] = g_param_spec_boxed ("border",
+ "Border",
+ "Border",
+ GTK_TYPE_BORDER,
+ G_PARAM_READABLE |
+ G_PARAM_EXPLICIT_NOTIFY |
+ G_PARAM_STATIC_NAME |
+ G_PARAM_STATIC_NICK |
+ G_PARAM_STATIC_BLURB);
+
+ g_object_class_install_properties (object_class,
+ G_N_ELEMENTS (props),
+ props);
+}
+
+static void
+meta_frame_content_init (MetaFrameContent *content)
+{
+}
+
+GtkWidget *
+meta_frame_content_new (Window window)
+{
+ return g_object_new (META_TYPE_FRAME_CONTENT,
+ "xwindow", window,
+ NULL);
+}
+
+Window
+meta_frame_content_get_window (MetaFrameContent *content)
+{
+ return content->window;
+}
+
+GtkBorder
+meta_frame_content_get_border (MetaFrameContent *content)
+{
+ return content->border;
+}
diff --git a/src/frames/meta-frame-content.h b/src/frames/meta-frame-content.h
new file mode 100644
index 000000000..6125ef931
--- /dev/null
+++ b/src/frames/meta-frame-content.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 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 .
+ *
+ * Author: Carlos Garnacho
+ */
+
+#ifndef META_FRAME_CONTENT_H
+#define META_FRAME_CONTENT_H
+
+#include
+
+#include
+
+#define META_TYPE_FRAME_CONTENT (meta_frame_content_get_type ())
+G_DECLARE_FINAL_TYPE (MetaFrameContent, meta_frame_content,
+ META, FRAME_CONTENT, GtkWidget)
+
+GtkWidget * meta_frame_content_new (Window window);
+
+Window meta_frame_content_get_window (MetaFrameContent *content);
+
+GtkBorder meta_frame_content_get_border (MetaFrameContent *content);
+
+#endif /* META_FRAME_CONTENT_H */
diff --git a/src/frames/meta-frame-header.c b/src/frames/meta-frame-header.c
new file mode 100644
index 000000000..5f2cda1c5
--- /dev/null
+++ b/src/frames/meta-frame-header.c
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2022 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 .
+ *
+ * Author: Carlos Garnacho
+ */
+
+#include "config.h"
+
+#include "meta-frame-header.h"
+
+struct _MetaFrameHeader
+{
+ GtkWidget parent_instance;
+};
+
+G_DEFINE_TYPE (MetaFrameHeader, meta_frame_header, GTK_TYPE_WIDGET)
+
+static void
+meta_frame_header_dispose (GObject *object)
+{
+ GtkWidget *widget = GTK_WIDGET (object);
+ GtkWidget *child;
+
+ child = gtk_widget_get_first_child (widget);
+ if (child)
+ gtk_widget_unparent (child);
+
+ G_OBJECT_CLASS (meta_frame_header_parent_class)->dispose (object);
+}
+
+static void
+meta_frame_header_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ *minimum_baseline = *natural_baseline = -1;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ {
+ *minimum = *natural = 1;
+ }
+ else
+ {
+ GtkWidget *child;
+
+ child = gtk_widget_get_first_child (widget);
+ gtk_widget_measure (child,
+ orientation, for_size,
+ minimum, natural,
+ minimum_baseline,
+ natural_baseline);
+ }
+}
+
+static void
+meta_frame_header_size_allocate (GtkWidget *widget,
+ int width,
+ int height,
+ int baseline)
+{
+ GtkWidget *child;
+ int minimum;
+ gboolean shrunk;
+ GtkAllocation child_allocation;
+
+ child = gtk_widget_get_first_child (widget);
+
+ gtk_widget_measure (child,
+ GTK_ORIENTATION_HORIZONTAL,
+ height,
+ &minimum, NULL, NULL, NULL);
+
+ shrunk = width < minimum;
+
+ child_allocation.x = shrunk ? width - minimum : 0;
+ child_allocation.y = 0;
+ child_allocation.width = shrunk ? minimum : width;
+ child_allocation.height = height;
+
+ gtk_widget_size_allocate (child, &child_allocation, baseline);
+}
+
+static void
+meta_frame_header_class_init (MetaFrameHeaderClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = meta_frame_header_dispose;
+
+ widget_class->measure = meta_frame_header_measure;
+ widget_class->size_allocate = meta_frame_header_size_allocate;
+}
+
+static void
+meta_frame_header_init (MetaFrameHeader *content)
+{
+ GtkWidget *header_bar;
+
+ header_bar = gtk_header_bar_new ();
+ gtk_widget_insert_before (header_bar, GTK_WIDGET (content), NULL);
+}
+
+GtkWidget *
+meta_frame_header_new (void)
+{
+ return g_object_new (META_TYPE_FRAME_HEADER, NULL);
+}
diff --git a/src/frames/meta-frame-header.h b/src/frames/meta-frame-header.h
new file mode 100644
index 000000000..758fac8e7
--- /dev/null
+++ b/src/frames/meta-frame-header.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 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 .
+ *
+ * Author: Carlos Garnacho
+ */
+
+#ifndef META_FRAME_HEADER_H
+#define META_FRAME_HEADER_H
+
+#include
+
+#define META_TYPE_FRAME_HEADER (meta_frame_header_get_type ())
+G_DECLARE_FINAL_TYPE (MetaFrameHeader, meta_frame_header,
+ META, FRAME_HEADER, GtkWidget)
+
+GtkWidget * meta_frame_header_new (void);
+
+#endif /* META_FRAME_HEADER_H */
diff --git a/src/frames/meta-frame.c b/src/frames/meta-frame.c
new file mode 100644
index 000000000..26d0e1f1a
--- /dev/null
+++ b/src/frames/meta-frame.c
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2022 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 .
+ *
+ * Author: Carlos Garnacho
+ */
+
+#include "config.h"
+
+#include "meta-frame.h"
+
+#include "meta-frame-content.h"
+#include "meta-frame-header.h"
+
+#include
+#include
+
+struct _MetaFrame
+{
+ GtkWindow parent_instance;
+ GtkWidget *content;
+};
+
+typedef struct
+{
+ unsigned long flags;
+ unsigned long functions;
+ unsigned long decorations;
+ long input_mode;
+ unsigned long status;
+} MotifWmHints;
+
+#define MWM_FUNC_ALL (1L << 0)
+#define MWM_FUNC_RESIZE (1L << 1)
+#define MWM_FUNC_MINIMIZE (1L << 3)
+#define MWM_FUNC_MAXIMIZE (1L << 4)
+#define MWM_FUNC_CLOSE (1L << 5)
+
+G_DEFINE_TYPE (MetaFrame, meta_frame, GTK_TYPE_WINDOW)
+
+static void
+meta_frame_class_init (MetaFrameClass *klass)
+{
+}
+
+static gboolean
+on_frame_close_request (GtkWindow *window,
+ gpointer user_data)
+{
+ GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (window));
+ GtkWidget *content;
+ XClientMessageEvent ev;
+ Window client_xwindow;
+
+ content = gtk_window_get_child (window);
+ if (!content)
+ return FALSE;
+
+ client_xwindow =
+ meta_frame_content_get_window (META_FRAME_CONTENT (content));
+
+ ev.type = ClientMessage;
+ ev.window = client_xwindow;
+ ev.message_type =
+ gdk_x11_get_xatom_by_name_for_display (display, "WM_PROTOCOLS");
+ ev.format = 32;
+ ev.data.l[0] =
+ gdk_x11_get_xatom_by_name_for_display (display, "WM_DELETE_WINDOW");
+ ev.data.l[1] = 0; /* FIXME: missing timestamp */
+
+ gdk_x11_display_error_trap_push (display);
+ XSendEvent (gdk_x11_display_get_xdisplay (display),
+ client_xwindow, False, 0, (XEvent*) &ev);
+ gdk_x11_display_error_trap_pop_ignored (display);
+
+ return TRUE;
+}
+
+static void
+meta_frame_init (MetaFrame *frame)
+{
+ g_signal_connect (frame, "close-request",
+ G_CALLBACK (on_frame_close_request), NULL);
+}
+
+static void
+meta_frame_update_extents (MetaFrame *frame,
+ GtkBorder border)
+{
+ GtkWindow *window = GTK_WINDOW (frame);
+ GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (frame));
+ GdkSurface *surface;
+ Window xframe;
+ unsigned long data[4];
+
+ surface = gtk_native_get_surface (GTK_NATIVE (window));
+ if (!surface)
+ return;
+
+ data[0] = border.left;
+ data[1] = border.right;
+ data[2] = border.top;
+ data[3] = border.bottom;
+
+ xframe = gdk_x11_surface_get_xid (surface);
+ XChangeProperty (gdk_x11_display_get_xdisplay (display),
+ xframe,
+ gdk_x11_get_xatom_by_name_for_display (display, "_MUTTER_FRAME_EXTENTS"),
+ XA_CARDINAL,
+ 32,
+ PropModeReplace,
+ (guchar *) &data, 4);
+}
+
+static void
+on_border_changed (GObject *object,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ MetaFrame *frame = user_data;
+ GtkWidget *content;
+ GtkBorder border;
+
+ content = gtk_window_get_child (GTK_WINDOW (frame));
+ border = meta_frame_content_get_border (META_FRAME_CONTENT (content));
+ meta_frame_update_extents (frame, border);
+}
+
+static void
+frame_sync_title (GtkWindow *frame,
+ Window client_window)
+{
+ GdkDisplay *display;
+ char *title = NULL;
+ int format;
+ Atom type;
+ unsigned long nitems, bytes_after;
+
+ display = gtk_widget_get_display (GTK_WIDGET (frame));
+
+ XGetWindowProperty (gdk_x11_display_get_xdisplay (display),
+ client_window,
+ gdk_x11_get_xatom_by_name_for_display (display,
+ "_NET_WM_NAME"),
+ 0, G_MAXLONG, False,
+ gdk_x11_get_xatom_by_name_for_display (display,
+ "UTF8_STRING"),
+ &type, &format,
+ &nitems, &bytes_after,
+ (unsigned char **) &title);
+
+ gtk_window_set_title (frame, title);
+ g_free (title);
+}
+
+static void
+frame_sync_motif_wm_hints (GtkWindow *frame,
+ Window client_window)
+{
+ GdkDisplay *display;
+ MotifWmHints *mwm_hints = NULL;
+ int format;
+ Atom type;
+ unsigned long nitems, bytes_after;
+ gboolean deletable = TRUE;
+
+ display = gtk_widget_get_display (GTK_WIDGET (frame));
+
+ XGetWindowProperty (gdk_x11_display_get_xdisplay (display),
+ client_window,
+ gdk_x11_get_xatom_by_name_for_display (display,
+ "_MOTIF_WM_HINTS"),
+ 0, sizeof (MotifWmHints) / sizeof (long),
+ False, AnyPropertyType,
+ &type, &format,
+ &nitems, &bytes_after,
+ (unsigned char **) &mwm_hints);
+
+ if (mwm_hints)
+ {
+ if ((mwm_hints->functions & MWM_FUNC_ALL) == 0)
+ deletable = (mwm_hints->functions & MWM_FUNC_CLOSE) != 0;
+ else
+ deletable = (mwm_hints->functions & MWM_FUNC_CLOSE) == 0;
+
+ g_free (mwm_hints);
+ }
+
+ gtk_window_set_deletable (frame, deletable);
+}
+
+static void
+frame_sync_wm_normal_hints (GtkWindow *frame,
+ Window client_window)
+{
+ GdkDisplay *display;
+ XSizeHints size_hints;
+ long nitems;
+ gboolean resizable = TRUE;
+
+ display = gtk_widget_get_display (GTK_WIDGET (frame));
+
+ XGetWMNormalHints (gdk_x11_display_get_xdisplay (display),
+ client_window,
+ &size_hints,
+ &nitems);
+
+ if (nitems > 0)
+ {
+ resizable = ((size_hints.flags & PMinSize) == 0 ||
+ (size_hints.flags & PMaxSize) == 0 ||
+ size_hints.min_width != size_hints.max_width ||
+ size_hints.min_height != size_hints.max_height);
+ }
+
+ gtk_window_set_resizable (frame, resizable);
+}
+
+GtkWidget *
+meta_frame_new (Window window)
+{
+ GtkWidget *frame, *header, *content;
+ GdkSurface *surface;
+ int frame_height;
+ double scale;
+
+ frame = g_object_new (META_TYPE_FRAME, NULL);
+
+ header = meta_frame_header_new ();
+
+ gtk_window_set_titlebar (GTK_WINDOW (frame), header);
+
+ content = meta_frame_content_new (window);
+ gtk_window_set_child (GTK_WINDOW (frame), content);
+
+ g_signal_connect (content, "notify::border",
+ G_CALLBACK (on_border_changed), frame);
+
+ gtk_widget_realize (GTK_WIDGET (frame));
+ surface = gtk_native_get_surface (GTK_NATIVE (frame));
+ gdk_x11_surface_set_frame_sync_enabled (surface, FALSE);
+
+ gtk_widget_measure (header,
+ GTK_ORIENTATION_VERTICAL, 1,
+ &frame_height,
+ NULL, NULL, NULL);
+
+ scale = gdk_surface_get_scale_factor (gtk_native_get_surface (GTK_NATIVE (frame)));
+
+ meta_frame_update_extents (META_FRAME (frame),
+ (GtkBorder) {
+ 0, 0,
+ frame_height * scale, 0,
+ });
+
+ frame_sync_title (GTK_WINDOW (frame), window);
+ frame_sync_motif_wm_hints (GTK_WINDOW (frame), window);
+ frame_sync_wm_normal_hints (GTK_WINDOW (frame), window);
+
+ return frame;
+}
+
+void
+meta_frame_handle_xevent (MetaFrame *frame,
+ Window window,
+ XEvent *xevent)
+{
+ GdkDisplay *display;
+ GtkWidget *content;
+ gboolean is_frame, is_content;
+ GdkSurface *surface;
+
+ surface = gtk_native_get_surface (GTK_NATIVE (frame));
+ if (!surface)
+ return;
+
+ content = gtk_window_get_child (GTK_WINDOW (frame));
+ if (!content)
+ return;
+
+ is_frame = window == gdk_x11_surface_get_xid (surface);
+ is_content =
+ window == meta_frame_content_get_window (META_FRAME_CONTENT (content));
+
+ if (!is_frame && !is_content)
+ return;
+
+ display = gtk_widget_get_display (GTK_WIDGET (frame));
+
+ if (is_content && xevent->type == PropertyNotify)
+ {
+ if (xevent->xproperty.atom ==
+ gdk_x11_get_xatom_by_name_for_display (display, "_NET_WM_NAME"))
+ frame_sync_title (GTK_WINDOW (frame), xevent->xproperty.window);
+ else if (xevent->xproperty.atom ==
+ gdk_x11_get_xatom_by_name_for_display (display, "_MOTIF_WM_HINTS"))
+ frame_sync_motif_wm_hints (GTK_WINDOW (frame), xevent->xproperty.window);
+ else if (xevent->xproperty.atom ==
+ gdk_x11_get_xatom_by_name_for_display (display, "WM_NORMAL_HINTS"))
+ frame_sync_wm_normal_hints (GTK_WINDOW (frame), xevent->xproperty.window);
+ }
+}
diff --git a/src/frames/meta-frame.h b/src/frames/meta-frame.h
new file mode 100644
index 000000000..d39dc8218
--- /dev/null
+++ b/src/frames/meta-frame.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 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 .
+ *
+ * Author: Carlos Garnacho
+ */
+
+#ifndef META_FRAME_H
+#define META_FRAME_H
+
+#include
+#include
+
+#define META_TYPE_FRAME (meta_frame_get_type ())
+G_DECLARE_FINAL_TYPE (MetaFrame, meta_frame, META, FRAME, GtkWindow)
+
+GtkWidget * meta_frame_new (Window window);
+
+void meta_frame_handle_xevent (MetaFrame *frame,
+ Window window,
+ XEvent *xevent);
+
+#endif /* META_FRAME_CONTENT_H */
diff --git a/src/frames/meta-window-tracker.c b/src/frames/meta-window-tracker.c
new file mode 100644
index 000000000..7487904c4
--- /dev/null
+++ b/src/frames/meta-window-tracker.c
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2022 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 .
+ *
+ * Author: Carlos Garnacho
+ */
+
+#include "config.h"
+
+#include "meta-window-tracker.h"
+
+#include "meta-frame.h"
+
+#include
+#include
+#include
+
+struct _MetaWindowTracker
+{
+ GObject parent_instance;
+ GdkDisplay *display;
+ GHashTable *frames;
+ GHashTable *client_windows;
+ int xinput_opcode;
+};
+
+enum {
+ PROP_0,
+ PROP_DISPLAY,
+ N_PROPS
+};
+
+static GParamSpec *props[N_PROPS] = { 0, };
+
+G_DEFINE_TYPE (MetaWindowTracker, meta_window_tracker, G_TYPE_OBJECT)
+
+static void
+meta_window_tracker_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ MetaWindowTracker *window_tracker = META_WINDOW_TRACKER (object);
+
+ switch (prop_id)
+ {
+ case PROP_DISPLAY:
+ window_tracker->display = g_value_get_object (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+meta_window_tracker_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ MetaWindowTracker *window_tracker = META_WINDOW_TRACKER (object);
+
+ switch (prop_id)
+ {
+ case PROP_DISPLAY:
+ g_value_set_object (value, window_tracker->display);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+set_up_frame (MetaWindowTracker *window_tracker,
+ Window xwindow)
+{
+ GdkDisplay *display = window_tracker->display;
+ Display *xdisplay = gdk_x11_display_get_xdisplay (display);
+ GdkSurface *surface;
+ Window xframe;
+ unsigned long data[1];
+ GtkWidget *frame;
+
+ /* Double check it's not a request for a frame of our own. */
+ if (g_hash_table_contains (window_tracker->frames,
+ GUINT_TO_POINTER (xwindow)))
+ return;
+
+ /* Create a frame window */
+ frame = meta_frame_new (xwindow);
+ surface = gtk_native_get_surface (GTK_NATIVE (frame));
+ xframe = gdk_x11_surface_get_xid (surface);
+
+ XAddToSaveSet (xdisplay, xwindow);
+
+ data[0] = xwindow;
+ XChangeProperty (xdisplay,
+ xframe,
+ gdk_x11_get_xatom_by_name_for_display (display, "_MUTTER_FRAME_FOR"),
+ XA_WINDOW,
+ 32,
+ PropModeReplace,
+ (guchar *) data, 1);
+
+ g_hash_table_insert (window_tracker->frames,
+ GUINT_TO_POINTER (xframe), frame);
+ g_hash_table_insert (window_tracker->client_windows,
+ GUINT_TO_POINTER (xwindow), frame);
+ gtk_widget_show (GTK_WIDGET (frame));
+}
+
+static void
+listen_set_up_frame (MetaWindowTracker *window_tracker,
+ Window xwindow)
+{
+ GdkDisplay *display = window_tracker->display;
+ Display *xdisplay = gdk_x11_display_get_xdisplay (display);
+ int format;
+ Atom type;
+ unsigned long nitems, bytes_after;
+ unsigned char *data;
+
+ gdk_x11_display_error_trap_push (display);
+
+ XSelectInput (xdisplay, xwindow,
+ PropertyChangeMask | StructureNotifyMask);
+
+ XGetWindowProperty (xdisplay,
+ xwindow,
+ gdk_x11_get_xatom_by_name_for_display (display,
+ "_MUTTER_NEEDS_FRAME"),
+ 0, 1,
+ False, XA_CARDINAL,
+ &type, &format,
+ &nitems, &bytes_after,
+ (unsigned char **) &data);
+
+ if (gdk_x11_display_error_trap_pop (display))
+ return;
+
+ if (nitems > 0 && data[0])
+ set_up_frame (window_tracker, xwindow);
+
+ XFree (data);
+}
+
+static void
+remove_frame (MetaWindowTracker *window_tracker,
+ Window xwindow)
+{
+ GdkDisplay *display = window_tracker->display;
+ Display *xdisplay = gdk_x11_display_get_xdisplay (display);
+ GtkWidget *frame;
+ GdkSurface *surface;
+ Window xframe;
+
+ frame = g_hash_table_lookup (window_tracker->client_windows,
+ GUINT_TO_POINTER (xwindow));
+ if (!frame)
+ return;
+
+ surface = gtk_native_get_surface (GTK_NATIVE (frame));
+ xframe = gdk_x11_surface_get_xid (surface);
+
+ gdk_x11_display_error_trap_push (display);
+ XRemoveFromSaveSet (xdisplay, xwindow);
+ gdk_x11_display_error_trap_pop_ignored (display);
+
+ g_hash_table_remove (window_tracker->client_windows,
+ GUINT_TO_POINTER (xwindow));
+ g_hash_table_remove (window_tracker->frames,
+ GUINT_TO_POINTER (xframe));
+}
+
+static gboolean
+on_xevent (GdkDisplay *display,
+ XEvent *xevent,
+ gpointer user_data)
+{
+ Window xroot = gdk_x11_display_get_xrootwindow (display);
+ Window xwindow = xevent->xany.window;
+ MetaWindowTracker *window_tracker = user_data;
+ GtkWidget *frame;
+
+ if (xevent->type == CreateNotify &&
+ xevent->xcreatewindow.parent == xroot &&
+ !xevent->xcreatewindow.override_redirect &&
+ !g_hash_table_contains (window_tracker->frames,
+ GUINT_TO_POINTER (xevent->xcreatewindow.window)))
+ {
+ xwindow = xevent->xcreatewindow.window;
+ listen_set_up_frame (window_tracker, xwindow);
+ }
+ else if (xevent->type == DestroyNotify)
+ {
+ xwindow = xevent->xdestroywindow.window;
+ remove_frame (window_tracker, xwindow);
+ }
+ else if (xevent->type == PropertyNotify &&
+ xevent->xproperty.atom ==
+ gdk_x11_get_xatom_by_name_for_display (display, "_MUTTER_NEEDS_FRAME"))
+ {
+ if (xevent->xproperty.state == PropertyNewValue)
+ set_up_frame (window_tracker, xwindow);
+ else if (xevent->xproperty.state == PropertyDelete)
+ remove_frame (window_tracker, xwindow);
+ }
+ else if (xevent->type == PropertyNotify)
+ {
+ frame = g_hash_table_lookup (window_tracker->frames,
+ GUINT_TO_POINTER (xwindow));
+
+ if (!frame)
+ {
+ frame = g_hash_table_lookup (window_tracker->client_windows,
+ GUINT_TO_POINTER (xwindow));
+ }
+
+ if (frame)
+ meta_frame_handle_xevent (META_FRAME (frame), xwindow, xevent);
+ }
+ else if (xevent->type == GenericEvent &&
+ xevent->xcookie.extension == window_tracker->xinput_opcode)
+ {
+ Display *xdisplay = gdk_x11_display_get_xdisplay (display);
+ XIEvent *xi_event;
+
+ xi_event = (XIEvent *) xevent->xcookie.data;
+
+ if (xi_event->evtype == XI_Leave)
+ {
+ XILeaveEvent *crossing = (XILeaveEvent *) xi_event;
+
+ xwindow = crossing->event;
+ frame = g_hash_table_lookup (window_tracker->frames,
+ GUINT_TO_POINTER (xwindow));
+
+ /* When crossing from the frame to the client
+ * window, we may need to restore the cursor to
+ * its default.
+ */
+ if (frame && crossing->detail == XINotifyInferior)
+ XIUndefineCursor (xdisplay, crossing->deviceid, xwindow);
+ }
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+query_xi_extension (MetaWindowTracker *window_tracker,
+ Display *xdisplay)
+{
+ int major = 2, minor = 3;
+ int unused;
+
+ if (XQueryExtension (xdisplay,
+ "XInputExtension",
+ &window_tracker->xinput_opcode,
+ &unused,
+ &unused))
+ {
+ if (XIQueryVersion (xdisplay, &major, &minor) == Success)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+meta_window_tracker_constructed (GObject *object)
+{
+ MetaWindowTracker *window_tracker = META_WINDOW_TRACKER (object);
+ GdkDisplay *display = window_tracker->display;
+ Display *xdisplay = gdk_x11_display_get_xdisplay (display);
+ Window xroot = gdk_x11_display_get_xrootwindow (display);
+ Window *windows, ignored1, ignored2;
+ unsigned int i, n_windows;
+
+ G_OBJECT_CLASS (meta_window_tracker_parent_class)->constructed (object);
+
+ query_xi_extension (window_tracker, xdisplay);
+
+ XSelectInput (xdisplay, xroot,
+ KeyPressMask |
+ PropertyChangeMask);
+
+ g_signal_connect (display, "xevent",
+ G_CALLBACK (on_xevent), object);
+
+ gdk_x11_display_error_trap_push (display);
+
+ XQueryTree (xdisplay,
+ xroot,
+ &ignored1, &ignored2,
+ &windows, &n_windows);
+
+ if (gdk_x11_display_error_trap_pop (display))
+ {
+ g_warning ("Could not query existing windows");
+ return;
+ }
+
+ for (i = 0; i < n_windows; i++)
+ {
+ XWindowAttributes attrs;
+
+ gdk_x11_display_error_trap_push (display);
+
+ XGetWindowAttributes (xdisplay,
+ windows[i],
+ &attrs);
+
+ if (gdk_x11_display_error_trap_pop (display))
+ continue;
+
+ if (attrs.override_redirect)
+ continue;
+
+ listen_set_up_frame (window_tracker, windows[i]);
+ }
+
+ XFree (windows);
+}
+
+static void
+meta_window_tracker_finalize (GObject *object)
+{
+ MetaWindowTracker *window_tracker = META_WINDOW_TRACKER (object);
+
+ g_clear_pointer (&window_tracker->frames,
+ g_hash_table_unref);
+ g_clear_pointer (&window_tracker->client_windows,
+ g_hash_table_unref);
+
+ G_OBJECT_CLASS (meta_window_tracker_parent_class)->finalize (object);
+}
+
+static void
+meta_window_tracker_class_init (MetaWindowTrackerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = meta_window_tracker_set_property;
+ object_class->get_property = meta_window_tracker_get_property;
+ object_class->constructed = meta_window_tracker_constructed;
+ object_class->finalize = meta_window_tracker_finalize;
+
+ props[PROP_DISPLAY] = g_param_spec_object ("display",
+ "Display",
+ "Display",
+ GDK_TYPE_DISPLAY,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_NAME |
+ G_PARAM_STATIC_NICK |
+ G_PARAM_STATIC_BLURB);
+
+ g_object_class_install_properties (object_class,
+ G_N_ELEMENTS (props),
+ props);
+}
+
+static void
+meta_window_tracker_init (MetaWindowTracker *window_tracker)
+{
+ window_tracker->frames =
+ g_hash_table_new_full (NULL, NULL, NULL,
+ (GDestroyNotify) gtk_window_destroy);
+ window_tracker->client_windows = g_hash_table_new (NULL, NULL);
+}
+
+MetaWindowTracker *
+meta_window_tracker_new (GdkDisplay *display)
+{
+ return g_object_new (META_TYPE_WINDOW_TRACKER,
+ "display", display,
+ NULL);
+}
diff --git a/src/frames/meta-window-tracker.h b/src/frames/meta-window-tracker.h
new file mode 100644
index 000000000..4b52b33e9
--- /dev/null
+++ b/src/frames/meta-window-tracker.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 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 .
+ *
+ * Author: Carlos Garnacho
+ */
+
+#ifndef META_WINDOW_TRACKER_H
+#define META_WINDOW_TRACKER_H
+
+#include
+#include
+
+#define META_TYPE_WINDOW_TRACKER (meta_window_tracker_get_type ())
+G_DECLARE_FINAL_TYPE (MetaWindowTracker, meta_window_tracker,
+ META, WINDOW_TRACKER,
+ GObject)
+
+MetaWindowTracker * meta_window_tracker_new (GdkDisplay *display);
+
+#endif /* META_WINDOW_TRACKER_H */
diff --git a/src/meson.build b/src/meson.build
index 91cf29adb..6a995f3d4 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1189,6 +1189,7 @@ pkg.generate(libmutter,
)
subdir('compositor/plugins')
+subdir('frames')
if have_core_tests
subdir('tests')