// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Reflection; using System.Threading; using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Controls; using Microsoft.Maui.Dispatching; using Microsoft.Maui.Hosting; using Microsoft.Maui.Platform.Linux.Dispatching; using Microsoft.Maui.Platform.Linux.Hosting; using Microsoft.Maui.Platform.Linux.Native; using Microsoft.Maui.Platform.Linux.Rendering; using Microsoft.Maui.Platform.Linux.Services; using Microsoft.Maui.Platform.Linux.Window; using Microsoft.Maui.Platform; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux; public partial class LinuxApplication { /// /// 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) { // Force X11 backend for GTK/WebKitGTK - MUST be set before any GTK code runs Environment.SetEnvironmentVariable("GDK_BACKEND", "x11"); // Pre-initialize GTK for WebView compatibility (even when using X11 mode) int argc = 0; IntPtr argv = IntPtr.Zero; if (!GtkNative.gtk_init_check(ref argc, ref argv)) { DiagnosticLog.Warn("LinuxApplication", "GTK initialization failed - WebView may not work"); } else { DiagnosticLog.Debug("LinuxApplication", "GTK pre-initialized for WebView support"); } // Set application name for desktop integration (taskbar, etc.) // Try to get the name from environment or use executable name string? appName = Environment.GetEnvironmentVariable("APPIMAGE_NAME"); if (string.IsNullOrEmpty(appName)) { appName = Path.GetFileNameWithoutExtension(Environment.ProcessPath ?? "MauiApp"); } string prgName = appName.Replace(" ", ""); GtkNative.g_set_prgname(prgName); GtkNative.g_set_application_name(appName); DiagnosticLog.Debug("LinuxApplication", $"Set application name: {appName} (prgname: {prgName})"); // Initialize dispatcher LinuxDispatcher.Initialize(); DispatcherProvider.SetCurrent(LinuxDispatcherProvider.Instance); DiagnosticLog.Debug("LinuxApplication", "Dispatcher initialized"); var options = app.Services.GetService() ?? new LinuxApplicationOptions(); configure?.Invoke(options); ParseCommandLineOptions(args, options); var linuxApp = new LinuxApplication(); try { linuxApp.Initialize(options); // Create MAUI context var mauiContext = new LinuxMauiContext(app.Services, linuxApp); // Get the application and render it var application = app.Services.GetService(); SkiaView? rootView = null; if (application is Application mauiApplication) { // Force Application.Current to be this instance var currentProperty = typeof(Application).GetProperty("Current"); if (currentProperty != null && currentProperty.CanWrite) { currentProperty.SetValue(null, mauiApplication); } // Set initial theme based on system theme var systemTheme = SystemThemeService.Instance.CurrentTheme; DiagnosticLog.Debug("LinuxApplication", $"System theme detected at startup: {systemTheme}"); if (systemTheme == SystemTheme.Dark) { mauiApplication.UserAppTheme = AppTheme.Dark; DiagnosticLog.Debug("LinuxApplication", "Set initial UserAppTheme to Dark based on system theme"); } else { mauiApplication.UserAppTheme = AppTheme.Light; DiagnosticLog.Debug("LinuxApplication", "Set initial UserAppTheme to Light based on system theme"); } // Initialize GTK theme service and apply initial CSS GtkThemeService.ApplyTheme(); // Handle user-initiated theme changes ((BindableObject)mauiApplication).PropertyChanged += (s, e) => { if (e.PropertyName == "UserAppTheme") { DiagnosticLog.Debug("LinuxApplication", $"User theme changed to: {mauiApplication.UserAppTheme}"); // Apply GTK CSS for dialogs, menus, and window decorations GtkThemeService.ApplyTheme(); LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme(); // Force re-render the entire page to pick up theme changes linuxApp.RefreshPageForThemeChange(); // Invalidate to redraw - use correct method based on mode if (linuxApp._useGtk) { linuxApp._gtkWindow?.RequestRedraw(); } else { linuxApp._renderingEngine?.InvalidateAll(); } } }; // Handle system theme changes (e.g., GNOME/KDE dark mode toggle) SystemThemeService.Instance.ThemeChanged += (s, e) => { DiagnosticLog.Debug("LinuxApplication", $"System theme changed to: {e.NewTheme}"); // Update MAUI's UserAppTheme to match system theme // This will trigger the PropertyChanged handler which does the refresh var newAppTheme = e.NewTheme == SystemTheme.Dark ? AppTheme.Dark : AppTheme.Light; if (mauiApplication.UserAppTheme != newAppTheme) { DiagnosticLog.Debug("LinuxApplication", $"Setting UserAppTheme to {newAppTheme} to match system"); mauiApplication.UserAppTheme = newAppTheme; } else { // If UserAppTheme didn't change (user manually set it), still refresh LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme(); linuxApp.RefreshPageForThemeChange(); if (linuxApp._useGtk) { linuxApp._gtkWindow?.RequestRedraw(); } else { linuxApp._renderingEngine?.InvalidateAll(); } } }; // Get the main page - prefer CreateWindow() over deprecated MainPage Page? mainPage = null; // Try CreateWindow() first (the modern MAUI pattern) try { // CreateWindow is protected, use reflection var createWindowMethod = typeof(Application).GetMethod("CreateWindow", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, new[] { typeof(IActivationState) }, null); if (createWindowMethod != null) { var mauiWindow = createWindowMethod.Invoke(mauiApplication, new object?[] { null }) as Microsoft.Maui.Controls.Window; if (mauiWindow != null) { DiagnosticLog.Debug("LinuxApplication", $"Got Window from CreateWindow: {mauiWindow.GetType().Name}"); mainPage = mauiWindow.Page; DiagnosticLog.Debug("LinuxApplication", $"Window.Page: {mainPage?.GetType().Name}"); // Add to windows list var windowsField = typeof(Application).GetField("_windows", BindingFlags.NonPublic | BindingFlags.Instance); var windowsList = windowsField?.GetValue(mauiApplication) as List; if (windowsList != null && !windowsList.Contains(mauiWindow)) { windowsList.Add(mauiWindow); mauiWindow.Parent = mauiApplication; } } } } catch (Exception ex) { DiagnosticLog.Error("LinuxApplication", $"CreateWindow failed: {ex.Message}"); } // Fall back to deprecated MainPage if CreateWindow didn't work if (mainPage == null && mauiApplication.MainPage != null) { DiagnosticLog.Debug("LinuxApplication", $"Falling back to MainPage: {mauiApplication.MainPage.GetType().Name}"); mainPage = mauiApplication.MainPage; var windowsField = typeof(Application).GetField("_windows", BindingFlags.NonPublic | BindingFlags.Instance); var windowsList = windowsField?.GetValue(mauiApplication) as 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) { windowsList[0].Page = mainPage; } } if (mainPage != null) { var renderer = new LinuxViewRenderer(mauiContext); rootView = renderer.RenderPage(mainPage); string windowTitle = "OpenMaui App"; if (mainPage is NavigationPage navPage) { windowTitle = navPage.Title ?? windowTitle; } else if (mainPage is Shell shell) { windowTitle = shell.Title ?? windowTitle; } else { windowTitle = mainPage.Title ?? windowTitle; } linuxApp.SetWindowTitle(windowTitle); } } if (rootView == null) { rootView = LinuxProgramHost.CreateDemoView(); } linuxApp.RootView = rootView; linuxApp.Run(); } finally { linuxApp?.Dispose(); } } 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; } } } /// /// Shows the main window and runs the event loop. /// public void Run() { if (_useGtk) { RunGtk(); } else { RunX11(); } } private void RunX11() { if (_mainWindow == null) throw new InvalidOperationException("Application not initialized"); _mainWindow.Show(); Render(); DiagnosticLog.Debug("LinuxApplication", "Starting event loop"); while (_mainWindow.IsRunning) { _loopCounter++; if (_loopCounter % 1000 == 0) { DiagnosticLog.Debug("LinuxApplication", $"Loop iteration {_loopCounter}"); } _mainWindow.ProcessEvents(); SkiaWebView.ProcessGtkEvents(); UpdateAnimations(); Render(); Thread.Sleep(1); } DiagnosticLog.Debug("LinuxApplication", "Event loop ended"); } private void RunGtk() { if (_gtkWindow == null) throw new InvalidOperationException("Application not initialized"); StartHeartbeat(); PerformGtkLayout(_gtkWindow.Width, _gtkWindow.Height); _gtkWindow.RequestRedraw(); _gtkWindow.Run(); GtkHostService.Instance.Shutdown(); } private void PerformGtkLayout(int width, int height) { if (_rootView != null) { _rootView.Measure(new Microsoft.Maui.Graphics.Size(width, height)); _rootView.Arrange(new Microsoft.Maui.Graphics.Rect(0, 0, width, height)); } } /// /// Forces all views to refresh their theme-dependent properties. /// This is needed because AppThemeBinding may not automatically trigger /// property mappers on all platforms. /// private void RefreshPageForThemeChange() { DiagnosticLog.Debug("LinuxApplication", "RefreshPageForThemeChange - forcing property updates"); // First, try to trigger MAUI's RequestedThemeChanged event using reflection // This ensures AppThemeBinding bindings re-evaluate TriggerMauiThemeChanged(); if (_rootView == null) return; // Traverse the visual tree and force theme-dependent properties to update RefreshViewTheme(_rootView); } /// /// Called after theme change to refresh views. /// Note: MAUI's Application.UserAppTheme setter automatically triggers RequestedThemeChanged /// via WeakEventManager, which AppThemeBinding subscribes to. This method handles /// any additional platform-specific refresh needed. /// private void TriggerMauiThemeChanged() { var app = Application.Current; if (app == null) return; DiagnosticLog.Debug("LinuxApplication", $"Theme is now: {app.UserAppTheme}, RequestedTheme: {app.RequestedTheme}"); } private void RefreshViewTheme(SkiaView view) { // Get the associated MAUI view and handler var mauiView = view.MauiView; var handler = mauiView?.Handler; if (handler != null && mauiView != null) { // Force key properties to be re-mapped // This ensures theme-dependent bindings are re-evaluated try { // Background/BackgroundColor - both need updating for AppThemeBinding handler.UpdateValue(nameof(IView.Background)); handler.UpdateValue("BackgroundColor"); // For ImageButton, force Source to be re-mapped if (mauiView is Microsoft.Maui.Controls.ImageButton) { handler.UpdateValue(nameof(IImageSourcePart.Source)); } // For Image, force Source to be re-mapped if (mauiView is Microsoft.Maui.Controls.Image) { handler.UpdateValue(nameof(IImageSourcePart.Source)); } // For views with text colors if (mauiView is ITextStyle) { handler.UpdateValue(nameof(ITextStyle.TextColor)); } // For Entry/Editor placeholder colors if (mauiView is IPlaceholder) { handler.UpdateValue(nameof(IPlaceholder.PlaceholderColor)); } // For Border stroke if (mauiView is IBorderStroke) { handler.UpdateValue(nameof(IBorderStroke.Stroke)); } } catch (Exception ex) { DiagnosticLog.Error("LinuxApplication", $"Error refreshing theme for {mauiView.GetType().Name}: {ex.Message}"); } } // Special handling for ItemsViews (CollectionView, ListView) // Their item views are cached separately and need to be refreshed if (view is SkiaItemsView itemsView) { itemsView.RefreshTheme(); } // Special handling for NavigationPage - it stores content in _currentPage if (view is SkiaNavigationPage navPage && navPage.CurrentPage != null) { RefreshViewTheme(navPage.CurrentPage); navPage.Invalidate(); // Force redraw of navigation page } // Special handling for SkiaPage - refresh via MauiPage handler and process Content if (view is SkiaPage page) { // Refresh page properties via handler if MauiPage is set var pageHandler = page.MauiPage?.Handler; if (pageHandler != null) { try { DiagnosticLog.Debug("LinuxApplication", $"Refreshing page theme: {page.MauiPage?.GetType().Name}"); pageHandler.UpdateValue(nameof(IView.Background)); pageHandler.UpdateValue("BackgroundColor"); } catch (Exception ex) { DiagnosticLog.Error("LinuxApplication", $"Error refreshing page theme: {ex.Message}"); } } page.Invalidate(); // Force redraw to pick up theme-aware background if (page.Content != null) { RefreshViewTheme(page.Content); } } // Recursively process children // Note: SkiaLayoutView hides SkiaView.Children with 'new', so we need to cast IReadOnlyList children = view is SkiaLayoutView layout ? layout.Children : view.Children; foreach (var child in children) { RefreshViewTheme(child); } } private void Render() { if (_renderingEngine != null && _rootView != null) { _renderingEngine.Render(_rootView); } } public void Dispose() { if (!_disposed) { _renderingEngine?.Dispose(); _mainWindow?.Dispose(); if (Current == this) Current = null; _disposed = true; } } }