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:
2026-01-01 11:19:58 -05:00
parent e02af03be0
commit f7043ab9c7
56 changed files with 6061 additions and 473 deletions

View File

@@ -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>