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.
535 lines
18 KiB
C#
535 lines
18 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 Microsoft.Maui.Controls;
|
|
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
|
|
{
|
|
private void UpdateAnimations()
|
|
{
|
|
// Update cursor blink for text input controls
|
|
if (_focusedView is SkiaEntry entry)
|
|
{
|
|
entry.UpdateCursorBlink();
|
|
}
|
|
else if (_focusedView is SkiaEditor editor)
|
|
{
|
|
editor.UpdateCursorBlink();
|
|
}
|
|
}
|
|
|
|
private void OnWindowResized(object? sender, (int Width, int Height) size)
|
|
{
|
|
if (_rootView != null)
|
|
{
|
|
// Re-measure with new available size, then arrange
|
|
var availableSize = new Microsoft.Maui.Graphics.Size(size.Width, size.Height);
|
|
_rootView.Measure(availableSize);
|
|
_rootView.Arrange(new Microsoft.Maui.Graphics.Rect(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)
|
|
{
|
|
DiagnosticLog.Debug("LinuxApplication", $"OnPointerPressed at ({e.X}, {e.Y}), Button={e.Button}");
|
|
|
|
// 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);
|
|
DiagnosticLog.Debug("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;
|
|
}
|
|
|
|
DiagnosticLog.Debug("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)
|
|
{
|
|
DiagnosticLog.Debug("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);
|
|
DiagnosticLog.Debug("LinuxApplication", $"HitView: {hitView?.GetType().Name ?? "null"}");
|
|
// Bubble scroll events up to find a ScrollView
|
|
var view = hitView;
|
|
while (view != null)
|
|
{
|
|
DiagnosticLog.Debug("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)
|
|
{
|
|
DiagnosticLog.Debug("LinuxApplication", ">>> 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);
|
|
DiagnosticLog.Debug("LinuxApplication", "Drawing rootView...");
|
|
_rootView.Draw(surface.Canvas);
|
|
DiagnosticLog.Debug("LinuxApplication", "Drawing dialogs...");
|
|
var bounds = new SKRect(0, 0, surface.Width, surface.Height);
|
|
LinuxDialogService.DrawDialogs(surface.Canvas, bounds);
|
|
DiagnosticLog.Debug("LinuxApplication", "<<< 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})";
|
|
DiagnosticLog.Debug("LinuxApplication", $"GTK PointerPressed at ({e.X:F1}, {e.Y:F1}), Button={e.Button} ({buttonName})");
|
|
|
|
// Route to dialog if one is active
|
|
if (LinuxDialogService.HasActiveDialog)
|
|
{
|
|
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.TopDialog?.OnPointerPressed(args);
|
|
_gtkWindow?.RequestRedraw();
|
|
return;
|
|
}
|
|
|
|
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)
|
|
{
|
|
DiagnosticLog.Warn("LinuxApplication", "GTK _rootView is null!");
|
|
return;
|
|
}
|
|
|
|
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
|
|
DiagnosticLog.Debug("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);
|
|
DiagnosticLog.Debug("LinuxApplication", ">>> Before OnPointerPressed");
|
|
hitView.OnPointerPressed(args);
|
|
DiagnosticLog.Debug("LinuxApplication", "<<< After OnPointerPressed, calling RequestRedraw");
|
|
_gtkWindow?.RequestRedraw();
|
|
DiagnosticLog.Debug("LinuxApplication", "<<< After RequestRedraw, returning from handler");
|
|
}
|
|
}
|
|
|
|
private void OnGtkPointerReleased(object? sender, (double X, double Y, int Button) e)
|
|
{
|
|
DiagnosticLog.Debug("LinuxApplication", ">>> OnGtkPointerReleased ENTER");
|
|
|
|
// Route to dialog if one is active
|
|
if (LinuxDialogService.HasActiveDialog)
|
|
{
|
|
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.TopDialog?.OnPointerReleased(args);
|
|
_gtkWindow?.RequestRedraw();
|
|
return;
|
|
}
|
|
|
|
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);
|
|
DiagnosticLog.Debug("LinuxApplication", $"Calling OnPointerReleased on {_capturedView.GetType().Name}");
|
|
_capturedView.OnPointerReleased(args);
|
|
DiagnosticLog.Debug("LinuxApplication", "OnPointerReleased returned");
|
|
_capturedView = null;
|
|
_gtkWindow?.RequestRedraw();
|
|
DiagnosticLog.Debug("LinuxApplication", "<<< 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)
|
|
{
|
|
// Route to dialog if one is active
|
|
if (LinuxDialogService.HasActiveDialog)
|
|
{
|
|
var args = new PointerEventArgs((float)e.X, (float)e.Y);
|
|
LinuxDialogService.TopDialog?.OnPointerMoved(args);
|
|
_gtkWindow?.RequestRedraw();
|
|
return;
|
|
}
|
|
|
|
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)
|
|
{
|
|
var key = ConvertGdkKey(e.KeyVal);
|
|
var modifiers = ConvertGdkModifiers(e.State);
|
|
var args = new KeyEventArgs(key, modifiers);
|
|
|
|
// Route to dialog if one is active
|
|
if (LinuxDialogService.HasActiveDialog)
|
|
{
|
|
LinuxDialogService.TopDialog?.OnKeyDown(args);
|
|
_gtkWindow?.RequestRedraw();
|
|
return;
|
|
}
|
|
|
|
if (_focusedView != null)
|
|
{
|
|
_focusedView.OnKeyDown(args);
|
|
_gtkWindow?.RequestRedraw();
|
|
}
|
|
}
|
|
|
|
private void OnGtkKeyReleased(object? sender, (uint KeyVal, uint KeyCode, uint State) e)
|
|
{
|
|
var key = ConvertGdkKey(e.KeyVal);
|
|
var modifiers = ConvertGdkModifiers(e.State);
|
|
var args = new KeyEventArgs(key, modifiers);
|
|
|
|
// Route to dialog if one is active
|
|
if (LinuxDialogService.HasActiveDialog)
|
|
{
|
|
LinuxDialogService.TopDialog?.OnKeyUp(args);
|
|
_gtkWindow?.RequestRedraw();
|
|
return;
|
|
}
|
|
|
|
if (_focusedView != null)
|
|
{
|
|
_focusedView.OnKeyUp(args);
|
|
_gtkWindow?.RequestRedraw();
|
|
}
|
|
}
|
|
|
|
private void OnGtkScrolled(object? sender, (double X, double Y, double DeltaX, double DeltaY, uint State) e)
|
|
{
|
|
if (_rootView == null) return;
|
|
|
|
// Convert GDK state to KeyModifiers
|
|
var modifiers = ConvertGdkStateToModifiers(e.State);
|
|
bool isCtrlPressed = (modifiers & KeyModifiers.Control) != 0;
|
|
|
|
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
|
|
|
|
// Check for pinch gesture (Ctrl+Scroll) first
|
|
if (isCtrlPressed && hitView?.MauiView != null)
|
|
{
|
|
if (Handlers.GestureManager.ProcessScrollAsPinch(hitView.MauiView, e.X, e.Y, e.DeltaY, true))
|
|
{
|
|
_gtkWindow?.RequestRedraw();
|
|
return;
|
|
}
|
|
}
|
|
|
|
while (hitView != null)
|
|
{
|
|
if (hitView is SkiaScrollView scrollView)
|
|
{
|
|
var args = new ScrollEventArgs((float)e.X, (float)e.Y, (float)e.DeltaX, (float)e.DeltaY, modifiers);
|
|
scrollView.OnScroll(args);
|
|
_gtkWindow?.RequestRedraw();
|
|
break;
|
|
}
|
|
hitView = hitView.Parent;
|
|
}
|
|
}
|
|
|
|
private static KeyModifiers ConvertGdkStateToModifiers(uint state)
|
|
{
|
|
var modifiers = KeyModifiers.None;
|
|
// GDK modifier masks
|
|
const uint GDK_SHIFT_MASK = 1 << 0;
|
|
const uint GDK_CONTROL_MASK = 1 << 2;
|
|
const uint GDK_MOD1_MASK = 1 << 3; // Alt
|
|
const uint GDK_SUPER_MASK = 1 << 26;
|
|
const uint GDK_LOCK_MASK = 1 << 1; // Caps Lock
|
|
|
|
if ((state & GDK_SHIFT_MASK) != 0) modifiers |= KeyModifiers.Shift;
|
|
if ((state & GDK_CONTROL_MASK) != 0) modifiers |= KeyModifiers.Control;
|
|
if ((state & GDK_MOD1_MASK) != 0) modifiers |= KeyModifiers.Alt;
|
|
if ((state & GDK_SUPER_MASK) != 0) modifiers |= KeyModifiers.Super;
|
|
if ((state & GDK_LOCK_MASK) != 0) modifiers |= KeyModifiers.CapsLock;
|
|
|
|
return modifiers;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|