563 lines
17 KiB
C#
563 lines
17 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 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;
|
|
|
|
/// <summary>
|
|
/// Main Linux application class that bootstraps the MAUI application.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Gets the current application instance.
|
|
/// </summary>
|
|
public static LinuxApplication? Current { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the main window.
|
|
/// </summary>
|
|
public X11Window? MainWindow => _mainWindow;
|
|
|
|
/// <summary>
|
|
/// Gets the rendering engine.
|
|
/// </summary>
|
|
public SkiaRenderingEngine? RenderingEngine => _renderingEngine;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the root view.
|
|
/// </summary>
|
|
public SkiaView? RootView
|
|
{
|
|
get => _rootView;
|
|
set
|
|
{
|
|
_rootView = value;
|
|
if (_rootView != null && _mainWindow != null)
|
|
{
|
|
_rootView.Arrange(new SkiaSharp.SKRect(
|
|
0, 0,
|
|
_mainWindow.Width,
|
|
_mainWindow.Height));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the currently focused view.
|
|
/// </summary>
|
|
public SkiaView? FocusedView
|
|
{
|
|
get => _focusedView;
|
|
set
|
|
{
|
|
if (_focusedView != value)
|
|
{
|
|
if (_focusedView != null)
|
|
{
|
|
_focusedView.IsFocused = false;
|
|
}
|
|
|
|
_focusedView = value;
|
|
|
|
if (_focusedView != null)
|
|
{
|
|
_focusedView.IsFocused = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new Linux application.
|
|
/// </summary>
|
|
public LinuxApplication()
|
|
{
|
|
Current = this;
|
|
|
|
// Set up dialog service invalidation callback
|
|
LinuxDialogService.SetInvalidateCallback(() => _renderingEngine?.InvalidateAll());
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
var options = app.Services.GetService<LinuxApplicationOptions>()
|
|
?? 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<IApplication>();
|
|
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<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)
|
|
{
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the application with the specified options.
|
|
/// </summary>
|
|
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
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the window title.
|
|
/// </summary>
|
|
public void SetWindowTitle(string title)
|
|
{
|
|
_mainWindow?.SetTitle(title);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the main window and runs the event loop.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Options for Linux application initialization.
|
|
/// </summary>
|
|
public class LinuxApplicationOptions
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets the window title.
|
|
/// </summary>
|
|
public string? Title { get; set; } = "MAUI Application";
|
|
|
|
/// <summary>
|
|
/// Gets or sets the initial window width.
|
|
/// </summary>
|
|
public int Width { get; set; } = 800;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the initial window height.
|
|
/// </summary>
|
|
public int Height { get; set; } = 600;
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether to use hardware acceleration.
|
|
/// </summary>
|
|
public bool UseHardwareAcceleration { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the display server type.
|
|
/// </summary>
|
|
public DisplayServerType DisplayServer { get; set; } = DisplayServerType.Auto;
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether to force demo mode instead of loading the application's pages.
|
|
/// </summary>
|
|
public bool ForceDemo { get; set; } = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Display server type options.
|
|
/// </summary>
|
|
public enum DisplayServerType
|
|
{
|
|
/// <summary>
|
|
/// Automatically detect the display server.
|
|
/// </summary>
|
|
Auto,
|
|
|
|
/// <summary>
|
|
/// Use X11 (Xorg).
|
|
/// </summary>
|
|
X11,
|
|
|
|
/// <summary>
|
|
/// Use Wayland.
|
|
/// </summary>
|
|
Wayland
|
|
}
|