Files

511 lines
19 KiB
C#
Raw Permalink Normal View History

// 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
{
/// <summary>
/// Runs a MAUI application on Linux.
/// This is the main entry point for Linux apps.
/// </summary>
/// <param name="app">The MauiApp to run.</param>
/// <param name="args">Command line arguments.</param>
public static void Run(MauiApp app, string[] args)
{
Run(app, args, null);
}
/// <summary>
/// Runs a MAUI application on Linux with options.
/// </summary>
/// <param name="app">The MauiApp to run.</param>
/// <param name="args">Command line arguments.</param>
/// <param name="configure">Optional configuration action.</param>
public static void Run(MauiApp app, string[] args, Action<LinuxApplicationOptions>? 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<LinuxApplicationOptions>()
?? 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<IApplication>();
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<Microsoft.Maui.Controls.Window>;
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<Microsoft.Maui.Controls.Window>;
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;
}
}
}
/// <summary>
/// Shows the main window and runs the event loop.
/// </summary>
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));
}
}
/// <summary>
/// Forces all views to refresh their theme-dependent properties.
/// This is needed because AppThemeBinding may not automatically trigger
/// property mappers on all platforms.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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<SkiaView> 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;
}
}
}