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.
511 lines
19 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|