2
0
Files
maui-linux/LinuxApplication.cs
Dave Friedel f6eadaad57 Verify Views files against decompiled, extract embedded types
Fixed files:
- SkiaImageButton.cs: Added SVG support with multi-path search
- SkiaNavigationPage.cs: Added LinuxApplication.IsGtkMode check
- SkiaRefreshView.cs: Added ICommand support (Command, CommandParameter)
- SkiaTemplatedView.cs: Added missing using statements

Extracted embedded types to separate files (matching decompiled pattern):
- From SkiaMenuBar.cs: MenuBarItem, MenuItem, SkiaMenuFlyout, MenuItemClickedEventArgs
- From SkiaNavigationPage.cs: NavigationEventArgs
- From SkiaTabbedPage.cs: TabItem
- From SkiaVisualStateManager.cs: SkiaVisualStateGroupList, SkiaVisualStateGroup, SkiaVisualState, SkiaVisualStateSetter
- From SkiaSwipeView.cs: SwipeItem, SwipeStartedEventArgs, SwipeEndedEventArgs
- From SkiaFlyoutPage.cs: FlyoutLayoutBehavior (already separate)
- From SkiaIndicatorView.cs: IndicatorShape (already separate)
- From SkiaBorder.cs: SkiaFrame
- From SkiaCarouselView.cs: PositionChangedEventArgs
- From SkiaCollectionView.cs: SkiaSelectionMode, ItemsLayoutOrientation
- From SkiaContentPresenter.cs: LayoutAlignment

Verified matching decompiled:
- SkiaContextMenu.cs, SkiaFlexLayout.cs, SkiaGraphicsView.cs

Build: 0 errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 17:02:39 -05:00

980 lines
31 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;
/// <summary>
/// Main Linux application class that bootstraps the MAUI application.
/// </summary>
public class LinuxApplication : IDisposable
{
private static int _invalidateCount;
private static int _requestRedrawCount;
private static int _drawCount;
private static int _gtkThreadId;
private static DateTime _lastCounterReset = DateTime.Now;
private static bool _isRedrawing;
private static int _loopCounter = 0;
private X11Window? _mainWindow;
private GtkHostWindow? _gtkWindow;
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;
private bool _useGtk;
/// <summary>
/// Gets the current application instance.
/// </summary>
public static LinuxApplication? Current { get; private set; }
/// <summary>
/// Gets whether the application is running in GTK mode.
/// </summary>
public static bool IsGtkMode => Current?._useGtk ?? false;
/// <summary>
/// Logs an invalidate call for diagnostics.
/// </summary>
public static void LogInvalidate(string source)
{
int currentThread = Environment.CurrentManagedThreadId;
Interlocked.Increment(ref _invalidateCount);
if (currentThread != _gtkThreadId && _gtkThreadId != 0)
{
Console.WriteLine($"[DIAG] ⚠️ Invalidate from WRONG THREAD! GTK={_gtkThreadId}, Current={currentThread}, Source={source}");
}
}
/// <summary>
/// Logs a request redraw call for diagnostics.
/// </summary>
public static void LogRequestRedraw()
{
int currentThread = Environment.CurrentManagedThreadId;
Interlocked.Increment(ref _requestRedrawCount);
if (currentThread != _gtkThreadId && _gtkThreadId != 0)
{
Console.WriteLine($"[DIAG] ⚠️ RequestRedraw from WRONG THREAD! GTK={_gtkThreadId}, Current={currentThread}");
}
}
private static void StartHeartbeat()
{
_gtkThreadId = Environment.CurrentManagedThreadId;
Console.WriteLine($"[DIAG] GTK thread ID: {_gtkThreadId}");
GLibNative.TimeoutAdd(250, () =>
{
DateTime now = DateTime.Now;
if ((now - _lastCounterReset).TotalSeconds >= 1.0)
{
int invalidates = Interlocked.Exchange(ref _invalidateCount, 0);
int redraws = Interlocked.Exchange(ref _requestRedrawCount, 0);
int draws = Interlocked.Exchange(ref _drawCount, 0);
Console.WriteLine($"[DIAG] ❤️ Heartbeat | Invalidate={invalidates}/s, RequestRedraw={redraws}/s, Draw={draws}/s");
_lastCounterReset = now;
}
return true;
});
}
/// <summary>
/// Logs a draw call for diagnostics.
/// </summary>
public static void LogDraw()
{
Interlocked.Increment(ref _drawCount);
}
/// <summary>
/// Requests a redraw of the application.
/// </summary>
public static void RequestRedraw()
{
LogRequestRedraw();
if (_isRedrawing)
return;
_isRedrawing = true;
try
{
if (Current != null && Current._useGtk)
{
Current._gtkWindow?.RequestRedraw();
}
else
{
Current?._renderingEngine?.InvalidateAll();
}
}
finally
{
_isRedrawing = false;
}
}
/// <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)
{
// Initialize dispatcher
LinuxDispatcher.Initialize();
DispatcherProvider.SetCurrent(LinuxDispatcherProvider.Instance);
Console.WriteLine("[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);
}
// Handle theme changes
((BindableObject)mauiApplication).PropertyChanged += (s, e) =>
{
if (e.PropertyName == "UserAppTheme")
{
Console.WriteLine($"[LinuxApplication] Theme changed to: {mauiApplication.UserAppTheme}");
LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme();
linuxApp._renderingEngine?.InvalidateAll();
}
};
if (mauiApplication.MainPage != null)
{
var 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;
}
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>
/// Initializes the application with the specified options.
/// </summary>
public void Initialize(LinuxApplicationOptions options)
{
_useGtk = options.UseGtk;
if (_useGtk)
{
InitializeGtk(options);
}
else
{
InitializeX11(options);
}
RegisterServices();
}
private void InitializeX11(LinuxApplicationOptions options)
{
_mainWindow = new X11Window(
options.Title ?? "MAUI Application",
options.Width,
options.Height);
// Set up WebView main window
SkiaWebView.SetMainWindow(_mainWindow.Display, _mainWindow.Handle);
// Set window icon
string? iconPath = ResolveIconPath(options.IconPath);
if (!string.IsNullOrEmpty(iconPath))
{
_mainWindow.SetIcon(iconPath);
}
_renderingEngine = new SkiaRenderingEngine(_mainWindow);
_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;
}
private void InitializeGtk(LinuxApplicationOptions options)
{
_gtkWindow = GtkHostService.Instance.GetOrCreateHostWindow(
options.Title ?? "MAUI Application",
options.Width,
options.Height);
string? iconPath = ResolveIconPath(options.IconPath);
if (!string.IsNullOrEmpty(iconPath))
{
GtkHostService.Instance.SetWindowIcon(iconPath);
}
if (_gtkWindow.SkiaSurface != null)
{
_gtkWindow.SkiaSurface.DrawRequested += OnGtkDrawRequested;
_gtkWindow.SkiaSurface.PointerPressed += OnGtkPointerPressed;
_gtkWindow.SkiaSurface.PointerReleased += OnGtkPointerReleased;
_gtkWindow.SkiaSurface.PointerMoved += OnGtkPointerMoved;
_gtkWindow.SkiaSurface.KeyPressed += OnGtkKeyPressed;
_gtkWindow.SkiaSurface.KeyReleased += OnGtkKeyReleased;
_gtkWindow.SkiaSurface.Scrolled += OnGtkScrolled;
_gtkWindow.SkiaSurface.TextInput += OnGtkTextInput;
}
_gtkWindow.Resized += OnGtkResized;
}
private static string? ResolveIconPath(string? explicitPath)
{
if (!string.IsNullOrEmpty(explicitPath))
{
if (Path.IsPathRooted(explicitPath))
{
return File.Exists(explicitPath) ? explicitPath : null;
}
string resolved = Path.Combine(AppContext.BaseDirectory, explicitPath);
return File.Exists(resolved) ? resolved : null;
}
string baseDir = AppContext.BaseDirectory;
// Check for appicon.meta (generated icon)
string metaPath = Path.Combine(baseDir, "appicon.meta");
if (File.Exists(metaPath))
{
string? generated = MauiIconGenerator.GenerateIcon(metaPath);
if (!string.IsNullOrEmpty(generated) && File.Exists(generated))
{
return generated;
}
}
// Check for appicon.png
string pngPath = Path.Combine(baseDir, "appicon.png");
if (File.Exists(pngPath)) return pngPath;
// Check for appicon.svg
string svgPath = Path.Combine(baseDir, "appicon.svg");
if (File.Exists(svgPath)) return svgPath;
return null;
}
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 (_useGtk)
{
RunGtk();
}
else
{
RunX11();
}
}
private void RunX11()
{
if (_mainWindow == null)
throw new InvalidOperationException("Application not initialized");
_mainWindow.Show();
Render();
Console.WriteLine("[LinuxApplication] Starting event loop");
while (_mainWindow.IsRunning)
{
_loopCounter++;
if (_loopCounter % 1000 == 0)
{
Console.WriteLine($"[LinuxApplication] Loop iteration {_loopCounter}");
}
_mainWindow.ProcessEvents();
SkiaWebView.ProcessGtkEvents();
UpdateAnimations();
Render();
Thread.Sleep(1);
}
Console.WriteLine("[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 SKSize(width, height));
_rootView.Arrange(new SKRect(0, 0, width, height));
}
}
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 context menu if one is active
if (LinuxDialogService.HasContextMenu)
{
LinuxDialogService.ActiveContextMenu?.OnPointerMoved(e);
return;
}
// 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);
// Update cursor based on view's cursor type
CursorType cursor = hitView?.CursorType ?? CursorType.Arrow;
_mainWindow?.SetCursor(cursor);
}
hitView?.OnPointerMoved(e);
}
}
private void OnPointerPressed(object? sender, PointerEventArgs e)
{
Console.WriteLine($"[LinuxApplication] OnPointerPressed at ({e.X}, {e.Y})");
// Route to context menu if one is active
if (LinuxDialogService.HasContextMenu)
{
LinuxDialogService.ActiveContextMenu?.OnPointerPressed(e);
return;
}
// 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();
}
// GTK Event Handlers
private void OnGtkDrawRequested(object? sender, EventArgs e)
{
Console.WriteLine("[DIAG] >>> OnGtkDrawRequested ENTER");
LogDraw();
var surface = _gtkWindow?.SkiaSurface;
if (surface?.Canvas != null && _rootView != null)
{
var bgColor = Application.Current?.UserAppTheme == AppTheme.Dark
? new SKColor(32, 33, 36)
: SKColors.White;
surface.Canvas.Clear(bgColor);
Console.WriteLine("[DIAG] Drawing rootView...");
_rootView.Draw(surface.Canvas);
Console.WriteLine("[DIAG] Drawing dialogs...");
var bounds = new SKRect(0, 0, surface.Width, surface.Height);
LinuxDialogService.DrawDialogs(surface.Canvas, bounds);
Console.WriteLine("[DIAG] <<< OnGtkDrawRequested EXIT");
}
}
private void OnGtkResized(object? sender, (int Width, int Height) size)
{
PerformGtkLayout(size.Width, size.Height);
_gtkWindow?.RequestRedraw();
}
private void OnGtkPointerPressed(object? sender, (double X, double Y, int Button) e)
{
string buttonName = e.Button == 1 ? "Left" : e.Button == 2 ? "Middle" : e.Button == 3 ? "Right" : $"Unknown({e.Button})";
Console.WriteLine($"[LinuxApplication.GTK] PointerPressed at ({e.X:F1}, {e.Y:F1}), Button={e.Button} ({buttonName})");
if (LinuxDialogService.HasContextMenu)
{
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
LinuxDialogService.ActiveContextMenu?.OnPointerPressed(args);
_gtkWindow?.RequestRedraw();
return;
}
if (_rootView == null)
{
Console.WriteLine("[LinuxApplication.GTK] _rootView is null!");
return;
}
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
Console.WriteLine($"[LinuxApplication.GTK] HitView: {hitView?.GetType().Name ?? "null"}");
if (hitView != null)
{
if (hitView.IsFocusable && _focusedView != hitView)
{
_focusedView?.OnFocusLost();
_focusedView = hitView;
_focusedView.OnFocusGained();
}
_capturedView = hitView;
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
Console.WriteLine("[DIAG] >>> Before OnPointerPressed");
hitView.OnPointerPressed(args);
Console.WriteLine("[DIAG] <<< After OnPointerPressed, calling RequestRedraw");
_gtkWindow?.RequestRedraw();
Console.WriteLine("[DIAG] <<< After RequestRedraw, returning from handler");
}
}
private void OnGtkPointerReleased(object? sender, (double X, double Y, int Button) e)
{
Console.WriteLine("[DIAG] >>> OnGtkPointerReleased ENTER");
if (_rootView == null) return;
if (_capturedView != null)
{
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
Console.WriteLine($"[DIAG] Calling OnPointerReleased on {_capturedView.GetType().Name}");
_capturedView.OnPointerReleased(args);
Console.WriteLine("[DIAG] OnPointerReleased returned");
_capturedView = null;
_gtkWindow?.RequestRedraw();
Console.WriteLine("[DIAG] <<< OnGtkPointerReleased EXIT (captured path)");
}
else
{
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
if (hitView != null)
{
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
hitView.OnPointerReleased(args);
_gtkWindow?.RequestRedraw();
}
}
}
private void OnGtkPointerMoved(object? sender, (double X, double Y) e)
{
if (LinuxDialogService.HasContextMenu)
{
var args = new PointerEventArgs((float)e.X, (float)e.Y);
LinuxDialogService.ActiveContextMenu?.OnPointerMoved(args);
_gtkWindow?.RequestRedraw();
return;
}
if (_rootView == null) return;
if (_capturedView != null)
{
var args = new PointerEventArgs((float)e.X, (float)e.Y);
_capturedView.OnPointerMoved(args);
_gtkWindow?.RequestRedraw();
return;
}
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
if (hitView != _hoveredView)
{
var args = new PointerEventArgs((float)e.X, (float)e.Y);
_hoveredView?.OnPointerExited(args);
_hoveredView = hitView;
_hoveredView?.OnPointerEntered(args);
_gtkWindow?.RequestRedraw();
}
if (hitView != null)
{
var args = new PointerEventArgs((float)e.X, (float)e.Y);
hitView.OnPointerMoved(args);
}
}
private void OnGtkKeyPressed(object? sender, (uint KeyVal, uint KeyCode, uint State) e)
{
if (_focusedView != null)
{
var key = ConvertGdkKey(e.KeyVal);
var modifiers = ConvertGdkModifiers(e.State);
var args = new KeyEventArgs(key, modifiers);
_focusedView.OnKeyDown(args);
_gtkWindow?.RequestRedraw();
}
}
private void OnGtkKeyReleased(object? sender, (uint KeyVal, uint KeyCode, uint State) e)
{
if (_focusedView != null)
{
var key = ConvertGdkKey(e.KeyVal);
var modifiers = ConvertGdkModifiers(e.State);
var args = new KeyEventArgs(key, modifiers);
_focusedView.OnKeyUp(args);
_gtkWindow?.RequestRedraw();
}
}
private void OnGtkScrolled(object? sender, (double X, double Y, double DeltaX, double DeltaY) e)
{
if (_rootView == null) return;
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
while (hitView != null)
{
if (hitView is SkiaScrollView scrollView)
{
var args = new ScrollEventArgs((float)e.X, (float)e.Y, (float)e.DeltaX, (float)e.DeltaY);
scrollView.OnScroll(args);
_gtkWindow?.RequestRedraw();
break;
}
hitView = hitView.Parent;
}
}
private void OnGtkTextInput(object? sender, string text)
{
if (_focusedView != null)
{
var args = new TextInputEventArgs(text);
_focusedView.OnTextInput(args);
_gtkWindow?.RequestRedraw();
}
}
private static Key ConvertGdkKey(uint keyval)
{
return keyval switch
{
65288 => Key.Backspace,
65289 => Key.Tab,
65293 => Key.Enter,
65307 => Key.Escape,
65360 => Key.Home,
65361 => Key.Left,
65362 => Key.Up,
65363 => Key.Right,
65364 => Key.Down,
65365 => Key.PageUp,
65366 => Key.PageDown,
65367 => Key.End,
65535 => Key.Delete,
>= 32 and <= 126 => (Key)keyval,
_ => Key.Unknown
};
}
private static KeyModifiers ConvertGdkModifiers(uint state)
{
var modifiers = KeyModifiers.None;
if ((state & 1) != 0) modifiers |= KeyModifiers.Shift;
if ((state & 4) != 0) modifiers |= KeyModifiers.Control;
if ((state & 8) != 0) modifiers |= KeyModifiers.Alt;
return modifiers;
}
public void Dispose()
{
if (!_disposed)
{
_renderingEngine?.Dispose();
_mainWindow?.Dispose();
if (Current == this)
Current = null;
_disposed = true;
}
}
}