Major production merge: GTK support, context menus, and dispatcher fixes
Core Infrastructure: - Add Dispatching folder with LinuxDispatcher, LinuxDispatcherProvider, LinuxDispatcherTimer - Add Native folder with P/Invoke wrappers (GTK, GLib, GDK, Cairo, WebKit) - Add GTK host window system with GtkHostWindow and GtkSkiaSurfaceWidget - Update LinuxApplication with GTK mode, theme handling, and icon support - Fix duplicate LinuxDispatcher in LinuxMauiContext Handlers: - Add GtkWebViewManager and GtkWebViewPlatformView for GTK WebView - Add FlexLayoutHandler and GestureManager - Update multiple handlers with ToViewHandler fix and missing mappers - Add MauiHandlerExtensions with ToViewHandler extension method Views: - Add SkiaContextMenu with hover, keyboard, and dark theme support - Add LinuxDialogService with context menu management - Add SkiaFlexLayout for flex container support - Update SkiaShell with RefreshTheme, MauiShell, ContentRenderer - Update SkiaWebView with SetMainWindow, ProcessGtkEvents - Update SkiaImage with LoadFromBitmap method Services: - Add AppInfoService, ConnectivityService, DeviceDisplayService, DeviceInfoService - Add GtkHostService, GtkContextMenuService, MauiIconGenerator Window: - Add CursorType enum and GtkHostWindow - Update X11Window with SetIcon, SetCursor methods Build: SUCCESS (0 errors) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
101
Views/LinuxDialogService.cs
Normal file
101
Views/LinuxDialogService.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
public static class LinuxDialogService
|
||||
{
|
||||
private static readonly List<SkiaAlertDialog> _activeDialogs = new List<SkiaAlertDialog>();
|
||||
|
||||
private static Action? _invalidateCallback;
|
||||
|
||||
private static SkiaContextMenu? _activeContextMenu;
|
||||
|
||||
private static Action? _showPopupCallback;
|
||||
|
||||
private static Action? _hidePopupCallback;
|
||||
|
||||
public static bool HasActiveDialog => _activeDialogs.Count > 0;
|
||||
|
||||
public static SkiaAlertDialog? TopDialog
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_activeDialogs.Count <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return _activeDialogs[_activeDialogs.Count - 1];
|
||||
}
|
||||
}
|
||||
|
||||
public static SkiaContextMenu? ActiveContextMenu => _activeContextMenu;
|
||||
|
||||
public static bool HasContextMenu => _activeContextMenu != null;
|
||||
|
||||
public static void SetInvalidateCallback(Action callback)
|
||||
{
|
||||
_invalidateCallback = callback;
|
||||
}
|
||||
|
||||
public static Task<bool> ShowAlertAsync(string title, string message, string? accept, string? cancel)
|
||||
{
|
||||
var dialog = new SkiaAlertDialog(title, message, accept, cancel);
|
||||
_activeDialogs.Add(dialog);
|
||||
_invalidateCallback?.Invoke();
|
||||
return dialog.Result;
|
||||
}
|
||||
|
||||
internal static void HideDialog(SkiaAlertDialog dialog)
|
||||
{
|
||||
_activeDialogs.Remove(dialog);
|
||||
_invalidateCallback?.Invoke();
|
||||
}
|
||||
|
||||
public static void DrawDialogs(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
DrawDialogsOnly(canvas, bounds);
|
||||
DrawContextMenuOnly(canvas, bounds);
|
||||
}
|
||||
|
||||
public static void DrawDialogsOnly(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
foreach (var dialog in _activeDialogs)
|
||||
{
|
||||
dialog.Measure(new SKSize(bounds.Width, bounds.Height));
|
||||
dialog.Arrange(bounds);
|
||||
dialog.Draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawContextMenuOnly(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
if (_activeContextMenu != null)
|
||||
{
|
||||
_activeContextMenu.Draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetPopupCallbacks(Action showPopup, Action hidePopup)
|
||||
{
|
||||
_showPopupCallback = showPopup;
|
||||
_hidePopupCallback = hidePopup;
|
||||
}
|
||||
|
||||
public static void ShowContextMenu(SkiaContextMenu menu)
|
||||
{
|
||||
Console.WriteLine("[LinuxDialogService] ShowContextMenu called");
|
||||
_activeContextMenu = menu;
|
||||
_showPopupCallback?.Invoke();
|
||||
_invalidateCallback?.Invoke();
|
||||
}
|
||||
|
||||
public static void HideContextMenu()
|
||||
{
|
||||
_activeContextMenu = null;
|
||||
_hidePopupCallback?.Invoke();
|
||||
_invalidateCallback?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -323,63 +323,3 @@ public class SkiaAlertDialog : SkiaView
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for showing modal dialogs in OpenMaui Linux.
|
||||
/// </summary>
|
||||
public static class LinuxDialogService
|
||||
{
|
||||
private static readonly List<SkiaAlertDialog> _activeDialogs = new();
|
||||
private static Action? _invalidateCallback;
|
||||
|
||||
/// <summary>
|
||||
/// Registers the invalidation callback (called by LinuxApplication).
|
||||
/// </summary>
|
||||
public static void SetInvalidateCallback(Action callback)
|
||||
{
|
||||
_invalidateCallback = callback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows an alert dialog and returns when dismissed.
|
||||
/// </summary>
|
||||
public static Task<bool> ShowAlertAsync(string title, string message, string? accept, string? cancel)
|
||||
{
|
||||
var dialog = new SkiaAlertDialog(title, message, accept, cancel);
|
||||
_activeDialogs.Add(dialog);
|
||||
_invalidateCallback?.Invoke();
|
||||
return dialog.Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides a dialog.
|
||||
/// </summary>
|
||||
internal static void HideDialog(SkiaAlertDialog dialog)
|
||||
{
|
||||
_activeDialogs.Remove(dialog);
|
||||
_invalidateCallback?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether there are active dialogs.
|
||||
/// </summary>
|
||||
public static bool HasActiveDialog => _activeDialogs.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the topmost dialog.
|
||||
/// </summary>
|
||||
public static SkiaAlertDialog? TopDialog => _activeDialogs.Count > 0 ? _activeDialogs[^1] : null;
|
||||
|
||||
/// <summary>
|
||||
/// Draws all active dialogs.
|
||||
/// </summary>
|
||||
public static void DrawDialogs(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
foreach (var dialog in _activeDialogs)
|
||||
{
|
||||
dialog.Measure(new SKSize(bounds.Width, bounds.Height));
|
||||
dialog.Arrange(bounds);
|
||||
dialog.Draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +199,39 @@ public class SkiaButton : SkiaView
|
||||
typeof(SkiaButton),
|
||||
null);
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for ImageSource.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty ImageSourceProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(ImageSource),
|
||||
typeof(SKBitmap),
|
||||
typeof(SkiaButton),
|
||||
null,
|
||||
propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for ImageSpacing.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty ImageSpacingProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(ImageSpacing),
|
||||
typeof(float),
|
||||
typeof(SkiaButton),
|
||||
8f,
|
||||
propertyChanged: (b, o, n) => ((SkiaButton)b).InvalidateMeasure());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for ContentLayoutPosition (0=Left, 1=Top, 2=Right, 3=Bottom).
|
||||
/// </summary>
|
||||
public static readonly BindableProperty ContentLayoutPositionProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(ContentLayoutPosition),
|
||||
typeof(int),
|
||||
typeof(SkiaButton),
|
||||
0,
|
||||
propertyChanged: (b, o, n) => ((SkiaButton)b).InvalidateMeasure());
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
@@ -356,6 +389,33 @@ public class SkiaButton : SkiaView
|
||||
set => SetValue(CommandParameterProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image source for the button.
|
||||
/// </summary>
|
||||
public SKBitmap? ImageSource
|
||||
{
|
||||
get => (SKBitmap?)GetValue(ImageSourceProperty);
|
||||
set => SetValue(ImageSourceProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the spacing between the image and text.
|
||||
/// </summary>
|
||||
public float ImageSpacing
|
||||
{
|
||||
get => (float)GetValue(ImageSpacingProperty);
|
||||
set => SetValue(ImageSpacingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the content layout position (0=Left, 1=Top, 2=Right, 3=Bottom).
|
||||
/// </summary>
|
||||
public int ContentLayoutPosition
|
||||
{
|
||||
get => (int)GetValue(ContentLayoutPositionProperty);
|
||||
set => SetValue(ContentLayoutPositionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the button is currently pressed.
|
||||
/// </summary>
|
||||
@@ -504,53 +564,151 @@ public class SkiaButton : SkiaView
|
||||
canvas.DrawRoundRect(focusRect, focusPaint);
|
||||
}
|
||||
|
||||
// Draw text
|
||||
if (!string.IsNullOrEmpty(Text))
|
||||
// Draw content (text and/or image)
|
||||
DrawContent(canvas, bounds, isTextOnly);
|
||||
}
|
||||
|
||||
private void DrawContent(SKCanvas canvas, SKRect bounds, bool isTextOnly)
|
||||
{
|
||||
var fontStyle = new SKFontStyle(
|
||||
IsBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal,
|
||||
SKFontStyleWidth.Normal,
|
||||
IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright);
|
||||
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle)
|
||||
?? SKTypeface.Default;
|
||||
|
||||
using var font = new SKFont(typeface, FontSize);
|
||||
|
||||
// Determine text color
|
||||
SKColor textColorToUse;
|
||||
if (!IsEnabled)
|
||||
{
|
||||
var fontStyle = new SKFontStyle(
|
||||
IsBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal,
|
||||
SKFontStyleWidth.Normal,
|
||||
IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright);
|
||||
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle)
|
||||
?? SKTypeface.Default;
|
||||
textColorToUse = TextColor.WithAlpha(128);
|
||||
}
|
||||
else if (isTextOnly && (IsHovered || IsPressed))
|
||||
{
|
||||
textColorToUse = new SKColor(
|
||||
(byte)Math.Max(0, TextColor.Red - 40),
|
||||
(byte)Math.Max(0, TextColor.Green - 40),
|
||||
(byte)Math.Max(0, TextColor.Blue - 40),
|
||||
TextColor.Alpha);
|
||||
}
|
||||
else
|
||||
{
|
||||
textColorToUse = TextColor;
|
||||
}
|
||||
|
||||
using var font = new SKFont(typeface, FontSize);
|
||||
using var textPaint = new SKPaint(font)
|
||||
{
|
||||
Color = textColorToUse,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
// For text-only buttons, darken text on hover/press for feedback
|
||||
SKColor textColorToUse;
|
||||
if (!IsEnabled)
|
||||
// Measure text
|
||||
var textBounds = new SKRect();
|
||||
bool hasText = !string.IsNullOrEmpty(Text);
|
||||
if (hasText)
|
||||
{
|
||||
textPaint.MeasureText(Text, ref textBounds);
|
||||
}
|
||||
|
||||
// Calculate image size
|
||||
bool hasImage = ImageSource != null;
|
||||
float imageWidth = 0;
|
||||
float imageHeight = 0;
|
||||
if (hasImage)
|
||||
{
|
||||
float maxImageSize = Math.Min(bounds.Height - 8, 24f);
|
||||
float scale = Math.Min(maxImageSize / ImageSource!.Width, maxImageSize / ImageSource.Height);
|
||||
imageWidth = ImageSource.Width * scale;
|
||||
imageHeight = ImageSource.Height * scale;
|
||||
}
|
||||
|
||||
// Calculate total content size and position
|
||||
bool isHorizontal = ContentLayoutPosition == 0 || ContentLayoutPosition == 2;
|
||||
float totalWidth, totalHeight;
|
||||
if (hasImage && hasText)
|
||||
{
|
||||
if (isHorizontal)
|
||||
{
|
||||
textColorToUse = TextColor.WithAlpha(128);
|
||||
}
|
||||
else if (isTextOnly && (IsHovered || IsPressed))
|
||||
{
|
||||
// Darken the text color slightly for hover/press feedback
|
||||
textColorToUse = new SKColor(
|
||||
(byte)Math.Max(0, TextColor.Red - 40),
|
||||
(byte)Math.Max(0, TextColor.Green - 40),
|
||||
(byte)Math.Max(0, TextColor.Blue - 40),
|
||||
TextColor.Alpha);
|
||||
totalWidth = imageWidth + ImageSpacing + textBounds.Width;
|
||||
totalHeight = Math.Max(imageHeight, textBounds.Height);
|
||||
}
|
||||
else
|
||||
{
|
||||
textColorToUse = TextColor;
|
||||
totalWidth = Math.Max(imageWidth, textBounds.Width);
|
||||
totalHeight = imageHeight + ImageSpacing + textBounds.Height;
|
||||
}
|
||||
}
|
||||
else if (hasImage)
|
||||
{
|
||||
totalWidth = imageWidth;
|
||||
totalHeight = imageHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
totalWidth = textBounds.Width;
|
||||
totalHeight = textBounds.Height;
|
||||
}
|
||||
|
||||
float startX = bounds.MidX - totalWidth / 2;
|
||||
float startY = bounds.MidY - totalHeight / 2;
|
||||
|
||||
// Draw image and text based on layout position
|
||||
if (hasImage)
|
||||
{
|
||||
float imageX, imageY;
|
||||
float textX = 0, textY = 0;
|
||||
|
||||
switch (ContentLayoutPosition)
|
||||
{
|
||||
case 1: // Top - image above text
|
||||
imageX = bounds.MidX - imageWidth / 2;
|
||||
imageY = startY;
|
||||
textX = bounds.MidX - textBounds.Width / 2;
|
||||
textY = startY + imageHeight + ImageSpacing - textBounds.Top;
|
||||
break;
|
||||
case 2: // Right - image to right of text
|
||||
textX = startX;
|
||||
textY = bounds.MidY - textBounds.MidY;
|
||||
imageX = startX + textBounds.Width + ImageSpacing;
|
||||
imageY = bounds.MidY - imageHeight / 2;
|
||||
break;
|
||||
case 3: // Bottom - image below text
|
||||
textX = bounds.MidX - textBounds.Width / 2;
|
||||
textY = startY - textBounds.Top;
|
||||
imageX = bounds.MidX - imageWidth / 2;
|
||||
imageY = startY + textBounds.Height + ImageSpacing;
|
||||
break;
|
||||
default: // 0 = Left - image to left of text
|
||||
imageX = startX;
|
||||
imageY = bounds.MidY - imageHeight / 2;
|
||||
textX = startX + imageWidth + ImageSpacing;
|
||||
textY = bounds.MidY - textBounds.MidY;
|
||||
break;
|
||||
}
|
||||
|
||||
using var paint = new SKPaint(font)
|
||||
// Draw image
|
||||
var imageRect = new SKRect(imageX, imageY, imageX + imageWidth, imageY + imageHeight);
|
||||
using var imagePaint = new SKPaint { IsAntialias = true };
|
||||
if (!IsEnabled)
|
||||
{
|
||||
Color = textColorToUse,
|
||||
IsAntialias = true
|
||||
};
|
||||
imagePaint.ColorFilter = SKColorFilter.CreateBlendMode(new SKColor(128, 128, 128, 128), SKBlendMode.Modulate);
|
||||
}
|
||||
canvas.DrawBitmap(ImageSource!, imageRect, imagePaint);
|
||||
|
||||
// Measure text
|
||||
var textBounds = new SKRect();
|
||||
paint.MeasureText(Text, ref textBounds);
|
||||
|
||||
// Center text
|
||||
// Draw text
|
||||
if (hasText)
|
||||
{
|
||||
canvas.DrawText(Text!, textX, textY, textPaint);
|
||||
}
|
||||
}
|
||||
else if (hasText)
|
||||
{
|
||||
// Just text, centered
|
||||
var x = bounds.MidX - textBounds.MidX;
|
||||
var y = bounds.MidY - textBounds.MidY;
|
||||
|
||||
canvas.DrawText(Text, x, y, paint);
|
||||
canvas.DrawText(Text!, x, y, textPaint);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
221
Views/SkiaContextMenu.cs
Normal file
221
Views/SkiaContextMenu.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
public class SkiaContextMenu : SkiaView
|
||||
{
|
||||
private readonly List<ContextMenuItem> _items;
|
||||
private readonly float _x;
|
||||
private readonly float _y;
|
||||
private int _hoveredIndex = -1;
|
||||
private SKRect[] _itemBounds = Array.Empty<SKRect>();
|
||||
|
||||
private static readonly SKColor MenuBackground = new SKColor(255, 255, 255);
|
||||
private static readonly SKColor MenuBackgroundDark = new SKColor(48, 48, 48);
|
||||
private static readonly SKColor ItemHoverBackground = new SKColor(227, 242, 253);
|
||||
private static readonly SKColor ItemHoverBackgroundDark = new SKColor(80, 80, 80);
|
||||
private static readonly SKColor ItemTextColor = new SKColor(33, 33, 33);
|
||||
private static readonly SKColor ItemTextColorDark = new SKColor(224, 224, 224);
|
||||
private static readonly SKColor DisabledTextColor = new SKColor(158, 158, 158);
|
||||
private static readonly SKColor SeparatorColor = new SKColor(224, 224, 224);
|
||||
private static readonly SKColor ShadowColor = new SKColor(0, 0, 0, 40);
|
||||
|
||||
private const float MenuPadding = 4f;
|
||||
private const float ItemHeight = 32f;
|
||||
private const float ItemPaddingH = 16f;
|
||||
private const float SeparatorHeight = 9f;
|
||||
private const float CornerRadius = 4f;
|
||||
private const float MinWidth = 120f;
|
||||
|
||||
private bool _isDarkTheme;
|
||||
|
||||
public SkiaContextMenu(float x, float y, List<ContextMenuItem> items, bool isDarkTheme = false)
|
||||
{
|
||||
_x = x;
|
||||
_y = y;
|
||||
_items = items;
|
||||
_isDarkTheme = isDarkTheme;
|
||||
IsFocusable = true;
|
||||
}
|
||||
|
||||
public override void Draw(SKCanvas canvas)
|
||||
{
|
||||
float menuWidth = CalculateMenuWidth();
|
||||
float menuHeight = CalculateMenuHeight();
|
||||
float posX = _x;
|
||||
float posY = _y;
|
||||
|
||||
// Adjust position to stay within bounds
|
||||
canvas.GetDeviceClipBounds(out var clipBounds);
|
||||
if (posX + menuWidth > clipBounds.Right)
|
||||
{
|
||||
posX = clipBounds.Right - menuWidth - 4f;
|
||||
}
|
||||
if (posY + menuHeight > clipBounds.Bottom)
|
||||
{
|
||||
posY = clipBounds.Bottom - menuHeight - 4f;
|
||||
}
|
||||
|
||||
var menuRect = new SKRect(posX, posY, posX + menuWidth, posY + menuHeight);
|
||||
|
||||
// Draw shadow
|
||||
using (var shadowPaint = new SKPaint
|
||||
{
|
||||
Color = ShadowColor,
|
||||
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4f)
|
||||
})
|
||||
{
|
||||
canvas.DrawRoundRect(menuRect.Left + 2f, menuRect.Top + 2f, menuWidth, menuHeight, CornerRadius, CornerRadius, shadowPaint);
|
||||
}
|
||||
|
||||
// Draw background
|
||||
using (var bgPaint = new SKPaint
|
||||
{
|
||||
Color = _isDarkTheme ? MenuBackgroundDark : MenuBackground,
|
||||
IsAntialias = true
|
||||
})
|
||||
{
|
||||
canvas.DrawRoundRect(menuRect, CornerRadius, CornerRadius, bgPaint);
|
||||
}
|
||||
|
||||
// Draw border
|
||||
using (var borderPaint = new SKPaint
|
||||
{
|
||||
Color = SeparatorColor,
|
||||
Style = SKPaintStyle.Stroke,
|
||||
StrokeWidth = 1f,
|
||||
IsAntialias = true
|
||||
})
|
||||
{
|
||||
canvas.DrawRoundRect(menuRect, CornerRadius, CornerRadius, borderPaint);
|
||||
}
|
||||
|
||||
// Draw items
|
||||
_itemBounds = new SKRect[_items.Count];
|
||||
float itemY = posY + MenuPadding;
|
||||
|
||||
for (int i = 0; i < _items.Count; i++)
|
||||
{
|
||||
var item = _items[i];
|
||||
|
||||
if (item.IsSeparator)
|
||||
{
|
||||
float separatorY = itemY + SeparatorHeight / 2f;
|
||||
using (var sepPaint = new SKPaint { Color = SeparatorColor, StrokeWidth = 1f })
|
||||
{
|
||||
canvas.DrawLine(posX + 8f, separatorY, posX + menuWidth - 8f, separatorY, sepPaint);
|
||||
}
|
||||
_itemBounds[i] = new SKRect(posX, itemY, posX + menuWidth, itemY + SeparatorHeight);
|
||||
itemY += SeparatorHeight;
|
||||
continue;
|
||||
}
|
||||
|
||||
var itemRect = new SKRect(posX + MenuPadding, itemY, posX + menuWidth - MenuPadding, itemY + ItemHeight);
|
||||
_itemBounds[i] = itemRect;
|
||||
|
||||
// Draw hover background
|
||||
if (i == _hoveredIndex && item.IsEnabled)
|
||||
{
|
||||
using (var hoverPaint = new SKPaint
|
||||
{
|
||||
Color = _isDarkTheme ? ItemHoverBackgroundDark : ItemHoverBackground,
|
||||
IsAntialias = true
|
||||
})
|
||||
{
|
||||
canvas.DrawRoundRect(itemRect, CornerRadius, CornerRadius, hoverPaint);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw text
|
||||
using (var textPaint = new SKPaint
|
||||
{
|
||||
Color = !item.IsEnabled ? DisabledTextColor : (_isDarkTheme ? ItemTextColorDark : ItemTextColor),
|
||||
TextSize = 14f,
|
||||
IsAntialias = true,
|
||||
Typeface = SKTypeface.Default
|
||||
})
|
||||
{
|
||||
float textY = itemRect.MidY + textPaint.TextSize / 3f;
|
||||
canvas.DrawText(item.Text, itemRect.Left + ItemPaddingH, textY, textPaint);
|
||||
}
|
||||
|
||||
itemY += ItemHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private float CalculateMenuWidth()
|
||||
{
|
||||
float maxWidth = MinWidth;
|
||||
using (var paint = new SKPaint { TextSize = 14f, Typeface = SKTypeface.Default })
|
||||
{
|
||||
foreach (var item in _items)
|
||||
{
|
||||
if (!item.IsSeparator)
|
||||
{
|
||||
float textWidth = paint.MeasureText(item.Text) + ItemPaddingH * 2f;
|
||||
maxWidth = Math.Max(maxWidth, textWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxWidth + MenuPadding * 2f;
|
||||
}
|
||||
|
||||
private float CalculateMenuHeight()
|
||||
{
|
||||
float height = MenuPadding * 2f;
|
||||
foreach (var item in _items)
|
||||
{
|
||||
height += item.IsSeparator ? SeparatorHeight : ItemHeight;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
public override void OnPointerMoved(PointerEventArgs e)
|
||||
{
|
||||
int oldHovered = _hoveredIndex;
|
||||
_hoveredIndex = -1;
|
||||
|
||||
for (int i = 0; i < _itemBounds.Length; i++)
|
||||
{
|
||||
if (_itemBounds[i].Contains(e.X, e.Y) && !_items[i].IsSeparator)
|
||||
{
|
||||
_hoveredIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldHovered != _hoveredIndex)
|
||||
{
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnPointerPressed(PointerEventArgs e)
|
||||
{
|
||||
for (int i = 0; i < _itemBounds.Length; i++)
|
||||
{
|
||||
if (_itemBounds[i].Contains(e.X, e.Y))
|
||||
{
|
||||
var item = _items[i];
|
||||
if (item.IsEnabled && !item.IsSeparator && item.Action != null)
|
||||
{
|
||||
LinuxDialogService.HideContextMenu();
|
||||
item.Action();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
LinuxDialogService.HideContextMenu();
|
||||
}
|
||||
|
||||
public override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
LinuxDialogService.HideContextMenu();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using SkiaSharp;
|
||||
using Microsoft.Maui.Platform.Linux.Rendering;
|
||||
using Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
@@ -980,11 +982,17 @@ public class SkiaEntry : SkiaView
|
||||
|
||||
public override void OnPointerPressed(PointerEventArgs e)
|
||||
{
|
||||
Console.WriteLine($"[SkiaEntry] OnPointerPressed - Text='{Text}', Placeholder='{Placeholder}', IsEnabled={IsEnabled}, IsFocused={IsFocused}");
|
||||
Console.WriteLine($"[SkiaEntry] Bounds={Bounds}, ScreenBounds={ScreenBounds}, e.X={e.X}, e.Y={e.Y}");
|
||||
|
||||
Console.WriteLine($"[SkiaEntry] OnPointerPressed Button={e.Button} at ({e.X}, {e.Y})");
|
||||
if (!IsEnabled) return;
|
||||
|
||||
// Handle right-click context menu
|
||||
if (e.Button == PointerButton.Right)
|
||||
{
|
||||
Console.WriteLine("[SkiaEntry] Right-click detected, showing context menu");
|
||||
ShowContextMenu(e.X, e.Y);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if clicked on clear button
|
||||
if (ShowClearButton && !string.IsNullOrEmpty(Text) && IsFocused)
|
||||
{
|
||||
@@ -1217,6 +1225,38 @@ public class SkiaEntry : SkiaView
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void ShowContextMenu(float x, float y)
|
||||
{
|
||||
Console.WriteLine($"[SkiaEntry] ShowContextMenu at ({x}, {y})");
|
||||
bool hasSelection = _selectionLength != 0;
|
||||
bool hasText = !string.IsNullOrEmpty(Text);
|
||||
bool hasClipboard = !string.IsNullOrEmpty(SystemClipboard.GetText());
|
||||
|
||||
GtkContextMenuService.ShowContextMenu(new List<GtkMenuItem>
|
||||
{
|
||||
new GtkMenuItem("Cut", () =>
|
||||
{
|
||||
CutToClipboard();
|
||||
Invalidate();
|
||||
}, hasSelection),
|
||||
new GtkMenuItem("Copy", () =>
|
||||
{
|
||||
CopyToClipboard();
|
||||
}, hasSelection),
|
||||
new GtkMenuItem("Paste", () =>
|
||||
{
|
||||
PasteFromClipboard();
|
||||
Invalidate();
|
||||
}, hasClipboard),
|
||||
GtkMenuItem.Separator,
|
||||
new GtkMenuItem("Select All", () =>
|
||||
{
|
||||
SelectAll();
|
||||
Invalidate();
|
||||
}, hasText)
|
||||
});
|
||||
}
|
||||
|
||||
public override void OnFocusGained()
|
||||
{
|
||||
base.OnFocusGained();
|
||||
|
||||
256
Views/SkiaFlexLayout.cs
Normal file
256
Views/SkiaFlexLayout.cs
Normal file
@@ -0,0 +1,256 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Maui.Controls;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
|
||||
public class SkiaFlexLayout : SkiaLayoutView
|
||||
{
|
||||
public static readonly BindableProperty DirectionProperty = BindableProperty.Create(
|
||||
nameof(Direction), typeof(FlexDirection), typeof(SkiaFlexLayout), FlexDirection.Row,
|
||||
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaFlexLayout)b).InvalidateMeasure());
|
||||
|
||||
public static readonly BindableProperty WrapProperty = BindableProperty.Create(
|
||||
nameof(Wrap), typeof(FlexWrap), typeof(SkiaFlexLayout), FlexWrap.NoWrap,
|
||||
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaFlexLayout)b).InvalidateMeasure());
|
||||
|
||||
public static readonly BindableProperty JustifyContentProperty = BindableProperty.Create(
|
||||
nameof(JustifyContent), typeof(FlexJustify), typeof(SkiaFlexLayout), FlexJustify.Start,
|
||||
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaFlexLayout)b).InvalidateMeasure());
|
||||
|
||||
public static readonly BindableProperty AlignItemsProperty = BindableProperty.Create(
|
||||
nameof(AlignItems), typeof(FlexAlignItems), typeof(SkiaFlexLayout), FlexAlignItems.Stretch,
|
||||
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaFlexLayout)b).InvalidateMeasure());
|
||||
|
||||
public static readonly BindableProperty AlignContentProperty = BindableProperty.Create(
|
||||
nameof(AlignContent), typeof(FlexAlignContent), typeof(SkiaFlexLayout), FlexAlignContent.Stretch,
|
||||
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaFlexLayout)b).InvalidateMeasure());
|
||||
|
||||
public static readonly BindableProperty OrderProperty = BindableProperty.CreateAttached(
|
||||
"Order", typeof(int), typeof(SkiaFlexLayout), 0, BindingMode.TwoWay);
|
||||
|
||||
public static readonly BindableProperty GrowProperty = BindableProperty.CreateAttached(
|
||||
"Grow", typeof(float), typeof(SkiaFlexLayout), 0f, BindingMode.TwoWay);
|
||||
|
||||
public static readonly BindableProperty ShrinkProperty = BindableProperty.CreateAttached(
|
||||
"Shrink", typeof(float), typeof(SkiaFlexLayout), 1f, BindingMode.TwoWay);
|
||||
|
||||
public static readonly BindableProperty BasisProperty = BindableProperty.CreateAttached(
|
||||
"Basis", typeof(FlexBasis), typeof(SkiaFlexLayout), FlexBasis.Auto, BindingMode.TwoWay);
|
||||
|
||||
public static readonly BindableProperty AlignSelfProperty = BindableProperty.CreateAttached(
|
||||
"AlignSelf", typeof(FlexAlignSelf), typeof(SkiaFlexLayout), FlexAlignSelf.Auto, BindingMode.TwoWay);
|
||||
|
||||
public FlexDirection Direction
|
||||
{
|
||||
get => (FlexDirection)GetValue(DirectionProperty);
|
||||
set => SetValue(DirectionProperty, value);
|
||||
}
|
||||
|
||||
public FlexWrap Wrap
|
||||
{
|
||||
get => (FlexWrap)GetValue(WrapProperty);
|
||||
set => SetValue(WrapProperty, value);
|
||||
}
|
||||
|
||||
public FlexJustify JustifyContent
|
||||
{
|
||||
get => (FlexJustify)GetValue(JustifyContentProperty);
|
||||
set => SetValue(JustifyContentProperty, value);
|
||||
}
|
||||
|
||||
public FlexAlignItems AlignItems
|
||||
{
|
||||
get => (FlexAlignItems)GetValue(AlignItemsProperty);
|
||||
set => SetValue(AlignItemsProperty, value);
|
||||
}
|
||||
|
||||
public FlexAlignContent AlignContent
|
||||
{
|
||||
get => (FlexAlignContent)GetValue(AlignContentProperty);
|
||||
set => SetValue(AlignContentProperty, value);
|
||||
}
|
||||
|
||||
public static int GetOrder(SkiaView view) => (int)view.GetValue(OrderProperty);
|
||||
public static void SetOrder(SkiaView view, int value) => view.SetValue(OrderProperty, value);
|
||||
|
||||
public static float GetGrow(SkiaView view) => (float)view.GetValue(GrowProperty);
|
||||
public static void SetGrow(SkiaView view, float value) => view.SetValue(GrowProperty, value);
|
||||
|
||||
public static float GetShrink(SkiaView view) => (float)view.GetValue(ShrinkProperty);
|
||||
public static void SetShrink(SkiaView view, float value) => view.SetValue(ShrinkProperty, value);
|
||||
|
||||
public static FlexBasis GetBasis(SkiaView view) => (FlexBasis)view.GetValue(BasisProperty);
|
||||
public static void SetBasis(SkiaView view, FlexBasis value) => view.SetValue(BasisProperty, value);
|
||||
|
||||
public static FlexAlignSelf GetAlignSelf(SkiaView view) => (FlexAlignSelf)view.GetValue(AlignSelfProperty);
|
||||
public static void SetAlignSelf(SkiaView view, FlexAlignSelf value) => view.SetValue(AlignSelfProperty, value);
|
||||
|
||||
protected override SKSize MeasureOverride(SKSize availableSize)
|
||||
{
|
||||
bool isRow = Direction == FlexDirection.Row || Direction == FlexDirection.RowReverse;
|
||||
float totalMain = 0f;
|
||||
float maxCross = 0f;
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (!child.IsVisible)
|
||||
continue;
|
||||
|
||||
var childSize = child.Measure(availableSize);
|
||||
if (isRow)
|
||||
{
|
||||
totalMain += childSize.Width;
|
||||
maxCross = Math.Max(maxCross, childSize.Height);
|
||||
}
|
||||
else
|
||||
{
|
||||
totalMain += childSize.Height;
|
||||
maxCross = Math.Max(maxCross, childSize.Width);
|
||||
}
|
||||
}
|
||||
|
||||
return isRow ? new SKSize(totalMain, maxCross) : new SKSize(maxCross, totalMain);
|
||||
}
|
||||
|
||||
protected override SKRect ArrangeOverride(SKRect bounds)
|
||||
{
|
||||
if (Children.Count == 0)
|
||||
return bounds;
|
||||
|
||||
bool isRow = Direction == FlexDirection.Row || Direction == FlexDirection.RowReverse;
|
||||
bool isReverse = Direction == FlexDirection.RowReverse || Direction == FlexDirection.ColumnReverse;
|
||||
|
||||
var orderedChildren = Children.Where(c => c.IsVisible).OrderBy(c => GetOrder(c)).ToList();
|
||||
if (orderedChildren.Count == 0)
|
||||
return bounds;
|
||||
|
||||
float mainSize = isRow ? bounds.Width : bounds.Height;
|
||||
float crossSize = isRow ? bounds.Height : bounds.Width;
|
||||
|
||||
var childInfos = new List<(SkiaView child, SKSize size, float grow, float shrink)>();
|
||||
float totalBasis = 0f;
|
||||
float totalGrow = 0f;
|
||||
float totalShrink = 0f;
|
||||
|
||||
foreach (var child in orderedChildren)
|
||||
{
|
||||
var basis = GetBasis(child);
|
||||
float grow = GetGrow(child);
|
||||
float shrink = GetShrink(child);
|
||||
|
||||
SKSize size;
|
||||
if (basis.IsAuto)
|
||||
{
|
||||
size = child.Measure(new SKSize(bounds.Width, bounds.Height));
|
||||
}
|
||||
else
|
||||
{
|
||||
float length = basis.Length;
|
||||
size = isRow
|
||||
? child.Measure(new SKSize(length, bounds.Height))
|
||||
: child.Measure(new SKSize(bounds.Width, length));
|
||||
}
|
||||
|
||||
childInfos.Add((child, size, grow, shrink));
|
||||
totalBasis += isRow ? size.Width : size.Height;
|
||||
totalGrow += grow;
|
||||
totalShrink += shrink;
|
||||
}
|
||||
|
||||
float freeSpace = mainSize - totalBasis;
|
||||
|
||||
var resolvedSizes = new List<(SkiaView child, float mainSize, float crossSize)>();
|
||||
foreach (var (child, size, grow, shrink) in childInfos)
|
||||
{
|
||||
float childMainSize = isRow ? size.Width : size.Height;
|
||||
float childCrossSize = isRow ? size.Height : size.Width;
|
||||
|
||||
if (freeSpace > 0f && totalGrow > 0f)
|
||||
{
|
||||
childMainSize += freeSpace * (grow / totalGrow);
|
||||
}
|
||||
else if (freeSpace < 0f && totalShrink > 0f)
|
||||
{
|
||||
childMainSize += freeSpace * (shrink / totalShrink);
|
||||
}
|
||||
|
||||
resolvedSizes.Add((child, Math.Max(0f, childMainSize), childCrossSize));
|
||||
}
|
||||
|
||||
float usedSpace = resolvedSizes.Sum(s => s.mainSize);
|
||||
float remainingSpace = Math.Max(0f, mainSize - usedSpace);
|
||||
|
||||
float position = isRow ? bounds.Left : bounds.Top;
|
||||
float spacing = 0f;
|
||||
|
||||
switch (JustifyContent)
|
||||
{
|
||||
case FlexJustify.Center:
|
||||
position += remainingSpace / 2f;
|
||||
break;
|
||||
case FlexJustify.End:
|
||||
position += remainingSpace;
|
||||
break;
|
||||
case FlexJustify.SpaceBetween:
|
||||
if (resolvedSizes.Count > 1)
|
||||
spacing = remainingSpace / (resolvedSizes.Count - 1);
|
||||
break;
|
||||
case FlexJustify.SpaceAround:
|
||||
if (resolvedSizes.Count > 0)
|
||||
{
|
||||
spacing = remainingSpace / resolvedSizes.Count;
|
||||
position += spacing / 2f;
|
||||
}
|
||||
break;
|
||||
case FlexJustify.SpaceEvenly:
|
||||
if (resolvedSizes.Count > 0)
|
||||
{
|
||||
spacing = remainingSpace / (resolvedSizes.Count + 1);
|
||||
position += spacing;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var items = isReverse ? resolvedSizes.AsEnumerable().Reverse() : resolvedSizes;
|
||||
|
||||
foreach (var (child, childMainSize, childCrossSize) in items)
|
||||
{
|
||||
var alignSelf = GetAlignSelf(child);
|
||||
var effectiveAlign = alignSelf == FlexAlignSelf.Auto ? AlignItems : (FlexAlignItems)alignSelf;
|
||||
|
||||
float crossPos = isRow ? bounds.Top : bounds.Left;
|
||||
float finalCrossSize = childCrossSize;
|
||||
|
||||
switch (effectiveAlign)
|
||||
{
|
||||
case FlexAlignItems.End:
|
||||
crossPos = (isRow ? bounds.Bottom : bounds.Right) - finalCrossSize;
|
||||
break;
|
||||
case FlexAlignItems.Center:
|
||||
crossPos += (crossSize - finalCrossSize) / 2f;
|
||||
break;
|
||||
case FlexAlignItems.Stretch:
|
||||
finalCrossSize = crossSize;
|
||||
break;
|
||||
}
|
||||
|
||||
SKRect childBounds;
|
||||
if (isRow)
|
||||
{
|
||||
childBounds = new SKRect(position, crossPos, position + childMainSize, crossPos + finalCrossSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
childBounds = new SKRect(crossPos, position, crossPos + finalCrossSize, position + childMainSize);
|
||||
}
|
||||
|
||||
child.Arrange(childBounds);
|
||||
position += childMainSize + spacing;
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
@@ -210,6 +210,25 @@ public class SkiaImage : SkiaView
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the image from an SKBitmap.
|
||||
/// </summary>
|
||||
public void LoadFromBitmap(SKBitmap bitmap)
|
||||
{
|
||||
try
|
||||
{
|
||||
Bitmap = bitmap;
|
||||
_isLoading = false;
|
||||
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_isLoading = false;
|
||||
ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex));
|
||||
}
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
protected override SKSize MeasureOverride(SKSize availableSize)
|
||||
{
|
||||
if (_image == null)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// 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.Linq;
|
||||
using SkiaSharp;
|
||||
using Microsoft.Maui.Platform.Linux.Rendering;
|
||||
|
||||
@@ -24,6 +27,17 @@ public class SkiaLabel : SkiaView
|
||||
"",
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for FormattedSpans.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty FormattedSpansProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(FormattedSpans),
|
||||
typeof(IList<SkiaTextSpan>),
|
||||
typeof(SkiaLabel),
|
||||
null,
|
||||
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for TextColor.
|
||||
/// </summary>
|
||||
@@ -191,6 +205,15 @@ public class SkiaLabel : SkiaView
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the formatted text spans for rich text rendering.
|
||||
/// </summary>
|
||||
public IList<SkiaTextSpan>? FormattedSpans
|
||||
{
|
||||
get => (IList<SkiaTextSpan>?)GetValue(FormattedSpansProperty);
|
||||
set => SetValue(FormattedSpansProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text color.
|
||||
/// </summary>
|
||||
@@ -363,6 +386,11 @@ public class SkiaLabel : SkiaView
|
||||
|
||||
private static SKTypeface? _cachedTypeface;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when the label is tapped.
|
||||
/// </summary>
|
||||
public event EventHandler? Tapped;
|
||||
|
||||
private void OnTextChanged()
|
||||
{
|
||||
InvalidateMeasure();
|
||||
@@ -400,6 +428,20 @@ public class SkiaLabel : SkiaView
|
||||
|
||||
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
// Calculate content bounds with padding
|
||||
var contentBounds = new SKRect(
|
||||
bounds.Left + Padding.Left,
|
||||
bounds.Top + Padding.Top,
|
||||
bounds.Right - Padding.Right,
|
||||
bounds.Bottom - Padding.Bottom);
|
||||
|
||||
// Handle formatted spans first (rich text)
|
||||
if (FormattedSpans != null && FormattedSpans.Count > 0)
|
||||
{
|
||||
DrawFormattedText(canvas, contentBounds);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Text))
|
||||
return;
|
||||
|
||||
@@ -421,13 +463,6 @@ public class SkiaLabel : SkiaView
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
// Calculate content bounds with padding
|
||||
var contentBounds = new SKRect(
|
||||
bounds.Left + Padding.Left,
|
||||
bounds.Top + Padding.Top,
|
||||
bounds.Right - Padding.Right,
|
||||
bounds.Bottom - Padding.Bottom);
|
||||
|
||||
// Handle single line vs multiline
|
||||
// Use DrawMultiLineWithWrapping when: MaxLines > 1, text has newlines, OR WordWrap is enabled
|
||||
bool needsMultiLine = MaxLines > 1 || Text.Contains('\n') ||
|
||||
@@ -815,6 +850,181 @@ public class SkiaLabel : SkiaView
|
||||
totalHeight + Padding.Top + Padding.Bottom);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFormattedText(SKCanvas canvas, SKRect bounds)
|
||||
{
|
||||
if (FormattedSpans == null || FormattedSpans.Count == 0)
|
||||
return;
|
||||
|
||||
float currentX = bounds.Left;
|
||||
float currentY = bounds.Top;
|
||||
float lineHeight = 0f;
|
||||
|
||||
// First pass: calculate line data
|
||||
var lineSpans = new List<(SkiaTextSpan span, float x, float width, float height, SKPaint paint)>();
|
||||
|
||||
foreach (var span in FormattedSpans)
|
||||
{
|
||||
if (string.IsNullOrEmpty(span.Text))
|
||||
continue;
|
||||
|
||||
var paint = CreateSpanPaint(span);
|
||||
var textBounds = new SKRect();
|
||||
paint.MeasureText(span.Text, ref textBounds);
|
||||
lineHeight = Math.Max(lineHeight, textBounds.Height);
|
||||
|
||||
// Word wrap
|
||||
if (currentX + textBounds.Width > bounds.Right && currentX > bounds.Left)
|
||||
{
|
||||
currentY += lineHeight * LineHeight;
|
||||
currentX = bounds.Left;
|
||||
lineHeight = textBounds.Height;
|
||||
}
|
||||
|
||||
lineSpans.Add((span, currentX, textBounds.Width, textBounds.Height, paint));
|
||||
currentX += textBounds.Width;
|
||||
}
|
||||
|
||||
// Calculate vertical offset
|
||||
float totalHeight = currentY + lineHeight - bounds.Top;
|
||||
float verticalOffset = VerticalTextAlignment switch
|
||||
{
|
||||
TextAlignment.Start => 0f,
|
||||
TextAlignment.Center => (bounds.Height - totalHeight) / 2f,
|
||||
TextAlignment.End => bounds.Height - totalHeight,
|
||||
_ => 0f
|
||||
};
|
||||
|
||||
// Second pass: draw with alignment
|
||||
currentX = bounds.Left;
|
||||
currentY = bounds.Top + verticalOffset;
|
||||
lineHeight = 0f;
|
||||
|
||||
var currentLine = new List<(SkiaTextSpan span, float relX, float width, float height, SKPaint paint)>();
|
||||
float lineLeft = bounds.Left;
|
||||
|
||||
foreach (var span in FormattedSpans)
|
||||
{
|
||||
if (string.IsNullOrEmpty(span.Text))
|
||||
continue;
|
||||
|
||||
var paint = CreateSpanPaint(span);
|
||||
var textBounds = new SKRect();
|
||||
paint.MeasureText(span.Text, ref textBounds);
|
||||
lineHeight = Math.Max(lineHeight, textBounds.Height);
|
||||
|
||||
if (currentX + textBounds.Width > bounds.Right && currentX > bounds.Left)
|
||||
{
|
||||
DrawFormattedLine(canvas, bounds, currentLine, currentY + lineHeight);
|
||||
currentY += lineHeight * LineHeight;
|
||||
currentX = bounds.Left;
|
||||
lineHeight = textBounds.Height;
|
||||
currentLine.Clear();
|
||||
}
|
||||
|
||||
currentLine.Add((span, currentX - lineLeft, textBounds.Width, textBounds.Height, paint));
|
||||
currentX += textBounds.Width;
|
||||
}
|
||||
|
||||
if (currentLine.Count > 0)
|
||||
{
|
||||
DrawFormattedLine(canvas, bounds, currentLine, currentY + lineHeight);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFormattedLine(SKCanvas canvas, SKRect bounds,
|
||||
List<(SkiaTextSpan span, float x, float width, float height, SKPaint paint)> lineSpans, float y)
|
||||
{
|
||||
if (lineSpans.Count == 0) return;
|
||||
|
||||
float lineWidth = lineSpans.Sum(s => s.width);
|
||||
float startX = HorizontalTextAlignment switch
|
||||
{
|
||||
TextAlignment.Start => bounds.Left,
|
||||
TextAlignment.Center => bounds.Left + (bounds.Width - lineWidth) / 2f,
|
||||
TextAlignment.End => bounds.Right - lineWidth,
|
||||
_ => bounds.Left
|
||||
};
|
||||
|
||||
foreach (var (span, relX, width, height, paint) in lineSpans)
|
||||
{
|
||||
float x = startX + relX;
|
||||
|
||||
// Draw background if specified
|
||||
if (span.BackgroundColor.HasValue && span.BackgroundColor.Value != SKColors.Transparent)
|
||||
{
|
||||
using var bgPaint = new SKPaint
|
||||
{
|
||||
Color = span.BackgroundColor.Value,
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
canvas.DrawRect(x, y - height, width, height + 4f, bgPaint);
|
||||
}
|
||||
|
||||
canvas.DrawText(span.Text, x, y, paint);
|
||||
|
||||
// Draw underline
|
||||
if (span.IsUnderline)
|
||||
{
|
||||
using var linePaint = new SKPaint
|
||||
{
|
||||
Color = paint.Color,
|
||||
StrokeWidth = 1f,
|
||||
IsAntialias = true
|
||||
};
|
||||
canvas.DrawLine(x, y + 2f, x + width, y + 2f, linePaint);
|
||||
}
|
||||
|
||||
// Draw strikethrough
|
||||
if (span.IsStrikethrough)
|
||||
{
|
||||
using var linePaint = new SKPaint
|
||||
{
|
||||
Color = paint.Color,
|
||||
StrokeWidth = 1f,
|
||||
IsAntialias = true
|
||||
};
|
||||
canvas.DrawLine(x, y - height / 3f, x + width, y - height / 3f, linePaint);
|
||||
}
|
||||
|
||||
paint.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private SKPaint CreateSpanPaint(SkiaTextSpan span)
|
||||
{
|
||||
var fontStyle = new SKFontStyle(
|
||||
span.IsBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal,
|
||||
SKFontStyleWidth.Normal,
|
||||
span.IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright);
|
||||
|
||||
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(span.FontFamily ?? FontFamily, fontStyle);
|
||||
if (typeface == null || typeface == SKTypeface.Default)
|
||||
{
|
||||
typeface = GetLinuxTypeface();
|
||||
}
|
||||
|
||||
var fontSize = span.FontSize > 0f ? span.FontSize : FontSize;
|
||||
using var font = new SKFont(typeface, fontSize);
|
||||
|
||||
var color = span.TextColor ?? TextColor;
|
||||
if (!IsEnabled)
|
||||
{
|
||||
color = color.WithAlpha(128);
|
||||
}
|
||||
|
||||
return new SKPaint(font)
|
||||
{
|
||||
Color = color,
|
||||
IsAntialias = true
|
||||
};
|
||||
}
|
||||
|
||||
public override void OnPointerPressed(PointerEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
Tapped?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -284,6 +284,21 @@ public class SkiaShell : SkiaLayoutView
|
||||
/// </summary>
|
||||
public int CurrentSectionIndex => _selectedSectionIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the MAUI Shell this view represents.
|
||||
/// </summary>
|
||||
public Shell? MauiShell { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback to render content from a ShellContent.
|
||||
/// </summary>
|
||||
public Func<Microsoft.Maui.Controls.ShellContent, SkiaView?>? ContentRenderer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback to refresh shell colors.
|
||||
/// </summary>
|
||||
public Action<SkiaShell, Shell>? ColorRefresher { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when FlyoutIsPresented changes.
|
||||
/// </summary>
|
||||
@@ -342,6 +357,48 @@ public class SkiaShell : SkiaLayoutView
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the shell theme and re-renders all pages.
|
||||
/// </summary>
|
||||
public void RefreshTheme()
|
||||
{
|
||||
Console.WriteLine("[SkiaShell] RefreshTheme called - refreshing all pages");
|
||||
if (MauiShell != null && ColorRefresher != null)
|
||||
{
|
||||
Console.WriteLine("[SkiaShell] Refreshing shell colors");
|
||||
ColorRefresher(this, MauiShell);
|
||||
}
|
||||
if (ContentRenderer != null)
|
||||
{
|
||||
foreach (var section in _sections)
|
||||
{
|
||||
foreach (var item in section.Items)
|
||||
{
|
||||
if (item.MauiShellContent != null)
|
||||
{
|
||||
Console.WriteLine("[SkiaShell] Re-rendering: " + item.Title);
|
||||
var skiaView = ContentRenderer(item.MauiShellContent);
|
||||
if (skiaView != null)
|
||||
{
|
||||
item.Content = skiaView;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_selectedSectionIndex >= 0 && _selectedSectionIndex < _sections.Count)
|
||||
{
|
||||
var section = _sections[_selectedSectionIndex];
|
||||
if (_selectedItemIndex >= 0 && _selectedItemIndex < section.Items.Count)
|
||||
{
|
||||
var item = section.Items[_selectedItemIndex];
|
||||
SetCurrentContent(item.Content);
|
||||
}
|
||||
}
|
||||
InvalidateMeasure();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigates using a URI route.
|
||||
/// </summary>
|
||||
@@ -900,6 +957,11 @@ public class ShellContent
|
||||
/// The content view.
|
||||
/// </summary>
|
||||
public SkiaView? Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the MAUI ShellContent this represents.
|
||||
/// </summary>
|
||||
public Microsoft.Maui.Controls.ShellContent? MauiShellContent { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform.Linux;
|
||||
using Microsoft.Maui.Platform.Linux.Handlers;
|
||||
using Microsoft.Maui.Platform.Linux.Rendering;
|
||||
using Microsoft.Maui.Platform.Linux.Window;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform;
|
||||
@@ -218,6 +223,116 @@ public abstract class SkiaView : BindableObject, IDisposable
|
||||
typeof(SkiaView),
|
||||
string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for Scale.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty ScaleProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(Scale),
|
||||
typeof(double),
|
||||
typeof(SkiaView),
|
||||
1.0,
|
||||
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for ScaleX.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty ScaleXProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(ScaleX),
|
||||
typeof(double),
|
||||
typeof(SkiaView),
|
||||
1.0,
|
||||
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for ScaleY.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty ScaleYProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(ScaleY),
|
||||
typeof(double),
|
||||
typeof(SkiaView),
|
||||
1.0,
|
||||
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for Rotation.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty RotationProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(Rotation),
|
||||
typeof(double),
|
||||
typeof(SkiaView),
|
||||
0.0,
|
||||
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for RotationX.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty RotationXProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(RotationX),
|
||||
typeof(double),
|
||||
typeof(SkiaView),
|
||||
0.0,
|
||||
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for RotationY.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty RotationYProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(RotationY),
|
||||
typeof(double),
|
||||
typeof(SkiaView),
|
||||
0.0,
|
||||
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for TranslationX.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty TranslationXProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(TranslationX),
|
||||
typeof(double),
|
||||
typeof(SkiaView),
|
||||
0.0,
|
||||
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for TranslationY.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty TranslationYProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(TranslationY),
|
||||
typeof(double),
|
||||
typeof(SkiaView),
|
||||
0.0,
|
||||
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for AnchorX.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty AnchorXProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(AnchorX),
|
||||
typeof(double),
|
||||
typeof(SkiaView),
|
||||
0.5,
|
||||
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
|
||||
|
||||
/// <summary>
|
||||
/// Bindable property for AnchorY.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty AnchorYProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(AnchorY),
|
||||
typeof(double),
|
||||
typeof(SkiaView),
|
||||
0.5,
|
||||
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
|
||||
|
||||
#endregion
|
||||
|
||||
private bool _disposed;
|
||||
@@ -408,6 +523,107 @@ public abstract class SkiaView : BindableObject, IDisposable
|
||||
set => SetValue(NameProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the uniform scale factor.
|
||||
/// </summary>
|
||||
public double Scale
|
||||
{
|
||||
get => (double)GetValue(ScaleProperty);
|
||||
set => SetValue(ScaleProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the X-axis scale factor.
|
||||
/// </summary>
|
||||
public double ScaleX
|
||||
{
|
||||
get => (double)GetValue(ScaleXProperty);
|
||||
set => SetValue(ScaleXProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Y-axis scale factor.
|
||||
/// </summary>
|
||||
public double ScaleY
|
||||
{
|
||||
get => (double)GetValue(ScaleYProperty);
|
||||
set => SetValue(ScaleYProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the rotation in degrees around the Z-axis.
|
||||
/// </summary>
|
||||
public double Rotation
|
||||
{
|
||||
get => (double)GetValue(RotationProperty);
|
||||
set => SetValue(RotationProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the rotation in degrees around the X-axis.
|
||||
/// </summary>
|
||||
public double RotationX
|
||||
{
|
||||
get => (double)GetValue(RotationXProperty);
|
||||
set => SetValue(RotationXProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the rotation in degrees around the Y-axis.
|
||||
/// </summary>
|
||||
public double RotationY
|
||||
{
|
||||
get => (double)GetValue(RotationYProperty);
|
||||
set => SetValue(RotationYProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the X translation offset.
|
||||
/// </summary>
|
||||
public double TranslationX
|
||||
{
|
||||
get => (double)GetValue(TranslationXProperty);
|
||||
set => SetValue(TranslationXProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Y translation offset.
|
||||
/// </summary>
|
||||
public double TranslationY
|
||||
{
|
||||
get => (double)GetValue(TranslationYProperty);
|
||||
set => SetValue(TranslationYProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the X anchor point for transforms (0.0 to 1.0).
|
||||
/// </summary>
|
||||
public double AnchorX
|
||||
{
|
||||
get => (double)GetValue(AnchorXProperty);
|
||||
set => SetValue(AnchorXProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Y anchor point for transforms (0.0 to 1.0).
|
||||
/// </summary>
|
||||
public double AnchorY
|
||||
{
|
||||
get => (double)GetValue(AnchorYProperty);
|
||||
set => SetValue(AnchorYProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cursor type when hovering over this view.
|
||||
/// </summary>
|
||||
public CursorType CursorType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MAUI View this platform view represents.
|
||||
/// Used for gesture processing.
|
||||
/// </summary>
|
||||
public View? MauiView { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this view currently has keyboard focus.
|
||||
/// </summary>
|
||||
@@ -566,8 +782,23 @@ public abstract class SkiaView : BindableObject, IDisposable
|
||||
/// </summary>
|
||||
public void Invalidate()
|
||||
{
|
||||
LinuxApplication.LogInvalidate(GetType().Name);
|
||||
Invalidated?.Invoke(this, EventArgs.Empty);
|
||||
_parent?.Invalidate();
|
||||
|
||||
// Notify rendering engine of dirty region
|
||||
if (Bounds.Width > 0 && Bounds.Height > 0)
|
||||
{
|
||||
SkiaRenderingEngine.Current?.InvalidateRegion(Bounds);
|
||||
}
|
||||
|
||||
if (_parent != null)
|
||||
{
|
||||
_parent.Invalidate();
|
||||
}
|
||||
else
|
||||
{
|
||||
LinuxApplication.RequestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -583,7 +814,7 @@ public abstract class SkiaView : BindableObject, IDisposable
|
||||
/// <summary>
|
||||
/// Draws this view and its children to the canvas.
|
||||
/// </summary>
|
||||
public void Draw(SKCanvas canvas)
|
||||
public virtual void Draw(SKCanvas canvas)
|
||||
{
|
||||
if (!IsVisible || Opacity <= 0)
|
||||
{
|
||||
@@ -592,6 +823,42 @@ public abstract class SkiaView : BindableObject, IDisposable
|
||||
|
||||
canvas.Save();
|
||||
|
||||
// Apply transforms if any are set
|
||||
if (Scale != 1.0 || ScaleX != 1.0 || ScaleY != 1.0 ||
|
||||
Rotation != 0.0 || RotationX != 0.0 || RotationY != 0.0 ||
|
||||
TranslationX != 0.0 || TranslationY != 0.0)
|
||||
{
|
||||
// Calculate anchor point in absolute coordinates
|
||||
float anchorAbsX = Bounds.Left + (float)(Bounds.Width * AnchorX);
|
||||
float anchorAbsY = Bounds.Top + (float)(Bounds.Height * AnchorY);
|
||||
|
||||
// Move origin to anchor point
|
||||
canvas.Translate(anchorAbsX, anchorAbsY);
|
||||
|
||||
// Apply translation
|
||||
if (TranslationX != 0.0 || TranslationY != 0.0)
|
||||
{
|
||||
canvas.Translate((float)TranslationX, (float)TranslationY);
|
||||
}
|
||||
|
||||
// Apply rotation
|
||||
if (Rotation != 0.0)
|
||||
{
|
||||
canvas.RotateDegrees((float)Rotation);
|
||||
}
|
||||
|
||||
// Apply scale
|
||||
float scaleX = (float)(Scale * ScaleX);
|
||||
float scaleY = (float)(Scale * ScaleY);
|
||||
if (scaleX != 1f || scaleY != 1f)
|
||||
{
|
||||
canvas.Scale(scaleX, scaleY);
|
||||
}
|
||||
|
||||
// Move origin back
|
||||
canvas.Translate(-anchorAbsX, -anchorAbsY);
|
||||
}
|
||||
|
||||
// Apply opacity
|
||||
if (Opacity < 1.0f)
|
||||
{
|
||||
@@ -706,11 +973,47 @@ public abstract class SkiaView : BindableObject, IDisposable
|
||||
|
||||
#region Input Events
|
||||
|
||||
public virtual void OnPointerEntered(PointerEventArgs e) { }
|
||||
public virtual void OnPointerExited(PointerEventArgs e) { }
|
||||
public virtual void OnPointerMoved(PointerEventArgs e) { }
|
||||
public virtual void OnPointerPressed(PointerEventArgs e) { }
|
||||
public virtual void OnPointerReleased(PointerEventArgs e) { }
|
||||
public virtual void OnPointerEntered(PointerEventArgs e)
|
||||
{
|
||||
if (MauiView != null)
|
||||
{
|
||||
GestureManager.ProcessPointerEntered(MauiView, e.X, e.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnPointerExited(PointerEventArgs e)
|
||||
{
|
||||
if (MauiView != null)
|
||||
{
|
||||
GestureManager.ProcessPointerExited(MauiView, e.X, e.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnPointerMoved(PointerEventArgs e)
|
||||
{
|
||||
if (MauiView != null)
|
||||
{
|
||||
GestureManager.ProcessPointerMove(MauiView, e.X, e.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnPointerPressed(PointerEventArgs e)
|
||||
{
|
||||
if (MauiView != null)
|
||||
{
|
||||
GestureManager.ProcessPointerDown(MauiView, e.X, e.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnPointerReleased(PointerEventArgs e)
|
||||
{
|
||||
Console.WriteLine($"[SkiaView] OnPointerReleased on {GetType().Name}, MauiView={MauiView?.GetType().Name ?? "null"}");
|
||||
if (MauiView != null)
|
||||
{
|
||||
GestureManager.ProcessPointerUp(MauiView, e.X, e.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnScroll(ScrollEventArgs e) { }
|
||||
public virtual void OnKeyDown(KeyEventArgs e) { }
|
||||
public virtual void OnKeyUp(KeyEventArgs e) { }
|
||||
|
||||
@@ -129,6 +129,37 @@ public class SkiaWebView : SkiaView
|
||||
private const int RTLD_GLOBAL = 0x100;
|
||||
|
||||
private static IntPtr _webkitHandle;
|
||||
private static IntPtr _mainDisplay;
|
||||
private static IntPtr _mainWindow;
|
||||
private static readonly HashSet<SkiaWebView> _activeWebViews = new();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the main window for WebView operations.
|
||||
/// </summary>
|
||||
public static void SetMainWindow(IntPtr display, IntPtr window)
|
||||
{
|
||||
_mainDisplay = display;
|
||||
_mainWindow = window;
|
||||
Console.WriteLine($"[WebView] Main window set: display={display}, window={window}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes pending GTK events for WebViews.
|
||||
/// </summary>
|
||||
public static void ProcessGtkEvents()
|
||||
{
|
||||
bool hasActiveWebViews;
|
||||
lock (_activeWebViews)
|
||||
{
|
||||
hasActiveWebViews = _activeWebViews.Count > 0;
|
||||
}
|
||||
if (hasActiveWebViews && _gtkInitialized)
|
||||
{
|
||||
while (g_main_context_iteration(IntPtr.Zero, mayBlock: false))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
Reference in New Issue
Block a user