Files
maui-linux/LinuxApplication.Input.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

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