// 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; using Microsoft.Maui.Platform.Linux.Interop; namespace Microsoft.Maui.Platform.Linux.Services; /// /// Service for querying and monitoring display configuration using XRandR. /// public class MonitorService : IDisposable { private static MonitorService? _instance; private static readonly object _lock = new(); private IntPtr _display; private IntPtr _rootWindow; private List _monitors = new(); private bool _initialized; private bool _disposed; private int _eventBase; private int _errorBase; /// /// Gets the singleton instance of the monitor service. /// public static MonitorService Instance { get { if (_instance == null) { lock (_lock) { _instance ??= new MonitorService(); } } return _instance; } } /// /// Gets the list of connected monitors. /// public IReadOnlyList Monitors { get { EnsureInitialized(); return _monitors.AsReadOnly(); } } /// /// Gets the primary monitor. /// public MonitorInfo? PrimaryMonitor { get { EnsureInitialized(); return _monitors.FirstOrDefault(m => m.IsPrimary) ?? _monitors.FirstOrDefault(); } } /// /// Gets the total virtual desktop bounds (union of all monitors). /// public (int X, int Y, int Width, int Height) VirtualDesktopBounds { get { EnsureInitialized(); if (_monitors.Count == 0) return (0, 0, 1920, 1080); // Default fallback int minX = _monitors.Min(m => m.X); int minY = _monitors.Min(m => m.Y); int maxX = _monitors.Max(m => m.X + m.Width); int maxY = _monitors.Max(m => m.Y + m.Height); return (minX, minY, maxX - minX, maxY - minY); } } /// /// Event raised when monitor configuration changes. /// public event EventHandler? ConfigurationChanged; private MonitorService() { } private void EnsureInitialized() { if (_initialized) return; lock (_lock) { if (_initialized) return; try { _display = X11.XOpenDisplay(IntPtr.Zero); if (_display == IntPtr.Zero) { DiagnosticLog.Error("MonitorService", "Failed to open X11 display"); _initialized = true; return; } int screen = X11.XDefaultScreen(_display); _rootWindow = X11.XRootWindow(_display, screen); // Check if XRandR is available if (XRandR.XRRQueryExtension(_display, out _eventBase, out _errorBase) == 0) { DiagnosticLog.Warn("MonitorService", "XRandR extension not available"); _initialized = true; return; } if (XRandR.XRRQueryVersion(_display, out int major, out int minor) == 0) { DiagnosticLog.Error("MonitorService", "Failed to query XRandR version"); _initialized = true; return; } DiagnosticLog.Debug("MonitorService", $"XRandR {major}.{minor} available"); RefreshMonitors(); _initialized = true; } catch (Exception ex) { DiagnosticLog.Error("MonitorService", $"Initialization failed: {ex.Message}"); _initialized = true; } } } /// /// Refreshes the monitor list from the system. /// public void RefreshMonitors() { if (_display == IntPtr.Zero) return; var newMonitors = new List(); IntPtr resources = IntPtr.Zero; try { resources = XRandR.XRRGetScreenResourcesCurrent(_display, _rootWindow); if (resources == IntPtr.Zero) { DiagnosticLog.Error("MonitorService", "Failed to get screen resources"); return; } var res = Marshal.PtrToStructure(resources); // Get array of output IDs var outputIds = new ulong[res.NOutput]; for (int i = 0; i < res.NOutput; i++) { outputIds[i] = (ulong)Marshal.ReadInt64(res.Outputs, i * 8); } // Track which monitor is at 0,0 (likely primary) MonitorInfo? primaryCandidate = null; foreach (var outputId in outputIds) { IntPtr outputInfo = IntPtr.Zero; IntPtr crtcInfo = IntPtr.Zero; try { outputInfo = XRandR.XRRGetOutputInfo(_display, resources, outputId); if (outputInfo == IntPtr.Zero) continue; var output = Marshal.PtrToStructure(outputInfo); // Skip disconnected outputs if (output.Connection != XRandR.RR_Connected) continue; // Skip outputs without a CRTC (not currently displaying) if (output.Crtc == 0) continue; crtcInfo = XRandR.XRRGetCrtcInfo(_display, resources, output.Crtc); if (crtcInfo == IntPtr.Zero) continue; var crtc = Marshal.PtrToStructure(crtcInfo); // Get output name string name = output.Name != IntPtr.Zero && output.NameLen > 0 ? Marshal.PtrToStringAnsi(output.Name, output.NameLen) ?? $"Output-{outputId}" : $"Output-{outputId}"; // Calculate refresh rate from mode info double refreshRate = GetRefreshRate(resources, crtc.Mode); var monitor = new MonitorInfo { Id = outputId, Name = name, X = crtc.X, Y = crtc.Y, Width = (int)crtc.Width, Height = (int)crtc.Height, PhysicalWidthMm = (int)output.MmWidth, PhysicalHeightMm = (int)output.MmHeight, RefreshRate = refreshRate, IsPrimary = false // Will be set below }; newMonitors.Add(monitor); // Track the monitor at 0,0 as primary candidate if (crtc.X == 0 && crtc.Y == 0) { primaryCandidate = monitor; } } finally { if (crtcInfo != IntPtr.Zero) XRandR.XRRFreeCrtcInfo(crtcInfo); if (outputInfo != IntPtr.Zero) XRandR.XRRFreeOutputInfo(outputInfo); } } // Set primary monitor (the one at 0,0 or the first one) if (newMonitors.Count > 0) { var primary = primaryCandidate ?? newMonitors[0]; var index = newMonitors.IndexOf(primary); if (index >= 0) { newMonitors[index] = primary with { IsPrimary = true }; } } var oldMonitors = _monitors; _monitors = newMonitors; // Log detected monitors DiagnosticLog.Debug("MonitorService", $"Detected {_monitors.Count} monitor(s):"); foreach (var monitor in _monitors) { DiagnosticLog.Debug("MonitorService", $" {monitor}"); } // Notify if configuration changed if (!MonitorListsEqual(oldMonitors, newMonitors)) { ConfigurationChanged?.Invoke(this, new MonitorConfigurationChangedEventArgs(_monitors)); } } finally { if (resources != IntPtr.Zero) XRandR.XRRFreeScreenResources(resources); } } private double GetRefreshRate(IntPtr resources, ulong modeId) { if (modeId == 0) return 60.0; // Default var res = Marshal.PtrToStructure(resources); for (int i = 0; i < res.NMode; i++) { var modePtr = res.Modes + i * Marshal.SizeOf(); var mode = Marshal.PtrToStructure(modePtr); if (mode.Id == modeId) { if (mode.HTotal > 0 && mode.VTotal > 0 && mode.DotClock > 0) { return (double)mode.DotClock / (mode.HTotal * mode.VTotal); } break; } } return 60.0; // Default fallback } private bool MonitorListsEqual(List a, List b) { if (a.Count != b.Count) return false; for (int i = 0; i < a.Count; i++) { if (a[i].Id != b[i].Id || a[i].X != b[i].X || a[i].Y != b[i].Y || a[i].Width != b[i].Width || a[i].Height != b[i].Height) { return false; } } return true; } /// /// Gets the monitor containing the specified point. /// public MonitorInfo? GetMonitorAt(int x, int y) { EnsureInitialized(); return _monitors.FirstOrDefault(m => x >= m.X && x < m.X + m.Width && y >= m.Y && y < m.Y + m.Height); } /// /// Gets the monitor containing the center of the specified rectangle. /// public MonitorInfo? GetMonitorFromRect(int x, int y, int width, int height) { int centerX = x + width / 2; int centerY = y + height / 2; return GetMonitorAt(centerX, centerY); } /// /// Gets the monitor by name (e.g., "HDMI-1", "DP-2"). /// public MonitorInfo? GetMonitorByName(string name) { EnsureInitialized(); return _monitors.FirstOrDefault(m => m.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); } public void Dispose() { if (!_disposed) { if (_display != IntPtr.Zero) { X11.XCloseDisplay(_display); _display = IntPtr.Zero; } _disposed = true; } } } /// /// Event args for monitor configuration changes. /// public class MonitorConfigurationChangedEventArgs : EventArgs { /// /// Gets the updated list of monitors. /// public IReadOnlyList Monitors { get; } public MonitorConfigurationChangedEventArgs(IReadOnlyList monitors) { Monitors = monitors; } }