// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Hosting; using Microsoft.Maui.Platform.Linux.Rendering; using Microsoft.Maui.Platform.Linux.Window; using Microsoft.Maui.Platform.Linux.Services; using Microsoft.Maui.Platform; namespace Microsoft.Maui.Platform.Linux; /// /// Main Linux application class that bootstraps the MAUI application. /// public class LinuxApplication : IDisposable { private X11Window? _mainWindow; private SkiaRenderingEngine? _renderingEngine; private SkiaView? _rootView; private SkiaView? _focusedView; private SkiaView? _hoveredView; private SkiaView? _capturedView; // View that has captured pointer events during drag private bool _disposed; /// /// Gets the current application instance. /// public static LinuxApplication? Current { get; private set; } /// /// Gets the main window. /// public X11Window? MainWindow => _mainWindow; /// /// Gets the rendering engine. /// public SkiaRenderingEngine? RenderingEngine => _renderingEngine; /// /// Gets or sets the root view. /// public SkiaView? RootView { get => _rootView; set { _rootView = value; if (_rootView != null && _mainWindow != null) { _rootView.Arrange(new SkiaSharp.SKRect( 0, 0, _mainWindow.Width, _mainWindow.Height)); } } } /// /// Gets or sets the currently focused view. /// public SkiaView? FocusedView { get => _focusedView; set { if (_focusedView != value) { if (_focusedView != null) { _focusedView.IsFocused = false; } _focusedView = value; if (_focusedView != null) { _focusedView.IsFocused = true; } } } } /// /// Creates a new Linux application. /// public LinuxApplication() { Current = this; // Set up dialog service invalidation callback LinuxDialogService.SetInvalidateCallback(() => _renderingEngine?.InvalidateAll()); } /// /// Runs a MAUI application on Linux. /// This is the main entry point for Linux apps. /// /// The MauiApp to run. /// Command line arguments. public static void Run(MauiApp app, string[] args) { Run(app, args, null); } /// /// Runs a MAUI application on Linux with options. /// /// The MauiApp to run. /// Command line arguments. /// Optional configuration action. public static void Run(MauiApp app, string[] args, Action? configure) { var options = app.Services.GetService() ?? new LinuxApplicationOptions(); configure?.Invoke(options); ParseCommandLineOptions(args, options); using var linuxApp = new LinuxApplication(); linuxApp.Initialize(options); // Create MAUI context var mauiContext = new Hosting.LinuxMauiContext(app.Services, linuxApp); // Get the application and render it var application = app.Services.GetService(); SkiaView? rootView = null; if (application is Microsoft.Maui.Controls.Application mauiApplication) { // Force Application.Current to be this instance // The constructor sets Current = this, but we ensure it here var currentProperty = typeof(Microsoft.Maui.Controls.Application).GetProperty("Current"); if (currentProperty != null && currentProperty.CanWrite) { currentProperty.SetValue(null, mauiApplication); } if (mauiApplication.MainPage != null) { // Create a MAUI Window and add it to the application // This ensures Shell.Current works (it reads from Application.Current.Windows[0].Page) var mainPage = mauiApplication.MainPage; // Always ensure we have a window with the Shell/Page var windowsField = typeof(Microsoft.Maui.Controls.Application).GetField("_windows", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); var windowsList = windowsField?.GetValue(mauiApplication) as System.Collections.Generic.List; if (windowsList != null && windowsList.Count == 0) { var mauiWindow = new Microsoft.Maui.Controls.Window(mainPage); windowsList.Add(mauiWindow); mauiWindow.Parent = mauiApplication; } else if (windowsList != null && windowsList.Count > 0 && windowsList[0].Page == null) { // Window exists but has no page - set it windowsList[0].Page = mainPage; } var renderer = new Hosting.LinuxViewRenderer(mauiContext); rootView = renderer.RenderPage(mainPage); // Update window title based on app name (NavigationPage.Title takes precedence) string windowTitle = "OpenMaui App"; if (mainPage is Microsoft.Maui.Controls.NavigationPage navPage) { // Prefer NavigationPage.Title (app name) over CurrentPage.Title (page name) for window title windowTitle = navPage.Title ?? windowTitle; } else if (mainPage is Microsoft.Maui.Controls.Shell shell) { windowTitle = shell.Title ?? windowTitle; } else { windowTitle = mainPage.Title ?? windowTitle; } linuxApp.SetWindowTitle(windowTitle); } } // Fallback to demo if no view if (rootView == null) { rootView = Hosting.LinuxProgramHost.CreateDemoView(); } linuxApp.RootView = rootView; linuxApp.Run(); } private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options) { for (int i = 0; i < args.Length; i++) { switch (args[i].ToLowerInvariant()) { case "--title" when i + 1 < args.Length: options.Title = args[++i]; break; case "--width" when i + 1 < args.Length && int.TryParse(args[i + 1], out var w): options.Width = w; i++; break; case "--height" when i + 1 < args.Length && int.TryParse(args[i + 1], out var h): options.Height = h; i++; break; } } } /// /// Initializes the application with the specified options. /// public void Initialize(LinuxApplicationOptions options) { // Create the main window _mainWindow = new X11Window( options.Title ?? "MAUI Application", options.Width, options.Height); // Create the rendering engine _renderingEngine = new SkiaRenderingEngine(_mainWindow); // Wire up events _mainWindow.Resized += OnWindowResized; _mainWindow.Exposed += OnWindowExposed; _mainWindow.KeyDown += OnKeyDown; _mainWindow.KeyUp += OnKeyUp; _mainWindow.TextInput += OnTextInput; _mainWindow.PointerMoved += OnPointerMoved; _mainWindow.PointerPressed += OnPointerPressed; _mainWindow.PointerReleased += OnPointerReleased; _mainWindow.Scroll += OnScroll; _mainWindow.CloseRequested += OnCloseRequested; // Register platform services RegisterServices(); } private void RegisterServices() { // Platform services would be registered with the DI container here // For now, we create singleton instances } /// /// Sets the window title. /// public void SetWindowTitle(string title) { _mainWindow?.SetTitle(title); } /// /// Shows the main window and runs the event loop. /// public void Run() { if (_mainWindow == null) throw new InvalidOperationException("Application not initialized"); _mainWindow.Show(); // Initial render Render(); // Run the event loop while (_mainWindow.IsRunning) { _mainWindow.ProcessEvents(); // Update animations and render UpdateAnimations(); Render(); // Small delay to prevent 100% CPU usage Thread.Sleep(1); } } private void UpdateAnimations() { // Update cursor blink for entry controls if (_focusedView is SkiaEntry entry) { entry.UpdateCursorBlink(); } } private void Render() { if (_renderingEngine != null && _rootView != null) { _renderingEngine.Render(_rootView); } } private void OnWindowResized(object? sender, (int Width, int Height) size) { if (_rootView != null) { // Re-measure with new available size, then arrange var availableSize = new SkiaSharp.SKSize(size.Width, size.Height); _rootView.Measure(availableSize); _rootView.Arrange(new SkiaSharp.SKRect(0, 0, size.Width, size.Height)); } _renderingEngine?.InvalidateAll(); } private void OnWindowExposed(object? sender, EventArgs e) { Render(); } private void OnKeyDown(object? sender, KeyEventArgs e) { // Route to dialog if one is active if (LinuxDialogService.HasActiveDialog) { LinuxDialogService.TopDialog?.OnKeyDown(e); return; } if (_focusedView != null) { _focusedView.OnKeyDown(e); } } private void OnKeyUp(object? sender, KeyEventArgs e) { // Route to dialog if one is active if (LinuxDialogService.HasActiveDialog) { LinuxDialogService.TopDialog?.OnKeyUp(e); return; } if (_focusedView != null) { _focusedView.OnKeyUp(e); } } private void OnTextInput(object? sender, TextInputEventArgs e) { if (_focusedView != null) { _focusedView.OnTextInput(e); } } private void OnPointerMoved(object? sender, PointerEventArgs e) { // Route to dialog if one is active if (LinuxDialogService.HasActiveDialog) { LinuxDialogService.TopDialog?.OnPointerMoved(e); return; } if (_rootView != null) { // If a view has captured the pointer, send all events to it if (_capturedView != null) { _capturedView.OnPointerMoved(e); return; } // Check for popup overlay first var popupOwner = SkiaView.GetPopupOwnerAt(e.X, e.Y); var hitView = popupOwner ?? _rootView.HitTest(e.X, e.Y); // Track hover state changes if (hitView != _hoveredView) { _hoveredView?.OnPointerExited(e); _hoveredView = hitView; _hoveredView?.OnPointerEntered(e); } hitView?.OnPointerMoved(e); } } private void OnPointerPressed(object? sender, PointerEventArgs e) { Console.WriteLine($"[LinuxApplication] OnPointerPressed at ({e.X}, {e.Y})"); // Route to dialog if one is active if (LinuxDialogService.HasActiveDialog) { LinuxDialogService.TopDialog?.OnPointerPressed(e); return; } if (_rootView != null) { // Check for popup overlay first var popupOwner = SkiaView.GetPopupOwnerAt(e.X, e.Y); var hitView = popupOwner ?? _rootView.HitTest(e.X, e.Y); Console.WriteLine($"[LinuxApplication] HitView: {hitView?.GetType().Name ?? "null"}, rootView: {_rootView.GetType().Name}"); if (hitView != null) { // Capture pointer to this view for drag operations _capturedView = hitView; // Update focus if (hitView.IsFocusable) { FocusedView = hitView; } Console.WriteLine($"[LinuxApplication] Calling OnPointerPressed on {hitView.GetType().Name}"); hitView.OnPointerPressed(e); } else { // Close any open popups when clicking outside if (SkiaView.HasActivePopup && _focusedView != null) { _focusedView.OnFocusLost(); } FocusedView = null; } } } private void OnPointerReleased(object? sender, PointerEventArgs e) { // Route to dialog if one is active if (LinuxDialogService.HasActiveDialog) { LinuxDialogService.TopDialog?.OnPointerReleased(e); return; } if (_rootView != null) { // If a view has captured the pointer, send release to it if (_capturedView != null) { _capturedView.OnPointerReleased(e); _capturedView = null; // Release capture return; } // Check for popup overlay first var popupOwner = SkiaView.GetPopupOwnerAt(e.X, e.Y); var hitView = popupOwner ?? _rootView.HitTest(e.X, e.Y); hitView?.OnPointerReleased(e); } } private void OnScroll(object? sender, ScrollEventArgs e) { Console.WriteLine($"[LinuxApplication] OnScroll - X={e.X}, Y={e.Y}, DeltaX={e.DeltaX}, DeltaY={e.DeltaY}"); if (_rootView != null) { var hitView = _rootView.HitTest(e.X, e.Y); Console.WriteLine($"[LinuxApplication] HitView: {hitView?.GetType().Name ?? "null"}"); // Bubble scroll events up to find a ScrollView var view = hitView; while (view != null) { Console.WriteLine($"[LinuxApplication] Bubbling to: {view.GetType().Name}"); if (view is SkiaScrollView scrollView) { scrollView.OnScroll(e); return; } view.OnScroll(e); if (e.Handled) return; view = view.Parent; } } } private void OnCloseRequested(object? sender, EventArgs e) { _mainWindow?.Stop(); } public void Dispose() { if (!_disposed) { _renderingEngine?.Dispose(); _mainWindow?.Dispose(); if (Current == this) Current = null; _disposed = true; } } } /// /// Options for Linux application initialization. /// public class LinuxApplicationOptions { /// /// Gets or sets the window title. /// public string? Title { get; set; } = "MAUI Application"; /// /// Gets or sets the initial window width. /// public int Width { get; set; } = 800; /// /// Gets or sets the initial window height. /// public int Height { get; set; } = 600; /// /// Gets or sets whether to use hardware acceleration. /// public bool UseHardwareAcceleration { get; set; } = true; /// /// Gets or sets the display server type. /// public DisplayServerType DisplayServer { get; set; } = DisplayServerType.Auto; /// /// Gets or sets whether to force demo mode instead of loading the application's pages. /// public bool ForceDemo { get; set; } = false; } /// /// Display server type options. /// public enum DisplayServerType { /// /// Automatically detect the display server. /// Auto, /// /// Use X11 (Xorg). /// X11, /// /// Use Wayland. /// Wayland }