Editor and Search

This commit is contained in:
2026-01-16 04:39:50 +00:00
parent 209c56e592
commit d5a7560479
5 changed files with 630 additions and 96 deletions

View File

@@ -21,9 +21,11 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
[nameof(IEditor.Placeholder)] = MapPlaceholder,
[nameof(IEditor.PlaceholderColor)] = MapPlaceholderColor,
[nameof(IEditor.TextColor)] = MapTextColor,
[nameof(ITextStyle.Font)] = MapFont,
[nameof(IEditor.CharacterSpacing)] = MapCharacterSpacing,
[nameof(IEditor.IsReadOnly)] = MapIsReadOnly,
[nameof(IEditor.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled,
[nameof(IEditor.IsSpellCheckEnabled)] = MapIsSpellCheckEnabled,
[nameof(IEditor.MaxLength)] = MapMaxLength,
[nameof(IEditor.CursorPosition)] = MapCursorPosition,
[nameof(IEditor.SelectionLength)] = MapSelectionLength,
@@ -97,7 +99,7 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
if (handler.PlatformView is null) return;
if (editor.PlaceholderColor is not null)
{
handler.PlatformView.PlaceholderColor = editor.PlaceholderColor.ToSKColor();
handler.PlatformView.PlaceholderColor = editor.PlaceholderColor;
}
}
@@ -106,13 +108,34 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
if (handler.PlatformView is null) return;
if (editor.TextColor is not null)
{
handler.PlatformView.TextColor = editor.TextColor.ToSKColor();
handler.PlatformView.TextColor = editor.TextColor;
}
}
public static void MapFont(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView is null) return;
var font = editor.Font;
if (font.Size > 0)
handler.PlatformView.FontSize = font.Size;
if (!string.IsNullOrEmpty(font.Family))
handler.PlatformView.FontFamily = font.Family;
// Convert Font weight/slant to FontAttributes
FontAttributes attrs = FontAttributes.None;
if (font.Weight >= FontWeight.Bold)
attrs |= FontAttributes.Bold;
if (font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique)
attrs |= FontAttributes.Italic;
handler.PlatformView.FontAttributes = attrs;
}
public static void MapCharacterSpacing(EditorHandler handler, IEditor editor)
{
// Character spacing would require custom text rendering
if (handler.PlatformView is null) return;
handler.PlatformView.CharacterSpacing = editor.CharacterSpacing;
}
public static void MapIsReadOnly(EditorHandler handler, IEditor editor)
@@ -123,7 +146,14 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
public static void MapIsTextPredictionEnabled(EditorHandler handler, IEditor editor)
{
// Text prediction not applicable to desktop
if (handler.PlatformView is null) return;
handler.PlatformView.IsTextPredictionEnabled = editor.IsTextPredictionEnabled;
}
public static void MapIsSpellCheckEnabled(EditorHandler handler, IEditor editor)
{
if (handler.PlatformView is null) return;
handler.PlatformView.IsSpellCheckEnabled = editor.IsSpellCheckEnabled;
}
public static void MapMaxLength(EditorHandler handler, IEditor editor)
@@ -140,22 +170,39 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
public static void MapSelectionLength(EditorHandler handler, IEditor editor)
{
// Selection would need to be added to SkiaEditor
if (handler.PlatformView is null) return;
handler.PlatformView.SelectionLength = editor.SelectionLength;
}
public static void MapKeyboard(EditorHandler handler, IEditor editor)
{
// Virtual keyboard type not applicable to desktop
// Virtual keyboard type not applicable to desktop - stored for future use
}
public static void MapHorizontalTextAlignment(EditorHandler handler, IEditor editor)
{
// Text alignment would require changes to SkiaEditor drawing
if (handler.PlatformView is null) return;
handler.PlatformView.HorizontalTextAlignment = editor.HorizontalTextAlignment switch
{
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
_ => TextAlignment.Start
};
}
public static void MapVerticalTextAlignment(EditorHandler handler, IEditor editor)
{
// Text alignment would require changes to SkiaEditor drawing
if (handler.PlatformView is null) return;
handler.PlatformView.VerticalTextAlignment = editor.VerticalTextAlignment switch
{
Microsoft.Maui.TextAlignment.Start => TextAlignment.Start,
Microsoft.Maui.TextAlignment.Center => TextAlignment.Center,
Microsoft.Maui.TextAlignment.End => TextAlignment.End,
_ => TextAlignment.Start
};
}
public static void MapBackground(EditorHandler handler, IEditor editor)
@@ -164,7 +211,7 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
if (editor.Background is SolidPaint solidPaint && solidPaint.Color is not null)
{
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
handler.PlatformView.EditorBackgroundColor = solidPaint.Color;
}
}
@@ -172,9 +219,9 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
{
if (handler.PlatformView is null) return;
if (editor is VisualElement ve && ve.BackgroundColor != null)
if (editor is Editor ve && ve.BackgroundColor != null)
{
handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor();
handler.PlatformView.EditorBackgroundColor = ve.BackgroundColor;
handler.PlatformView.Invalidate();
}
}

View File

@@ -84,7 +84,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
if (handler.PlatformView is null) return;
if (searchBar.TextColor is not null)
handler.PlatformView.TextColor = searchBar.TextColor.ToSKColor();
handler.PlatformView.TextColor = searchBar.TextColor;
}
public static void MapFont(SearchBarHandler handler, ISearchBar searchBar)
@@ -110,7 +110,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
if (handler.PlatformView is null) return;
if (searchBar.PlaceholderColor is not null)
handler.PlatformView.PlaceholderColor = searchBar.PlaceholderColor.ToSKColor();
handler.PlatformView.PlaceholderColor = searchBar.PlaceholderColor;
}
public static void MapCancelButtonColor(SearchBarHandler handler, ISearchBar searchBar)

View File

@@ -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 System;
using System.Collections.Generic;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Rendering;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
@@ -42,9 +47,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty TextColorProperty =
BindableProperty.Create(
nameof(TextColor),
typeof(SKColor),
typeof(Color),
typeof(SkiaEditor),
SKColors.Black,
Colors.Black,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
@@ -54,9 +59,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty PlaceholderColorProperty =
BindableProperty.Create(
nameof(PlaceholderColor),
typeof(SKColor),
typeof(Color),
typeof(SkiaEditor),
new SKColor(0x80, 0x80, 0x80),
Color.FromRgb(0x80, 0x80, 0x80),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
@@ -66,9 +71,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty BorderColorProperty =
BindableProperty.Create(
nameof(BorderColor),
typeof(SKColor),
typeof(Color),
typeof(SkiaEditor),
new SKColor(0xBD, 0xBD, 0xBD),
Color.FromRgb(0xBD, 0xBD, 0xBD),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
@@ -78,9 +83,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty SelectionColorProperty =
BindableProperty.Create(
nameof(SelectionColor),
typeof(SKColor),
typeof(Color),
typeof(SkiaEditor),
new SKColor(0x21, 0x96, 0xF3, 0x60),
Color.FromRgba(0x21, 0x96, 0xF3, 0x60),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
@@ -90,9 +95,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty CursorColorProperty =
BindableProperty.Create(
nameof(CursorColor),
typeof(SKColor),
typeof(Color),
typeof(SkiaEditor),
new SKColor(0x21, 0x96, 0xF3),
Color.FromRgb(0x21, 0x96, 0xF3),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
@@ -114,9 +119,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty FontSizeProperty =
BindableProperty.Create(
nameof(FontSize),
typeof(float),
typeof(double),
typeof(SkiaEditor),
14f,
14.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
@@ -126,9 +131,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty LineHeightProperty =
BindableProperty.Create(
nameof(LineHeight),
typeof(float),
typeof(double),
typeof(SkiaEditor),
1.4f,
1.4,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
@@ -138,21 +143,21 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty CornerRadiusProperty =
BindableProperty.Create(
nameof(CornerRadius),
typeof(float),
typeof(double),
typeof(SkiaEditor),
4f,
4.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary>
/// Bindable property for Padding.
/// </summary>
public static readonly BindableProperty PaddingProperty =
public static new readonly BindableProperty PaddingProperty =
BindableProperty.Create(
nameof(Padding),
typeof(float),
typeof(Thickness),
typeof(SkiaEditor),
12f,
new Thickness(12),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
@@ -191,6 +196,127 @@ public class SkiaEditor : SkiaView
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
/// <summary>
/// Bindable property for FontAttributes.
/// </summary>
public static readonly BindableProperty FontAttributesProperty =
BindableProperty.Create(
nameof(FontAttributes),
typeof(FontAttributes),
typeof(SkiaEditor),
FontAttributes.None,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
/// <summary>
/// Bindable property for CharacterSpacing.
/// </summary>
public static readonly BindableProperty CharacterSpacingProperty =
BindableProperty.Create(
nameof(CharacterSpacing),
typeof(double),
typeof(SkiaEditor),
0.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary>
/// Bindable property for IsTextPredictionEnabled.
/// </summary>
public static readonly BindableProperty IsTextPredictionEnabledProperty =
BindableProperty.Create(
nameof(IsTextPredictionEnabled),
typeof(bool),
typeof(SkiaEditor),
true);
/// <summary>
/// Bindable property for IsSpellCheckEnabled.
/// </summary>
public static readonly BindableProperty IsSpellCheckEnabledProperty =
BindableProperty.Create(
nameof(IsSpellCheckEnabled),
typeof(bool),
typeof(SkiaEditor),
true);
/// <summary>
/// Bindable property for SelectionLength.
/// </summary>
public static readonly BindableProperty SelectionLengthProperty =
BindableProperty.Create(
nameof(SelectionLength),
typeof(int),
typeof(SkiaEditor),
0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary>
/// Bindable property for CursorPosition.
/// </summary>
public static readonly BindableProperty CursorPositionProperty =
BindableProperty.Create(
nameof(CursorPosition),
typeof(int),
typeof(SkiaEditor),
0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).OnCursorPositionPropertyChanged((int)n));
/// <summary>
/// Bindable property for HorizontalTextAlignment.
/// </summary>
public static readonly BindableProperty HorizontalTextAlignmentProperty =
BindableProperty.Create(
nameof(HorizontalTextAlignment),
typeof(TextAlignment),
typeof(SkiaEditor),
TextAlignment.Start,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary>
/// Bindable property for VerticalTextAlignment.
/// </summary>
public static readonly BindableProperty VerticalTextAlignmentProperty =
BindableProperty.Create(
nameof(VerticalTextAlignment),
typeof(TextAlignment),
typeof(SkiaEditor),
TextAlignment.Start,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary>
/// Bindable property for background color exposed for MAUI binding.
/// </summary>
public static readonly BindableProperty EditorBackgroundColorProperty =
BindableProperty.Create(
nameof(EditorBackgroundColor),
typeof(Color),
typeof(SkiaEditor),
Colors.White,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
#endregion
#region Color Conversion Helper
/// <summary>
/// Converts a MAUI Color to SkiaSharp SKColor.
/// </summary>
private static SKColor ToSKColor(Color color)
{
if (color == null) return SKColors.Transparent;
return new SKColor(
(byte)(color.Red * 255),
(byte)(color.Green * 255),
(byte)(color.Blue * 255),
(byte)(color.Alpha * 255));
}
#endregion
#region Properties
@@ -216,45 +342,45 @@ public class SkiaEditor : SkiaView
/// <summary>
/// Gets or sets the text color.
/// </summary>
public SKColor TextColor
public Color TextColor
{
get => (SKColor)GetValue(TextColorProperty);
get => (Color)GetValue(TextColorProperty);
set => SetValue(TextColorProperty, value);
}
/// <summary>
/// Gets or sets the placeholder color.
/// </summary>
public SKColor PlaceholderColor
public Color PlaceholderColor
{
get => (SKColor)GetValue(PlaceholderColorProperty);
get => (Color)GetValue(PlaceholderColorProperty);
set => SetValue(PlaceholderColorProperty, value);
}
/// <summary>
/// Gets or sets the border color.
/// </summary>
public SKColor BorderColor
public Color BorderColor
{
get => (SKColor)GetValue(BorderColorProperty);
get => (Color)GetValue(BorderColorProperty);
set => SetValue(BorderColorProperty, value);
}
/// <summary>
/// Gets or sets the selection color.
/// </summary>
public SKColor SelectionColor
public Color SelectionColor
{
get => (SKColor)GetValue(SelectionColorProperty);
get => (Color)GetValue(SelectionColorProperty);
set => SetValue(SelectionColorProperty, value);
}
/// <summary>
/// Gets or sets the cursor color.
/// </summary>
public SKColor CursorColor
public Color CursorColor
{
get => (SKColor)GetValue(CursorColorProperty);
get => (Color)GetValue(CursorColorProperty);
set => SetValue(CursorColorProperty, value);
}
@@ -270,36 +396,36 @@ public class SkiaEditor : SkiaView
/// <summary>
/// Gets or sets the font size.
/// </summary>
public float FontSize
public double FontSize
{
get => (float)GetValue(FontSizeProperty);
get => (double)GetValue(FontSizeProperty);
set => SetValue(FontSizeProperty, value);
}
/// <summary>
/// Gets or sets the line height multiplier.
/// </summary>
public float LineHeight
public double LineHeight
{
get => (float)GetValue(LineHeightProperty);
get => (double)GetValue(LineHeightProperty);
set => SetValue(LineHeightProperty, value);
}
/// <summary>
/// Gets or sets the corner radius.
/// </summary>
public float CornerRadius
public double CornerRadius
{
get => (float)GetValue(CornerRadiusProperty);
get => (double)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
/// <summary>
/// Gets or sets the padding.
/// </summary>
public float Padding
public new Thickness Padding
{
get => (float)GetValue(PaddingProperty);
get => (Thickness)GetValue(PaddingProperty);
set => SetValue(PaddingProperty, value);
}
@@ -330,6 +456,42 @@ public class SkiaEditor : SkiaView
set => SetValue(AutoSizeProperty, value);
}
/// <summary>
/// Gets or sets the font attributes (Bold, Italic, etc.).
/// </summary>
public FontAttributes FontAttributes
{
get => (FontAttributes)GetValue(FontAttributesProperty);
set => SetValue(FontAttributesProperty, value);
}
/// <summary>
/// Gets or sets the character spacing.
/// </summary>
public double CharacterSpacing
{
get => (double)GetValue(CharacterSpacingProperty);
set => SetValue(CharacterSpacingProperty, value);
}
/// <summary>
/// Gets or sets whether text prediction is enabled.
/// </summary>
public bool IsTextPredictionEnabled
{
get => (bool)GetValue(IsTextPredictionEnabledProperty);
set => SetValue(IsTextPredictionEnabledProperty, value);
}
/// <summary>
/// Gets or sets whether spell check is enabled.
/// </summary>
public bool IsSpellCheckEnabled
{
get => (bool)GetValue(IsSpellCheckEnabledProperty);
set => SetValue(IsSpellCheckEnabledProperty, value);
}
/// <summary>
/// Gets or sets the cursor position.
/// </summary>
@@ -338,14 +500,74 @@ public class SkiaEditor : SkiaView
get => _cursorPosition;
set
{
_cursorPosition = Math.Clamp(value, 0, Text.Length);
var newValue = Math.Clamp(value, 0, (Text ?? "").Length);
if (_cursorPosition != newValue)
{
_cursorPosition = newValue;
SetValue(CursorPositionProperty, newValue);
EnsureCursorVisible();
Invalidate();
}
}
}
/// <summary>
/// Gets or sets the selection length.
/// </summary>
public int SelectionLength
{
get => _selectionLength;
set
{
if (_selectionLength != value)
{
_selectionLength = value;
SetValue(SelectionLengthProperty, value);
Invalidate();
}
}
}
/// <summary>
/// Gets or sets the horizontal text alignment.
/// </summary>
public TextAlignment HorizontalTextAlignment
{
get => (TextAlignment)GetValue(HorizontalTextAlignmentProperty);
set => SetValue(HorizontalTextAlignmentProperty, value);
}
/// <summary>
/// Gets or sets the vertical text alignment.
/// </summary>
public TextAlignment VerticalTextAlignment
{
get => (TextAlignment)GetValue(VerticalTextAlignmentProperty);
set => SetValue(VerticalTextAlignmentProperty, value);
}
/// <summary>
/// Gets or sets the editor background color (MAUI-exposed property).
/// </summary>
public Color EditorBackgroundColor
{
get => (Color)GetValue(EditorBackgroundColorProperty);
set => SetValue(EditorBackgroundColorProperty, value);
}
#endregion
private void OnCursorPositionPropertyChanged(int newValue)
{
var clampedValue = Math.Clamp(newValue, 0, (Text ?? "").Length);
if (_cursorPosition != clampedValue)
{
_cursorPosition = clampedValue;
EnsureCursorVisible();
Invalidate();
}
}
#endregion
private int _cursorPosition;
private int _selectionStart = -1;
private int _selectionLength;
@@ -404,7 +626,7 @@ public class SkiaEditor : SkiaView
return;
}
using var font = new SKFont(SKTypeface.Default, FontSize);
using var font = new SKFont(SKTypeface.Default, (float)FontSize);
// Split by actual newlines first
var paragraphs = text.Split('\n');
@@ -494,8 +716,16 @@ public class SkiaEditor : SkiaView
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
var paddingLeft = (float)Padding.Left;
var paddingTop = (float)Padding.Top;
var paddingRight = (float)Padding.Right;
var paddingBottom = (float)Padding.Bottom;
var fontSize = (float)FontSize;
var lineHeight = (float)LineHeight;
var cornerRadius = (float)CornerRadius;
// Update wrap width if bounds changed and re-wrap text
var newWrapWidth = bounds.Width - Padding * 2;
var newWrapWidth = bounds.Width - paddingLeft - paddingRight;
if (Math.Abs(newWrapWidth - _wrapWidth) > 1)
{
_wrapWidth = newWrapWidth;
@@ -510,34 +740,36 @@ public class SkiaEditor : SkiaView
}
// Draw background
var bgColor = EditorBackgroundColor != null ? ToSKColor(EditorBackgroundColor) :
(IsEnabled ? SKColors.White : new SKColor(0xF5, 0xF5, 0xF5));
using var bgPaint = new SKPaint
{
Color = IsEnabled ? BackgroundColor : new SKColor(0xF5, 0xF5, 0xF5),
Color = bgColor,
Style = SKPaintStyle.Fill,
IsAntialias = true
};
canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), bgPaint);
canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), bgPaint);
// Draw border
using var borderPaint = new SKPaint
{
Color = IsFocused ? CursorColor : BorderColor,
Color = IsFocused ? ToSKColor(CursorColor) : ToSKColor(BorderColor),
Style = SKPaintStyle.Stroke,
StrokeWidth = IsFocused ? 2 : 1,
IsAntialias = true
};
canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), borderPaint);
canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), borderPaint);
// Setup text rendering
using var font = new SKFont(SKTypeface.Default, FontSize);
var lineSpacing = FontSize * LineHeight;
using var font = new SKFont(SKTypeface.Default, fontSize);
var lineSpacing = fontSize * lineHeight;
// Clip to content area
var contentRect = new SKRect(
bounds.Left + Padding,
bounds.Top + Padding,
bounds.Right - Padding,
bounds.Bottom - Padding);
bounds.Left + paddingLeft,
bounds.Top + paddingTop,
bounds.Right - paddingRight,
bounds.Bottom - paddingBottom);
canvas.Save();
canvas.ClipRect(contentRect);
@@ -548,25 +780,26 @@ public class SkiaEditor : SkiaView
{
using var placeholderPaint = new SKPaint(font)
{
Color = PlaceholderColor,
Color = ToSKColor(PlaceholderColor),
IsAntialias = true
};
canvas.DrawText(Placeholder, contentRect.Left, contentRect.Top + FontSize, placeholderPaint);
canvas.DrawText(Placeholder, contentRect.Left, contentRect.Top + fontSize, placeholderPaint);
}
else
{
var textColor = ToSKColor(TextColor);
using var textPaint = new SKPaint(font)
{
Color = IsEnabled ? TextColor : TextColor.WithAlpha(128),
Color = IsEnabled ? textColor : textColor.WithAlpha(128),
IsAntialias = true
};
using var selectionPaint = new SKPaint
{
Color = SelectionColor,
Color = ToSKColor(SelectionColor),
Style = SKPaintStyle.Fill
};
var y = contentRect.Top + FontSize;
var y = contentRect.Top + fontSize;
var charIndex = 0;
for (int lineIndex = 0; lineIndex < _lines.Count; lineIndex++)
@@ -591,7 +824,7 @@ public class SkiaEditor : SkiaView
var startX = x + MeasureText(line.Substring(0, selStartInLine), font);
var endX = x + MeasureText(line.Substring(0, selEndInLine), font);
canvas.DrawRect(new SKRect(startX, y - FontSize, endX, y + lineSpacing - FontSize), selectionPaint);
canvas.DrawRect(new SKRect(startX, y - fontSize, endX, y + lineSpacing - fontSize), selectionPaint);
}
}
@@ -606,12 +839,12 @@ public class SkiaEditor : SkiaView
var cursorX = x + MeasureText(line.Substring(0, Math.Min(cursorCol, line.Length)), font);
using var cursorPaint = new SKPaint
{
Color = CursorColor,
Color = ToSKColor(CursorColor),
Style = SKPaintStyle.Stroke,
StrokeWidth = 2,
IsAntialias = true
};
canvas.DrawLine(cursorX, y - FontSize + 2, cursorX, y + 2, cursorPaint);
canvas.DrawLine(cursorX, y - fontSize + 2, cursorX, y + 2, cursorPaint);
}
}
@@ -623,7 +856,7 @@ public class SkiaEditor : SkiaView
canvas.Restore();
// Draw scrollbar if needed
var totalHeight = _lines.Count * FontSize * LineHeight;
var totalHeight = _lines.Count * fontSize * lineHeight;
if (totalHeight > contentRect.Height)
{
DrawScrollbar(canvas, bounds, contentRect.Height, totalHeight);
@@ -641,8 +874,9 @@ public class SkiaEditor : SkiaView
{
var scrollbarWidth = 6f;
var scrollbarMargin = 2f;
var paddingTop = (float)Padding.Top;
var scrollbarHeight = Math.Max(20, viewHeight * (viewHeight / contentHeight));
var scrollbarY = bounds.Top + Padding + (_scrollOffsetY / contentHeight) * (viewHeight - scrollbarHeight);
var scrollbarY = bounds.Top + paddingTop + (_scrollOffsetY / contentHeight) * (viewHeight - scrollbarHeight);
using var paint = new SKPaint
{
@@ -663,9 +897,11 @@ public class SkiaEditor : SkiaView
private void EnsureCursorVisible()
{
var (line, col) = GetLineColumn(_cursorPosition);
var lineSpacing = FontSize * LineHeight;
var fontSize = (float)FontSize;
var lineHeight = (float)LineHeight;
var lineSpacing = fontSize * lineHeight;
var cursorY = line * lineSpacing;
var viewHeight = Bounds.Height - Padding * 2;
var viewHeight = Bounds.Height - (float)(Padding.Top + Padding.Bottom);
if (cursorY < _scrollOffsetY)
{
@@ -685,13 +921,16 @@ public class SkiaEditor : SkiaView
// Use screen coordinates for proper hit detection
var screenBounds = ScreenBounds;
var contentX = e.X - screenBounds.Left - Padding;
var contentY = e.Y - screenBounds.Top - Padding + _scrollOffsetY;
var paddingLeft = (float)Padding.Left;
var paddingTop = (float)Padding.Top;
var contentX = e.X - screenBounds.Left - paddingLeft;
var contentY = e.Y - screenBounds.Top - paddingTop + _scrollOffsetY;
var lineSpacing = FontSize * LineHeight;
var fontSize = (float)FontSize;
var lineSpacing = fontSize * (float)LineHeight;
var clickedLine = Math.Clamp((int)(contentY / lineSpacing), 0, _lines.Count - 1);
using var font = new SKFont(SKTypeface.Default, FontSize);
using var font = new SKFont(SKTypeface.Default, fontSize);
var line = _lines[clickedLine];
var clickedCol = 0;
@@ -743,13 +982,16 @@ public class SkiaEditor : SkiaView
// Calculate position from mouse coordinates
var screenBounds = ScreenBounds;
var contentX = e.X - screenBounds.Left - Padding;
var contentY = e.Y - screenBounds.Top - Padding + _scrollOffsetY;
var paddingLeft = (float)Padding.Left;
var paddingTop = (float)Padding.Top;
var contentX = e.X - screenBounds.Left - paddingLeft;
var contentY = e.Y - screenBounds.Top - paddingTop + _scrollOffsetY;
var lineSpacing = FontSize * LineHeight;
var fontSize = (float)FontSize;
var lineSpacing = fontSize * (float)LineHeight;
var clickedLine = Math.Clamp((int)(contentY / lineSpacing), 0, _lines.Count - 1);
using var font = new SKFont(SKTypeface.Default, FontSize);
using var font = new SKFont(SKTypeface.Default, fontSize);
var line = _lines[clickedLine];
var clickedCol = 0;
@@ -963,9 +1205,11 @@ public class SkiaEditor : SkiaView
public override void OnScroll(ScrollEventArgs e)
{
var lineSpacing = FontSize * LineHeight;
var fontSize = (float)FontSize;
var lineHeight = (float)LineHeight;
var lineSpacing = fontSize * lineHeight;
var totalHeight = _lines.Count * lineSpacing;
var viewHeight = Bounds.Height - Padding * 2;
var viewHeight = Bounds.Height - (float)(Padding.Top + Padding.Bottom);
var maxScroll = Math.Max(0, totalHeight - viewHeight);
_scrollOffsetY = Math.Clamp(_scrollOffsetY - e.DeltaY * 3, 0, maxScroll);
@@ -1073,8 +1317,11 @@ public class SkiaEditor : SkiaView
{
if (AutoSize)
{
var lineSpacing = FontSize * LineHeight;
var height = Math.Max(lineSpacing + Padding * 2, _lines.Count * lineSpacing + Padding * 2);
var fontSize = (float)FontSize;
var lineHeight = (float)LineHeight;
var lineSpacing = fontSize * lineHeight;
var verticalPadding = (float)(Padding.Top + Padding.Bottom);
var height = Math.Max(lineSpacing + verticalPadding, _lines.Count * lineSpacing + verticalPadding);
return new SKSize(
availableSize.Width < float.MaxValue ? availableSize.Width : 200,
(float)Math.Min(height, availableSize.Height < float.MaxValue ? availableSize.Height : 200));

View File

@@ -347,6 +347,38 @@ public class SkiaLabel : SkiaView
#endregion
#region Selection State
private int _selectionStart = -1;
private int _selectionLength = 0;
private bool _isSelecting = false;
private DateTime _lastClickTime = DateTime.MinValue;
private float _lastClickX;
private const double DoubleClickThresholdMs = 400;
/// <summary>
/// Gets or sets whether text selection is enabled.
/// </summary>
public bool IsTextSelectionEnabled { get; set; } = true;
/// <summary>
/// Gets the currently selected text.
/// </summary>
public string SelectedText
{
get
{
if (_selectionStart < 0 || _selectionLength == 0) return string.Empty;
var text = GetDisplayText();
var start = Math.Min(_selectionStart, _selectionStart + _selectionLength);
var length = Math.Abs(_selectionLength);
if (start < 0 || start >= text.Length) return string.Empty;
return text.Substring(start, Math.Min(length, text.Length - start));
}
}
#endregion
#region Events
/// <summary>
@@ -362,10 +394,177 @@ public class SkiaLabel : SkiaView
Tapped?.Invoke(this, EventArgs.Empty);
}
public override void OnPointerPressed(PointerEventArgs e)
{
base.OnPointerPressed(e);
if (!IsTextSelectionEnabled || string.IsNullOrEmpty(Text)) return;
var text = GetDisplayText();
if (string.IsNullOrEmpty(text)) return;
// Calculate character position from click
var screenBounds = ScreenBounds;
var clickX = e.X - screenBounds.Left - (float)Padding.Left;
var charIndex = GetCharacterIndexAtX(clickX);
// Check for double-click (select word)
var now = DateTime.UtcNow;
var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds;
var distanceFromLastClick = Math.Abs(e.X - _lastClickX);
if (timeSinceLastClick < DoubleClickThresholdMs && distanceFromLastClick < 10)
{
// Double-click: select word
SelectWordAt(charIndex);
_lastClickTime = DateTime.MinValue;
_isSelecting = false;
}
else
{
// Single click: start selection
_selectionStart = charIndex;
_selectionLength = 0;
_isSelecting = true;
_lastClickTime = now;
_lastClickX = e.X;
}
Invalidate();
}
public override void OnPointerMoved(PointerEventArgs e)
{
base.OnPointerMoved(e);
if (!IsTextSelectionEnabled || !_isSelecting) return;
var text = GetDisplayText();
if (string.IsNullOrEmpty(text)) return;
var screenBounds = ScreenBounds;
var clickX = e.X - screenBounds.Left - (float)Padding.Left;
var charIndex = GetCharacterIndexAtX(clickX);
_selectionLength = charIndex - _selectionStart;
Invalidate();
}
public override void OnPointerReleased(PointerEventArgs e)
{
base.OnPointerReleased(e);
OnTapped();
if (_isSelecting && _selectionLength == 0)
{
// No drag happened, it's a tap
OnTapped();
}
_isSelecting = false;
}
public override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (!IsTextSelectionEnabled) return;
// Ctrl+A: Select All
if (e.Key == Key.A && e.Modifiers.HasFlag(KeyModifiers.Control))
{
SelectAll();
e.Handled = true;
}
// Ctrl+C: Copy
else if (e.Key == Key.C && e.Modifiers.HasFlag(KeyModifiers.Control))
{
CopyToClipboard();
e.Handled = true;
}
}
/// <summary>
/// Selects all text in the label.
/// </summary>
public void SelectAll()
{
var text = GetDisplayText();
_selectionStart = 0;
_selectionLength = text.Length;
Invalidate();
}
/// <summary>
/// Clears the current selection.
/// </summary>
public void ClearSelection()
{
_selectionStart = -1;
_selectionLength = 0;
Invalidate();
}
private void SelectWordAt(int charIndex)
{
var text = GetDisplayText();
if (string.IsNullOrEmpty(text) || charIndex < 0 || charIndex >= text.Length) return;
int start = charIndex;
int end = charIndex;
// Move start backwards to beginning of word
while (start > 0 && IsWordChar(text[start - 1]))
start--;
// Move end forwards to end of word
while (end < text.Length && IsWordChar(text[end]))
end++;
_selectionStart = start;
_selectionLength = end - start;
}
private static bool IsWordChar(char c)
{
return char.IsLetterOrDigit(c) || c == '_';
}
private int GetCharacterIndexAtX(float x)
{
var text = GetDisplayText();
if (string.IsNullOrEmpty(text)) return 0;
float fontSize = FontSize > 0 ? (float)FontSize : 14f;
var fontFamily = string.IsNullOrEmpty(FontFamily) ? "Sans" : FontFamily;
using var font = new SKFont(
SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(fontFamily, GetFontStyle()) ?? SKTypeface.Default,
fontSize);
using var paint = new SKPaint(font);
for (int i = 0; i <= text.Length; i++)
{
var substring = text.Substring(0, i);
var width = paint.MeasureText(substring);
if (CharacterSpacing != 0 && i > 0)
{
width += (float)(CharacterSpacing * i);
}
if (width > x)
{
return i > 0 ? i - 1 : 0;
}
}
return text.Length;
}
private void CopyToClipboard()
{
var selectedText = SelectedText;
if (!string.IsNullOrEmpty(selectedText))
{
SystemClipboard.SetText(selectedText);
}
}
#endregion
@@ -533,10 +732,50 @@ public class SkiaLabel : SkiaView
_ => bounds.MidY - textBounds.MidY
};
// Draw selection highlight if applicable
if (_selectionStart >= 0 && _selectionLength != 0)
{
DrawSelectionHighlight(canvas, paint, x, y, displayText, textBounds);
}
DrawTextWithSpacing(canvas, displayText, x, y, paint);
DrawTextDecorations(canvas, paint, x, y, textBounds);
}
private void DrawSelectionHighlight(SKCanvas canvas, SKPaint paint, float x, float y, string text, SKRect textBounds)
{
var selStart = Math.Min(_selectionStart, _selectionStart + _selectionLength);
var selEnd = Math.Max(_selectionStart, _selectionStart + _selectionLength);
// Clamp to text length
selStart = Math.Max(0, Math.Min(selStart, text.Length));
selEnd = Math.Max(0, Math.Min(selEnd, text.Length));
if (selStart >= selEnd) return;
var textToStart = text.Substring(0, selStart);
var textToEnd = text.Substring(0, selEnd);
float startX = x + paint.MeasureText(textToStart);
float endX = x + paint.MeasureText(textToEnd);
if (CharacterSpacing != 0)
{
startX += (float)(CharacterSpacing * selStart);
endX += (float)(CharacterSpacing * selEnd);
}
using var selectionPaint = new SKPaint
{
Color = new SKColor(0x21, 0x96, 0xF3, 0x60), // Semi-transparent blue
Style = SKPaintStyle.Fill
};
float selectionTop = y + textBounds.Top;
float selectionBottom = y + textBounds.Bottom;
canvas.DrawRect(new SKRect(startX, selectionTop, endX, selectionBottom), selectionPaint);
}
private void DrawMultiLineText(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds, string text)
{
float lineHeight = (float)(FontSize * LineHeight);

View File

@@ -1,8 +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 SkiaSharp;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Rendering;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
@@ -26,13 +27,13 @@ public class SkiaSearchBar : SkiaView
set => _entry.Placeholder = value;
}
public SKColor TextColor
public Color TextColor
{
get => _entry.TextColor;
set => _entry.TextColor = value;
}
public SKColor PlaceholderColor
public Color PlaceholderColor
{
get => _entry.PlaceholderColor;
set => _entry.PlaceholderColor = value;
@@ -55,10 +56,10 @@ public class SkiaSearchBar : SkiaView
_entry = new SkiaEntry
{
Placeholder = "Search...",
EntryBackgroundColor = SKColors.Transparent,
EntryBackgroundColor = Colors.Transparent,
BackgroundColor = SKColors.Transparent,
BorderColor = SKColors.Transparent,
FocusedBorderColor = SKColors.Transparent,
BorderColor = Colors.Transparent,
FocusedBorderColor = Colors.Transparent,
BorderWidth = 0
};