| File: src\Aspire.Tray\Linux\LinuxDesktopEnvironment.cs | Web Access |
| Project: src\tests\Aspire.Tray.Tests\Aspire.Tray.Tests.csproj (Aspire.Tray.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.InteropServices; namespace Aspire.Tray; internal enum LinuxPanelBackend { X11, Wayland } /// <summary>Selects native menus for GNOME and compositor-compatible panel hosting elsewhere.</summary> internal static partial class LinuxDesktopEnvironment { internal static bool IsGnome(string? currentDesktop, string? sessionDesktop) { // XDG_CURRENT_DESKTOP is a colon-separated list, e.g. "ubuntu:GNOME". // It describes the active desktop; the session name is only a fallback. // https://specifications.freedesktop.org/desktop-entry-spec/latest/recognized-keys.html var desktop = string.IsNullOrWhiteSpace(currentDesktop) ? sessionDesktop : currentDesktop; return desktop?.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Any(value => value.Equals("GNOME", StringComparison.OrdinalIgnoreCase) || value.StartsWith("GNOME-", StringComparison.OrdinalIgnoreCase)) == true; } internal static LinuxPanelBackend RequirePanelBackend() { var backend = GetPanelBackend(); if (backend == LinuxPanelBackend.X11 && !IsX11Composited()) { throw new InvalidOperationException("The Aspire panel requires an X11 compositing manager for transparent popup windows."); } return backend; } /// <summary> /// Selects the panel backend without requiring an X11 compositor. A compositor can start /// after the tray during login, so compositing is only required when the panel opens. /// </summary> internal static LinuxPanelBackend GetPanelBackend() { var display = LinuxPanelNative.gdk_display_get_default(); var type = Marshal.PtrToStringUTF8(g_type_name_from_instance(display)); switch (type) { case "GdkX11Display": return LinuxPanelBackend.X11; case "GdkWaylandDisplay": if (!LinuxLayerShell.IsLoaded) { throw new InvalidOperationException("The Aspire panel on Wayland requires libgtk-layer-shell.so.0. Install your distribution's gtk-layer-shell package."); } if (LinuxLayerShell.gtk_layer_is_supported() == 0) { throw new InvalidOperationException("This Wayland compositor does not support the layer-shell protocol required by the Aspire panel."); } return LinuxPanelBackend.Wayland; default: throw new InvalidOperationException("The Aspire panel requires a composited X11 session or a layer-shell-compatible Wayland compositor."); } } internal static bool IsX11Composited() => gdk_screen_is_composited( gdk_display_get_default_screen(LinuxPanelNative.gdk_display_get_default())) != 0; internal static bool IsWayland => Marshal.PtrToStringUTF8( g_type_name_from_instance(LinuxPanelNative.gdk_display_get_default())) == "GdkWaylandDisplay"; [LibraryImport("libgobject-2.0.so.0")] private static partial nint g_type_name_from_instance(nint instance); [LibraryImport("libgdk-3.so.0")] private static partial nint gdk_display_get_default_screen(nint display); [LibraryImport("libgdk-3.so.0")] private static partial int gdk_screen_is_composited(nint screen); } /// <summary>Hosts Wayland popups using the compositor's layer-shell protocol.</summary> internal static partial class LinuxLayerShell { private static nint s_library; internal static bool IsLoaded => s_library != 0; internal static void Preload() { // Load before GTK so layer-shell's Wayland integration participates in // symbol resolution. Keep the library loaded for cached P/Invoke targets. // https://github.com/wmww/gtk-layer-shell if (!IsLoaded) { NativeLibrary.TryLoad("libgtk-layer-shell.so.0", out s_library); } } internal static void Configure(nint window, nint monitor, bool keyboard) { gtk_layer_init_for_window(window); gtk_layer_set_namespace(window, "dev.aspire.tray"); gtk_layer_set_layer(window, 3); // GTK_LAYER_SHELL_LAYER_OVERLAY gtk_layer_set_monitor(window, monitor); for (var edge = 0; edge < 4; edge++) { gtk_layer_set_anchor(window, edge, 1); } // Drawing and input use full-monitor coordinates, including exclusive bars. // Zone -1 keeps the overlay anchored to the output rather than its workarea. // https://github.com/wmww/gtk-layer-shell/blob/master/protocol/wlr-layer-shell-unstable-v1.xml gtk_layer_set_exclusive_zone(window, -1); gtk_layer_set_keyboard_mode(window, keyboard ? 1 : 0); // EXCLUSIVE / NONE } [LibraryImport("libgtk-layer-shell.so.0")] internal static partial int gtk_layer_is_supported(); [LibraryImport("libgtk-layer-shell.so.0")] private static partial void gtk_layer_init_for_window(nint window); [LibraryImport("libgtk-layer-shell.so.0", StringMarshalling = StringMarshalling.Utf8)] private static partial void gtk_layer_set_namespace(nint window, string name); [LibraryImport("libgtk-layer-shell.so.0")] private static partial void gtk_layer_set_layer(nint window, int layer); [LibraryImport("libgtk-layer-shell.so.0")] private static partial void gtk_layer_set_monitor(nint window, nint monitor); [LibraryImport("libgtk-layer-shell.so.0")] private static partial void gtk_layer_set_anchor(nint window, int edge, int anchor); [LibraryImport("libgtk-layer-shell.so.0")] private static partial void gtk_layer_set_exclusive_zone(nint window, int zone); [LibraryImport("libgtk-layer-shell.so.0")] private static partial void gtk_layer_set_keyboard_mode(nint window, int mode); }