Files
maui-linux/LinuxApplication.Lifecycle.cs
logikonline 077abc2feb refactor: split large files into partial classes by concern
Split LinuxApplication into Input and Lifecycle partials. Extract SkiaView into Accessibility, Drawing, and Input partials. Split SkiaEntry and SkiaEditor into Drawing and Input partials. Extract TextRenderingHelper from SkiaRenderingEngine. Create dedicated files for SkiaAbsoluteLayout, SkiaGrid, and SkiaStackLayout. This reduces file sizes from 40K+ lines to manageable units organized by responsibility.
2026-03-06 22:36:23 -05:00

511 lines
19 KiB
C#

// 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;
}
}
}