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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user