From dc340189d6bd72b327b659486c6d8bf5e0927a49 Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Mon, 29 Jun 2026 17:27:38 +0200 Subject: [PATCH] Improve x11 support in Linux with GDK X11 fallback and scale support (#1556) (Closes #1551) * Fix GDK X11 support * Better scaling support for Linux --------- Co-authored-by: Alexandru Macocian --- Daybreak.Linux/Launcher.cs | 22 ++ .../Services/Screens/ScreenManager.cs | 237 ++++++++++-- Daybreak.Linux/Utils/DisplayScale.cs | 337 ++++++++++++++++++ Daybreak.Linux/Utils/NativeMethods.cs | 66 ++++ 4 files changed, 638 insertions(+), 24 deletions(-) create mode 100644 Daybreak.Linux/Utils/DisplayScale.cs diff --git a/Daybreak.Linux/Launcher.cs b/Daybreak.Linux/Launcher.cs index 1f064cf7..3639fa2a 100644 --- a/Daybreak.Linux/Launcher.cs +++ b/Daybreak.Linux/Launcher.cs @@ -1,4 +1,5 @@ using Daybreak.Linux.Configuration; +using Daybreak.Linux.Utils; namespace Daybreak.Linux; @@ -6,8 +7,29 @@ public static partial class Launcher { public static void Main(string[] args) { + ForceX11Backend(); + var bootstrap = Launch.Launcher.SetupBootstrap(); var platformConfiguration = new LinuxPlatformConfiguration(); Launch.Launcher.LaunchSequence(args, bootstrap, platformConfiguration); } + + /// + /// Forces GTK to use the X11 backend (via XWayland under Wayland sessions). + /// Photino/WebKitGTK/GTK3 and Daybreak's window interop rely on X11-only APIs + /// (gdk_x11_window_get_xid); without this, GTK selects the Wayland backend on + /// Wayland sessions and those calls abort with + /// "gdk_x11_window_get_xid: assertion 'GDK_IS_X11_WINDOW (window)' failed" + /// followed by a fatal Wayland protocol error (see issue #1551). + /// + /// Must run before any GTK initialization (which happens lazily when the first + /// Photino window is created) and must use the native libc setenv: managed + /// Environment.SetEnvironmentVariable does not propagate to native getenv, + /// which is how GTK reads GDK_BACKEND. overwrite=0 lets a user who explicitly + /// exports GDK_BACKEND (e.g. to test native Wayland) keep their choice. + /// + private static void ForceX11Backend() + { + NativeMethods.setenv("GDK_BACKEND", "x11", 0); + } } diff --git a/Daybreak.Linux/Services/Screens/ScreenManager.cs b/Daybreak.Linux/Services/Screens/ScreenManager.cs index 215840b4..6abe273b 100644 --- a/Daybreak.Linux/Services/Screens/ScreenManager.cs +++ b/Daybreak.Linux/Services/Screens/ScreenManager.cs @@ -1,4 +1,5 @@ using Daybreak.Configuration.Options; +using Daybreak.Linux.Utils; using Daybreak.Shared.Models; using Daybreak.Shared.Services.Options; using Daybreak.Shared.Services.Screens; @@ -9,26 +10,49 @@ using Photino.NET; using System.Core.Extensions; using System.Drawing; using System.Extensions; +using System.Extensions.Core; namespace Daybreak.Linux.Services.Screens; -// TODO: Implement proper Linux screen management using X11/Wayland APIs +/// +/// Linux screen management backed by GDK monitor enumeration. Reports real +/// monitor geometry and tracks each monitor's integer scale factor so the window +/// spawns sized to (and is restored on) the correct monitor. Scale is persisted +/// as DPI (scale * 96) in , mirroring the +/// Windows implementation, so a window saved on one monitor is rescaled when +/// restored on a monitor with a different scale. +/// +/// +/// GDK reports an integer scale factor; under X11/XWayland (Daybreak forces the +/// X11 backend) this reflects GDK_SCALE / Xft.dpi-derived scaling. True +/// fractional per-monitor scaling would require the Wayland backend. +/// internal sealed class ScreenManager( PhotinoWindow photinoWindow, IOptionsProvider optionsProvider, IOptionsMonitor liveUpdateableOptions, ILogger logger) : IScreenManager, IHostedService { + private const int DefaultDpi = 96; + private readonly PhotinoWindow photinoWindow = photinoWindow.ThrowIfNull(); private readonly IOptionsProvider optionsProvider = optionsProvider.ThrowIfNull(); private readonly IOptionsMonitor liveUpdateableOptions = liveUpdateableOptions.ThrowIfNull(); private readonly ILogger logger = logger.ThrowIfNull(); - // TODO: Implement proper screen enumeration for Linux - public IEnumerable Screens => GetDummyScreens(); + private readonly record struct MonitorInfo(Screen Screen, bool IsPrimary); + + // Enumerated once (lazily) and cached; provides real monitor geometry for + // window placement. Monitor scale comes from DisplayScale (not GDK), because + // under the forced X11 backend GDK always reports a scale of 1. + private IReadOnlyList? cachedMonitors; + private double? cachedScale; + + public IEnumerable Screens => this.GetMonitors().Select(static m => m.Screen); Task IHostedService.StartAsync(CancellationToken cancellationToken) { + this.ApplyDisplayScale(); return Task.CompletedTask; } @@ -38,6 +62,31 @@ internal sealed class ScreenManager( return Task.CompletedTask; } + /// + /// Zooms the Photino webview to match the desktop UI scale so the interface is + /// rendered at the correct physical size on HiDPI monitors. Photino's built-in + /// scale detection is defeated by the forced X11 backend, so this is applied + /// explicitly. Must run on the UI thread, hence the . + /// + private void ApplyDisplayScale() + { + var scale = this.GetEffectiveScale(); + var zoom = (int)Math.Round(scale * 100.0); + if (zoom == 100) + { + return; + } + + this.logger.LogDebug("Applying webview zoom {Zoom}% for display scale {Scale}", zoom, scale); + this.photinoWindow.Invoke(() => this.photinoWindow.Zoom = zoom); + } + + private double GetEffectiveScale() + { + this.cachedScale ??= DisplayScale.GetEffectiveScale(this.logger); + return this.cachedScale.Value; + } + public void MoveWindowToSavedPosition() { var savedPosition = this.GetSavedPosition(); @@ -55,11 +104,15 @@ internal sealed class ScreenManager( this.photinoWindow.Width, this.photinoWindow.Height); + var dpi = this.GetDpiForPosition(position.Left, position.Top); + var options = this.liveUpdateableOptions.CurrentValue; options.X = position.Left; options.Y = position.Top; options.Width = position.Width; options.Height = position.Height; + options.DpiX = dpi; + options.DpiY = dpi; this.optionsProvider.SaveOption(options); } @@ -70,6 +123,8 @@ internal sealed class ScreenManager( options.Y = 0; options.Width = 0; options.Height = 0; + options.DpiX = DefaultDpi; + options.DpiY = DefaultDpi; this.optionsProvider.SaveOption(options); } @@ -82,35 +137,169 @@ internal sealed class ScreenManager( public Rectangle GetSavedPosition() { + var options = this.liveUpdateableOptions.CurrentValue; var savedPosition = new Rectangle( - (int)this.liveUpdateableOptions.CurrentValue.X, - (int)this.liveUpdateableOptions.CurrentValue.Y, - (int)this.liveUpdateableOptions.CurrentValue.Width, - (int)this.liveUpdateableOptions.CurrentValue.Height); + (int)options.X, + (int)options.Y, + (int)options.Width, + (int)options.Height); if (savedPosition.Width is 0 || savedPosition.Height is 0) { - var firstScreen = this.Screens.FirstOrDefault(); - if (firstScreen.Size.IsEmpty) - { - // Return a reasonable default instead of throwing - return new Rectangle(0, 100, 1000, 900); - } - - return new Rectangle( - firstScreen.Size.X + (firstScreen.Size.Width / 4), - firstScreen.Size.Y + (firstScreen.Size.Height / 4), - firstScreen.Size.Width / 2, - firstScreen.Size.Height / 2); + return this.GetDefaultPosition(); } - return savedPosition; + // Rescale the saved geometry if the monitor under it now has a different + // scale than when it was saved (e.g. monitor swapped, scale changed). + var savedDpiX = options.DpiX > 0 ? options.DpiX : DefaultDpi; + var savedDpiY = options.DpiY > 0 ? options.DpiY : DefaultDpi; + var currentDpi = this.GetDpiForPosition(savedPosition.Left, savedPosition.Top); + + if (savedDpiX != currentDpi || savedDpiY != currentDpi) + { + var scaleX = (double)currentDpi / savedDpiX; + var scaleY = (double)currentDpi / savedDpiY; + savedPosition = new Rectangle( + (int)(savedPosition.X * scaleX), + (int)(savedPosition.Y * scaleY), + (int)(savedPosition.Width * scaleX), + (int)(savedPosition.Height * scaleY)); + } + + return this.EnsurePositionIsOnScreen(savedPosition); } - private static List GetDummyScreens() + private Rectangle GetDefaultPosition() { - // TODO: Use X11/Wayland APIs to enumerate actual screens - // For now, return a single dummy screen with reasonable defaults - return [new Screen(0, new Rectangle(0, 0, 1920, 1080))]; + var monitor = this.GetPrimaryMonitor(); + if (monitor is not { } m || m.Screen.Size.IsEmpty) + { + // Reasonable fallback when GDK could not enumerate any monitor. + return new Rectangle(0, 100, 1000, 900); + } + + var bounds = m.Screen.Size; + return new Rectangle( + bounds.X + (bounds.Width / 4), + bounds.Y + (bounds.Height / 4), + bounds.Width / 2, + bounds.Height / 2); + } + + private Rectangle EnsurePositionIsOnScreen(Rectangle position) + { + var monitors = this.GetMonitors(); + if (monitors.Count == 0) + { + return position; + } + + var centerX = position.Left + (position.Width / 2); + var centerY = position.Top + (position.Height / 2); + var isOnScreen = monitors.Any(m => + centerX >= m.Screen.Size.Left && centerX <= m.Screen.Size.Right && + centerY >= m.Screen.Size.Top && centerY <= m.Screen.Size.Bottom); + + if (isOnScreen) + { + return position; + } + + // Off-screen (e.g. a disconnected monitor): re-home onto the primary. + var primary = this.GetPrimaryMonitor() ?? monitors[0]; + var bounds = primary.Screen.Size; + return new Rectangle( + bounds.X + (bounds.Width / 4), + bounds.Y + (bounds.Height / 4), + Math.Min(position.Width, bounds.Width / 2), + Math.Min(position.Height, bounds.Height / 2)); + } + + /// + /// Returns the effective DPI (scale * 96) used for the saved-position rescale. + /// A single effective scale is used for the whole desktop: webview zoom is one + /// global value, and under the forced X11 backend GDK cannot report reliable + /// per-monitor scales anyway. + /// + private int GetDpiForPosition(int x, int y) + { + return (int)Math.Round(this.GetEffectiveScale() * DefaultDpi); + } + + private MonitorInfo? GetPrimaryMonitor() + { + var monitors = this.GetMonitors(); + if (monitors.Count == 0) + { + return null; + } + + foreach (var m in monitors) + { + if (m.IsPrimary) + { + return m; + } + } + + return monitors[0]; + } + + private IReadOnlyList GetMonitors() + { + if (this.cachedMonitors is { Count: > 0 }) + { + return this.cachedMonitors; + } + + var monitors = EnumerateMonitors(this.logger); + if (monitors.Count > 0) + { + // Only cache a successful enumeration so an early (pre-GTK) call can retry. + this.cachedMonitors = monitors; + } + + return monitors; + } + + private static IReadOnlyList EnumerateMonitors(ILogger logger) + { + var scopedLogger = logger.CreateScopedLogger(); + + // Ensure a default display exists in case monitors are queried before + // Photino created its first window. gtk_init_check is idempotent. + NativeMethods.gtk_init_check(nint.Zero, nint.Zero); + + var display = NativeMethods.gdk_display_get_default(); + if (display == nint.Zero) + { + scopedLogger.LogWarning("No default GdkDisplay available; cannot enumerate monitors"); + return []; + } + + var count = NativeMethods.gdk_display_get_n_monitors(display); + if (count <= 0) + { + scopedLogger.LogWarning("GDK reported {Count} monitors", count); + return []; + } + + var primary = NativeMethods.gdk_display_get_primary_monitor(display); + var result = new List(count); + for (var i = 0; i < count; i++) + { + var monitor = NativeMethods.gdk_display_get_monitor(display, i); + if (monitor == nint.Zero) + { + continue; + } + + NativeMethods.gdk_monitor_get_geometry(monitor, out var geometry); + var screen = new Screen(i, new Rectangle(geometry.X, geometry.Y, geometry.Width, geometry.Height)); + result.Add(new MonitorInfo(screen, monitor == primary && primary != nint.Zero)); + } + + scopedLogger.LogDebug("Enumerated {Count} monitor(s) via GDK", result.Count); + return result; } } diff --git a/Daybreak.Linux/Utils/DisplayScale.cs b/Daybreak.Linux/Utils/DisplayScale.cs new file mode 100644 index 00000000..3cf821e2 --- /dev/null +++ b/Daybreak.Linux/Utils/DisplayScale.cs @@ -0,0 +1,337 @@ +using System.Diagnostics; +using System.Globalization; +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace Daybreak.Linux.Utils; + +/// +/// Detects the effective desktop UI scale factor on Linux so the Photino webview +/// can be zoomed to match HiDPI monitors. +/// +/// +/// +/// This is needed because Daybreak forces the GTK X11 backend (see +/// Launcher.ForceX11Backend): under XWayland, GTK/GDK and therefore +/// Photino's built-in gdk_monitor_get_scale_factor based auto-zoom always +/// report a scale of 1, so a fractional Wayland scale (e.g. 1.5) is invisible and +/// the UI renders too small. +/// +/// +/// Detection is layered, first match wins, so it degrades safely across +/// environments: +/// +/// GDK_SCALE * GDK_DPI_SCALE environment override (any session). +/// Wayland session: the focused monitor's scale from the compositor +/// (hyprctl / swaymsg / wlr-randr / kscreen-doctor). Shelling out keeps the +/// detection out-of-process so a misbehaving tool cannot crash the launcher. +/// X11 session: Xft.dpi / 96 (set by KDE/GNOME when scaling on X11). +/// Fallback: 1.0 (current behaviour, no regression). +/// +/// +/// +internal static class DisplayScale +{ + private const double MinScale = 0.5; + private const double MaxScale = 4.0; + private const int ProcessTimeoutMs = 2000; + + /// + /// Returns the effective UI scale (e.g. 1.0, 1.5, 2.0), clamped to a sane range. + /// Returns 1.0 if nothing could be determined. + /// + public static double GetEffectiveScale(ILogger logger) + { + if (TryGetEnvScale(out var envScale)) + { + logger.LogDebug("Display scale {Scale} from GDK_SCALE/GDK_DPI_SCALE", envScale); + return Clamp(envScale); + } + + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"))) + { + if (TryGetWaylandScale(logger, out var wlScale)) + { + logger.LogDebug("Display scale {Scale} from Wayland compositor", wlScale); + return Clamp(wlScale); + } + } + else if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DISPLAY"))) + { + if (TryGetXftDpiScale(logger, out var x11Scale)) + { + logger.LogDebug("Display scale {Scale} from Xft.dpi", x11Scale); + return Clamp(x11Scale); + } + } + + logger.LogDebug("No display scale detected; defaulting to 1.0"); + return 1.0; + } + + private static bool TryGetEnvScale(out double scale) + { + scale = 1.0; + var gdkScale = Environment.GetEnvironmentVariable("GDK_SCALE"); + var gdkDpiScale = Environment.GetEnvironmentVariable("GDK_DPI_SCALE"); + var any = false; + + if (double.TryParse(gdkScale, NumberStyles.Float, CultureInfo.InvariantCulture, out var gs) && gs > 0) + { + scale *= gs; + any = true; + } + + if (double.TryParse(gdkDpiScale, NumberStyles.Float, CultureInfo.InvariantCulture, out var gd) && gd > 0) + { + scale *= gd; + any = true; + } + + return any; + } + + private static bool TryGetWaylandScale(ILogger logger, out double scale) + { + scale = 1.0; + + // hyprctl and swaymsg both emit a JSON array of monitors/outputs with + // "focused" and "scale" fields, so they share a parser. The focused + // output is where a new window spawns, which is exactly what we want. + if (TryRun("hyprctl", "monitors -j", logger, out var hypr) && + TryParseFocusedScale(hypr, out scale)) + { + return true; + } + + if (TryRun("swaymsg", "-t get_outputs -r", logger, out var sway) && + TryParseFocusedScale(sway, out scale)) + { + return true; + } + + // wlr-randr (generic wlroots) and kscreen-doctor (KDE) have no "focused" + // flag; fall back to the first scaled output reported. + if (TryRun("wlr-randr", "--json", logger, out var wlr) && + TryParseFirstScale(wlr, "scale", out scale)) + { + return true; + } + + if (TryRun("kscreen-doctor", "-j", logger, out var kscreen) && + TryParseKScreenScale(kscreen, out scale)) + { + return true; + } + + return false; + } + + private static bool TryParseFocusedScale(string json, out double scale) + { + scale = 1.0; + try + { + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.ValueKind != JsonValueKind.Array) + { + return false; + } + + double? firstScale = null; + foreach (var output in doc.RootElement.EnumerateArray()) + { + if (!output.TryGetProperty("scale", out var scaleProp) || + scaleProp.ValueKind != JsonValueKind.Number) + { + continue; + } + + var value = scaleProp.GetDouble(); + firstScale ??= value; + + if (output.TryGetProperty("focused", out var focused) && + focused.ValueKind is JsonValueKind.True) + { + scale = value; + return value > 0; + } + } + + if (firstScale is > 0) + { + scale = firstScale.Value; + return true; + } + } + catch (JsonException) + { + } + + return false; + } + + private static bool TryParseFirstScale(string json, string property, out double scale) + { + scale = 1.0; + try + { + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.ValueKind != JsonValueKind.Array) + { + return false; + } + + foreach (var output in doc.RootElement.EnumerateArray()) + { + if (output.TryGetProperty(property, out var scaleProp) && + scaleProp.ValueKind == JsonValueKind.Number) + { + var value = scaleProp.GetDouble(); + if (value > 0) + { + scale = value; + return true; + } + } + } + } + catch (JsonException) + { + } + + return false; + } + + private static bool TryParseKScreenScale(string json, out double scale) + { + scale = 1.0; + try + { + using var doc = JsonDocument.Parse(json); + if (!doc.RootElement.TryGetProperty("outputs", out var outputs) || + outputs.ValueKind != JsonValueKind.Array) + { + return false; + } + + double? firstScale = null; + foreach (var output in outputs.EnumerateArray()) + { + if (!output.TryGetProperty("scale", out var scaleProp) || + scaleProp.ValueKind != JsonValueKind.Number) + { + continue; + } + + var value = scaleProp.GetDouble(); + firstScale ??= value; + + if (output.TryGetProperty("enabled", out var enabled) && + enabled.ValueKind is JsonValueKind.True && + value > 0) + { + scale = value; + return true; + } + } + + if (firstScale is > 0) + { + scale = firstScale.Value; + return true; + } + } + catch (JsonException) + { + } + + return false; + } + + private static bool TryGetXftDpiScale(ILogger logger, out double scale) + { + scale = 1.0; + if (!TryRun("xrdb", "-query", logger, out var output)) + { + return false; + } + + foreach (var line in output.Split('\n')) + { + if (!line.StartsWith("Xft.dpi:", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var value = line["Xft.dpi:".Length..].Trim(); + if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var dpi) && dpi > 0) + { + scale = dpi / 96.0; + return true; + } + } + + return false; + } + + /// + /// Runs a process and returns its stdout, or false if the executable is + /// missing, times out, or exits non-zero. + /// + private static bool TryRun(string fileName, string arguments, ILogger logger, out string output) + { + output = string.Empty; + try + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }, + }; + + if (!process.Start()) + { + return false; + } + + var stdout = process.StandardOutput.ReadToEnd(); + if (!process.WaitForExit(ProcessTimeoutMs)) + { + try + { + process.Kill(entireProcessTree: true); + } + catch + { + // Best effort; the process will be reaped by the OS. + } + + return false; + } + + if (process.ExitCode != 0) + { + return false; + } + + output = stdout; + return !string.IsNullOrWhiteSpace(output); + } + catch (Exception e) when (e is System.ComponentModel.Win32Exception or InvalidOperationException) + { + // Tool not installed / not on PATH — expected, try the next source. + logger.LogDebug("Display scale tool '{Tool}' unavailable: {Message}", fileName, e.Message); + return false; + } + } + + private static double Clamp(double scale) => Math.Clamp(scale, MinScale, MaxScale); +} diff --git a/Daybreak.Linux/Utils/NativeMethods.cs b/Daybreak.Linux/Utils/NativeMethods.cs index 652f1dac..416f9468 100644 --- a/Daybreak.Linux/Utils/NativeMethods.cs +++ b/Daybreak.Linux/Utils/NativeMethods.cs @@ -14,6 +14,7 @@ public static partial class NativeMethods public const string GtkLib = "libgtk-3.so.0"; public const string GdkLib = "libgdk-3.so.0"; public const string GlibLib = "libglib-2.0.so.0"; + public const string LibcLib = "libc"; #endregion @@ -208,6 +209,16 @@ public static partial class NativeMethods #region GTK3 P/Invoke + /// + /// Initializes GTK without aborting on failure. Idempotent and safe to call + /// repeatedly (no-op once GTK is already initialized). Used to guarantee a + /// default GdkDisplay exists before enumerating monitors, in case it + /// is queried before Photino has created its first window. + /// + [LibraryImport(GtkLib, EntryPoint = "gtk_init_check")] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool gtk_init_check(nint argc, nint argv); + [LibraryImport(GtkLib, EntryPoint = "gtk_widget_get_window")] public static partial nint gtk_widget_get_window(nint widget); @@ -294,5 +305,60 @@ public static partial class NativeMethods [LibraryImport(GdkLib, EntryPoint = "gdk_x11_window_get_xid")] public static partial ulong gdk_x11_window_get_xid(nint gdkWindow); + // --- Monitor enumeration / scaling --- + + /// GdkRectangle: monitor geometry in application (logical) pixels. + [StructLayout(LayoutKind.Sequential)] + public struct GdkRectangle + { + public int X; + public int Y; + public int Width; + public int Height; + } + + [LibraryImport(GdkLib, EntryPoint = "gdk_display_get_n_monitors")] + public static partial int gdk_display_get_n_monitors(nint display); + + [LibraryImport(GdkLib, EntryPoint = "gdk_display_get_monitor")] + public static partial nint gdk_display_get_monitor(nint display, int monitorNum); + + [LibraryImport(GdkLib, EntryPoint = "gdk_display_get_primary_monitor")] + public static partial nint gdk_display_get_primary_monitor(nint display); + + [LibraryImport(GdkLib, EntryPoint = "gdk_display_get_monitor_at_point")] + public static partial nint gdk_display_get_monitor_at_point(nint display, int x, int y); + + [LibraryImport(GdkLib, EntryPoint = "gdk_monitor_get_geometry")] + public static partial void gdk_monitor_get_geometry(nint monitor, out GdkRectangle geometry); + + /// + /// Returns the integer scale factor (1, 2, ...) GDK applies to the monitor. + /// Under X11/XWayland this reflects GDK_SCALE / Xft.dpi-derived integer + /// scaling; true fractional per-monitor scaling requires the Wayland backend. + /// + /// + /// Returns the integer scale factor (1, 2, ...) GDK applies to the monitor. + /// Note: Daybreak does not use this for UI scaling because, under the forced + /// X11 backend, GDK always reports 1 regardless of the real (fractional) + /// Wayland scale; see . + /// + [LibraryImport(GdkLib, EntryPoint = "gdk_monitor_get_scale_factor")] + public static partial int gdk_monitor_get_scale_factor(nint monitor); + + #endregion + + #region libc P/Invoke + + /// + /// Sets an environment variable in the native C runtime. Required because + /// + /// updates only the managed environment cache and is NOT visible to native + /// libraries' getenv() (e.g. GTK reading GDK_BACKEND at gtk_init). + /// + /// When 0, an existing value is preserved. + [LibraryImport(LibcLib, EntryPoint = "setenv", StringMarshalling = StringMarshalling.Utf8)] + public static partial int setenv(string name, string value, int overwrite); + #endregion }