diff --git a/protocol/pantheon-desktop-shell-v1.xml b/protocol/pantheon-desktop-shell-v1.xml
index fcd0f1758..eaf56cd6a 100644
--- a/protocol/pantheon-desktop-shell-v1.xml
+++ b/protocol/pantheon-desktop-shell-v1.xml
@@ -151,5 +151,14 @@
by the compositor.
+
+
+
+ Request to make the surface a monitor label for the given monitor. The surface will be placed
+ in the top left corner of the monitor and will be kept above other surfaces.
+
+
+
+
diff --git a/protocol/pantheon-desktop-shell.vapi b/protocol/pantheon-desktop-shell.vapi
index 778d1ca42..358582049 100644
--- a/protocol/pantheon-desktop-shell.vapi
+++ b/protocol/pantheon-desktop-shell.vapi
@@ -61,6 +61,7 @@ namespace Pantheon.Desktop {
public SetKeepAbove set_keep_above;
public MakeCentered make_centered;
public Focus focus;
+ public MakeMonitorLabel make_monitor_label;
}
[CCode (has_target = false, has_typedef = false)]
@@ -88,5 +89,7 @@ namespace Pantheon.Desktop {
[CCode (has_target = false, has_typedef = false)]
public delegate void MakeCentered (Wl.Client client, Wl.Resource resource);
[CCode (has_target = false, has_typedef = false)]
+ public delegate void MakeMonitorLabel (Wl.Client client, Wl.Resource resource, int monitor_index);
+ [CCode (has_target = false, has_typedef = false)]
public delegate void Destroy (Wl.Client client, Wl.Resource resource);
}
diff --git a/src/PantheonShell.vala b/src/PantheonShell.vala
index 936f6eb57..50af93671 100644
--- a/src/PantheonShell.vala
+++ b/src/PantheonShell.vala
@@ -53,6 +53,7 @@ namespace Gala {
set_keep_above,
make_centered,
focus_extended_behavior,
+ make_monitor_label,
};
PanelSurface.quark = GLib.Quark.from_string ("-gala-wayland-panel-surface-data");
@@ -376,6 +377,21 @@ namespace Gala {
ShellClientsManager.get_instance ().make_centered (window);
}
+ internal static void make_monitor_label (Wl.Client client, Wl.Resource resource, int monitor_index) {
+ unowned ExtendedBehaviorSurface? eb_surface = resource.get_user_data ();
+ if (eb_surface.wayland_surface == null) {
+ return;
+ }
+
+ Meta.Window? window;
+ eb_surface.wayland_surface.get ("window", out window, null);
+ if (window == null) {
+ return;
+ }
+
+ ShellClientsManager.get_instance ().make_monitor_label (window, monitor_index);
+ }
+
internal static void destroy_panel_surface (Wl.Client client, Wl.Resource resource) {
resource.destroy ();
}
diff --git a/src/ShellClients/MonitorLabelWindow.vala b/src/ShellClients/MonitorLabelWindow.vala
new file mode 100644
index 000000000..1ad53f5c6
--- /dev/null
+++ b/src/ShellClients/MonitorLabelWindow.vala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2025 elementary, Inc. (https://elementary.io)
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Authored by: Leonhard Kargl
+ */
+
+public class Gala.MonitorLabelWindow : PositionedWindow {
+ private const int MARGIN = 24;
+
+ public int monitor_index { get; construct; }
+
+ public MonitorLabelWindow (Meta.Window window, int monitor_index) {
+ Object (window: window, monitor_index: monitor_index);
+ }
+
+ protected override void get_window_position (Mtk.Rectangle window_rect, out int x, out int y) {
+ var monitor_rect = window.display.get_monitor_geometry (monitor_index);
+
+ x = monitor_rect.x + MARGIN;
+ y = monitor_rect.y + MARGIN;
+ }
+}
diff --git a/src/ShellClients/ShellClientsManager.vala b/src/ShellClients/ShellClientsManager.vala
index 988ba69dd..78dd92f73 100644
--- a/src/ShellClients/ShellClientsManager.vala
+++ b/src/ShellClients/ShellClientsManager.vala
@@ -29,6 +29,7 @@ public class Gala.ShellClientsManager : Object, GestureTarget {
private GLib.HashTable panel_windows = new GLib.HashTable (null, null);
private GLib.HashTable positioned_windows = new GLib.HashTable (null, null);
+ private GLib.HashTable monitor_label_windows = new GLib.HashTable (null, null);
private ShellClientsManager (WindowManager wm) {
Object (wm: wm);
@@ -240,6 +241,18 @@ public class Gala.ShellClientsManager : Object, GestureTarget {
window.unmanaging.connect_after ((_window) => positioned_windows.remove (_window));
}
+ public void make_monitor_label (Meta.Window window, int monitor_index) requires (!is_itself_positioned (window)) {
+ if (monitor_index < 0 || monitor_index > wm.get_display ().get_n_monitors ()) {
+ warning ("Invalid monitor index provided: %d", monitor_index);
+ return;
+ }
+
+ monitor_label_windows[window] = new MonitorLabelWindow (window, monitor_index);
+
+ // connect_after so we make sure that any queued move is unqueued
+ window.unmanaging.connect_after ((_window) => monitor_label_windows.remove (_window));
+ }
+
public void propagate (UpdateType update_type, GestureAction action, double progress) {
foreach (var window in positioned_windows.get_values ()) {
window.propagate (update_type, action, progress);
@@ -251,7 +264,8 @@ public class Gala.ShellClientsManager : Object, GestureTarget {
}
public bool is_itself_positioned (Meta.Window window) {
- return (window in positioned_windows) || (window in panel_windows) || NotificationStack.is_notification (window);
+ return (window in positioned_windows) || (window in panel_windows) ||
+ (window in monitor_label_windows) || NotificationStack.is_notification (window);
}
public bool is_positioned_window (Meta.Window window) {
@@ -350,6 +364,15 @@ public class Gala.ShellClientsManager : Object, GestureTarget {
set_restore_previous_x11_region (window);
break;
+ case "monitor-label":
+ int parsed;
+ if (int.try_parse (val, out parsed)) {
+ make_monitor_label (window, parsed);
+ } else {
+ warning ("Failed to parse %s as monitor label", val);
+ }
+ break;
+
default:
break;
}
diff --git a/src/meson.build b/src/meson.build
index d0503f355..66208077e 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -45,6 +45,7 @@ gala_bin_sources = files(
'ShellClients/ExtendedBehaviorWindow.vala',
'ShellClients/HideTracker.vala',
'ShellClients/ManagedClient.vala',
+ 'ShellClients/MonitorLabelWindow.vala',
'ShellClients/NotificationsClient.vala',
'ShellClients/PanelWindow.vala',
'ShellClients/PositionedWindow.vala',