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.Placeholder)] = MapPlaceholder,
[nameof(IEditor.PlaceholderColor)] = MapPlaceholderColor, [nameof(IEditor.PlaceholderColor)] = MapPlaceholderColor,
[nameof(IEditor.TextColor)] = MapTextColor, [nameof(IEditor.TextColor)] = MapTextColor,
[nameof(ITextStyle.Font)] = MapFont,
[nameof(IEditor.CharacterSpacing)] = MapCharacterSpacing, [nameof(IEditor.CharacterSpacing)] = MapCharacterSpacing,
[nameof(IEditor.IsReadOnly)] = MapIsReadOnly, [nameof(IEditor.IsReadOnly)] = MapIsReadOnly,
[nameof(IEditor.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled, [nameof(IEditor.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled,
[nameof(IEditor.IsSpellCheckEnabled)] = MapIsSpellCheckEnabled,
[nameof(IEditor.MaxLength)] = MapMaxLength, [nameof(IEditor.MaxLength)] = MapMaxLength,
[nameof(IEditor.CursorPosition)] = MapCursorPosition, [nameof(IEditor.CursorPosition)] = MapCursorPosition,
[nameof(IEditor.SelectionLength)] = MapSelectionLength, [nameof(IEditor.SelectionLength)] = MapSelectionLength,
@@ -97,7 +99,7 @@ public partial class EditorHandler : ViewHandler<IEditor, SkiaEditor>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (editor.PlaceholderColor is not null) 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 (handler.PlatformView is null) return;
if (editor.TextColor is not null) 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) 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) 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) 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) 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) 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) 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) 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) 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) 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) 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 (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(); handler.PlatformView.Invalidate();
} }
} }

View File

@@ -84,7 +84,7 @@ public partial class SearchBarHandler : ViewHandler<ISearchBar, SkiaSearchBar>
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (searchBar.TextColor is not null) 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) 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 (handler.PlatformView is null) return;
if (searchBar.PlaceholderColor is not null) 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) public static void MapCancelButtonColor(SearchBarHandler handler, ISearchBar searchBar)

View File

@@ -1,6 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // 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; using SkiaSharp;
namespace Microsoft.Maui.Platform; namespace Microsoft.Maui.Platform;
@@ -42,9 +47,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty TextColorProperty = public static readonly BindableProperty TextColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(TextColor), nameof(TextColor),
typeof(SKColor), typeof(Color),
typeof(SkiaEditor), typeof(SkiaEditor),
SKColors.Black, Colors.Black,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
@@ -54,9 +59,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty PlaceholderColorProperty = public static readonly BindableProperty PlaceholderColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(PlaceholderColor), nameof(PlaceholderColor),
typeof(SKColor), typeof(Color),
typeof(SkiaEditor), typeof(SkiaEditor),
new SKColor(0x80, 0x80, 0x80), Color.FromRgb(0x80, 0x80, 0x80),
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
@@ -66,9 +71,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty BorderColorProperty = public static readonly BindableProperty BorderColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(BorderColor), nameof(BorderColor),
typeof(SKColor), typeof(Color),
typeof(SkiaEditor), typeof(SkiaEditor),
new SKColor(0xBD, 0xBD, 0xBD), Color.FromRgb(0xBD, 0xBD, 0xBD),
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
@@ -78,9 +83,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty SelectionColorProperty = public static readonly BindableProperty SelectionColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(SelectionColor), nameof(SelectionColor),
typeof(SKColor), typeof(Color),
typeof(SkiaEditor), typeof(SkiaEditor),
new SKColor(0x21, 0x96, 0xF3, 0x60), Color.FromRgba(0x21, 0x96, 0xF3, 0x60),
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
@@ -90,9 +95,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty CursorColorProperty = public static readonly BindableProperty CursorColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(CursorColor), nameof(CursorColor),
typeof(SKColor), typeof(Color),
typeof(SkiaEditor), typeof(SkiaEditor),
new SKColor(0x21, 0x96, 0xF3), Color.FromRgb(0x21, 0x96, 0xF3),
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
@@ -114,9 +119,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty FontSizeProperty = public static readonly BindableProperty FontSizeProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(FontSize), nameof(FontSize),
typeof(float), typeof(double),
typeof(SkiaEditor), typeof(SkiaEditor),
14f, 14.0,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
@@ -126,9 +131,9 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty LineHeightProperty = public static readonly BindableProperty LineHeightProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(LineHeight), nameof(LineHeight),
typeof(float), typeof(double),
typeof(SkiaEditor), typeof(SkiaEditor),
1.4f, 1.4,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
@@ -138,21 +143,21 @@ public class SkiaEditor : SkiaView
public static readonly BindableProperty CornerRadiusProperty = public static readonly BindableProperty CornerRadiusProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(CornerRadius), nameof(CornerRadius),
typeof(float), typeof(double),
typeof(SkiaEditor), typeof(SkiaEditor),
4f, 4.0,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary> /// <summary>
/// Bindable property for Padding. /// Bindable property for Padding.
/// </summary> /// </summary>
public static readonly BindableProperty PaddingProperty = public static new readonly BindableProperty PaddingProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(Padding), nameof(Padding),
typeof(float), typeof(Thickness),
typeof(SkiaEditor), typeof(SkiaEditor),
12f, new Thickness(12),
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
@@ -191,6 +196,127 @@ public class SkiaEditor : SkiaView
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); 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 #endregion
#region Properties #region Properties
@@ -216,45 +342,45 @@ public class SkiaEditor : SkiaView
/// <summary> /// <summary>
/// Gets or sets the text color. /// Gets or sets the text color.
/// </summary> /// </summary>
public SKColor TextColor public Color TextColor
{ {
get => (SKColor)GetValue(TextColorProperty); get => (Color)GetValue(TextColorProperty);
set => SetValue(TextColorProperty, value); set => SetValue(TextColorProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the placeholder color. /// Gets or sets the placeholder color.
/// </summary> /// </summary>
public SKColor PlaceholderColor public Color PlaceholderColor
{ {
get => (SKColor)GetValue(PlaceholderColorProperty); get => (Color)GetValue(PlaceholderColorProperty);
set => SetValue(PlaceholderColorProperty, value); set => SetValue(PlaceholderColorProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the border color. /// Gets or sets the border color.
/// </summary> /// </summary>
public SKColor BorderColor public Color BorderColor
{ {
get => (SKColor)GetValue(BorderColorProperty); get => (Color)GetValue(BorderColorProperty);
set => SetValue(BorderColorProperty, value); set => SetValue(BorderColorProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the selection color. /// Gets or sets the selection color.
/// </summary> /// </summary>
public SKColor SelectionColor public Color SelectionColor
{ {
get => (SKColor)GetValue(SelectionColorProperty); get => (Color)GetValue(SelectionColorProperty);
set => SetValue(SelectionColorProperty, value); set => SetValue(SelectionColorProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the cursor color. /// Gets or sets the cursor color.
/// </summary> /// </summary>
public SKColor CursorColor public Color CursorColor
{ {
get => (SKColor)GetValue(CursorColorProperty); get => (Color)GetValue(CursorColorProperty);
set => SetValue(CursorColorProperty, value); set => SetValue(CursorColorProperty, value);
} }
@@ -270,36 +396,36 @@ public class SkiaEditor : SkiaView
/// <summary> /// <summary>
/// Gets or sets the font size. /// Gets or sets the font size.
/// </summary> /// </summary>
public float FontSize public double FontSize
{ {
get => (float)GetValue(FontSizeProperty); get => (double)GetValue(FontSizeProperty);
set => SetValue(FontSizeProperty, value); set => SetValue(FontSizeProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the line height multiplier. /// Gets or sets the line height multiplier.
/// </summary> /// </summary>
public float LineHeight public double LineHeight
{ {
get => (float)GetValue(LineHeightProperty); get => (double)GetValue(LineHeightProperty);
set => SetValue(LineHeightProperty, value); set => SetValue(LineHeightProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the corner radius. /// Gets or sets the corner radius.
/// </summary> /// </summary>
public float CornerRadius public double CornerRadius
{ {
get => (float)GetValue(CornerRadiusProperty); get => (double)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value); set => SetValue(CornerRadiusProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the padding. /// Gets or sets the padding.
/// </summary> /// </summary>
public float Padding public new Thickness Padding
{ {
get => (float)GetValue(PaddingProperty); get => (Thickness)GetValue(PaddingProperty);
set => SetValue(PaddingProperty, value); set => SetValue(PaddingProperty, value);
} }
@@ -330,6 +456,42 @@ public class SkiaEditor : SkiaView
set => SetValue(AutoSizeProperty, value); 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> /// <summary>
/// Gets or sets the cursor position. /// Gets or sets the cursor position.
/// </summary> /// </summary>
@@ -338,14 +500,74 @@ public class SkiaEditor : SkiaView
get => _cursorPosition; get => _cursorPosition;
set 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(); EnsureCursorVisible();
Invalidate(); 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 #endregion
private void OnCursorPositionPropertyChanged(int newValue)
{
var clampedValue = Math.Clamp(newValue, 0, (Text ?? "").Length);
if (_cursorPosition != clampedValue)
{
_cursorPosition = clampedValue;
EnsureCursorVisible();
Invalidate();
}
}
private int _cursorPosition; private int _cursorPosition;
private int _selectionStart = -1; private int _selectionStart = -1;
private int _selectionLength; private int _selectionLength;
@@ -404,7 +626,7 @@ public class SkiaEditor : SkiaView
return; return;
} }
using var font = new SKFont(SKTypeface.Default, FontSize); using var font = new SKFont(SKTypeface.Default, (float)FontSize);
// Split by actual newlines first // Split by actual newlines first
var paragraphs = text.Split('\n'); var paragraphs = text.Split('\n');
@@ -494,8 +716,16 @@ public class SkiaEditor : SkiaView
protected override void OnDraw(SKCanvas canvas, SKRect bounds) 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 // 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) if (Math.Abs(newWrapWidth - _wrapWidth) > 1)
{ {
_wrapWidth = newWrapWidth; _wrapWidth = newWrapWidth;
@@ -510,34 +740,36 @@ public class SkiaEditor : SkiaView
} }
// Draw background // Draw background
var bgColor = EditorBackgroundColor != null ? ToSKColor(EditorBackgroundColor) :
(IsEnabled ? SKColors.White : new SKColor(0xF5, 0xF5, 0xF5));
using var bgPaint = new SKPaint using var bgPaint = new SKPaint
{ {
Color = IsEnabled ? BackgroundColor : new SKColor(0xF5, 0xF5, 0xF5), Color = bgColor,
Style = SKPaintStyle.Fill, Style = SKPaintStyle.Fill,
IsAntialias = true IsAntialias = true
}; };
canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), bgPaint); canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), bgPaint);
// Draw border // Draw border
using var borderPaint = new SKPaint using var borderPaint = new SKPaint
{ {
Color = IsFocused ? CursorColor : BorderColor, Color = IsFocused ? ToSKColor(CursorColor) : ToSKColor(BorderColor),
Style = SKPaintStyle.Stroke, Style = SKPaintStyle.Stroke,
StrokeWidth = IsFocused ? 2 : 1, StrokeWidth = IsFocused ? 2 : 1,
IsAntialias = true IsAntialias = true
}; };
canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), borderPaint); canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), borderPaint);
// Setup text rendering // Setup text rendering
using var font = new SKFont(SKTypeface.Default, FontSize); using var font = new SKFont(SKTypeface.Default, fontSize);
var lineSpacing = FontSize * LineHeight; var lineSpacing = fontSize * lineHeight;
// Clip to content area // Clip to content area
var contentRect = new SKRect( var contentRect = new SKRect(
bounds.Left + Padding, bounds.Left + paddingLeft,
bounds.Top + Padding, bounds.Top + paddingTop,
bounds.Right - Padding, bounds.Right - paddingRight,
bounds.Bottom - Padding); bounds.Bottom - paddingBottom);
canvas.Save(); canvas.Save();
canvas.ClipRect(contentRect); canvas.ClipRect(contentRect);
@@ -548,25 +780,26 @@ public class SkiaEditor : SkiaView
{ {
using var placeholderPaint = new SKPaint(font) using var placeholderPaint = new SKPaint(font)
{ {
Color = PlaceholderColor, Color = ToSKColor(PlaceholderColor),
IsAntialias = true IsAntialias = true
}; };
canvas.DrawText(Placeholder, contentRect.Left, contentRect.Top + FontSize, placeholderPaint); canvas.DrawText(Placeholder, contentRect.Left, contentRect.Top + fontSize, placeholderPaint);
} }
else else
{ {
var textColor = ToSKColor(TextColor);
using var textPaint = new SKPaint(font) using var textPaint = new SKPaint(font)
{ {
Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), Color = IsEnabled ? textColor : textColor.WithAlpha(128),
IsAntialias = true IsAntialias = true
}; };
using var selectionPaint = new SKPaint using var selectionPaint = new SKPaint
{ {
Color = SelectionColor, Color = ToSKColor(SelectionColor),
Style = SKPaintStyle.Fill Style = SKPaintStyle.Fill
}; };
var y = contentRect.Top + FontSize; var y = contentRect.Top + fontSize;
var charIndex = 0; var charIndex = 0;
for (int lineIndex = 0; lineIndex < _lines.Count; lineIndex++) 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 startX = x + MeasureText(line.Substring(0, selStartInLine), font);
var endX = x + MeasureText(line.Substring(0, selEndInLine), 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); var cursorX = x + MeasureText(line.Substring(0, Math.Min(cursorCol, line.Length)), font);
using var cursorPaint = new SKPaint using var cursorPaint = new SKPaint
{ {
Color = CursorColor, Color = ToSKColor(CursorColor),
Style = SKPaintStyle.Stroke, Style = SKPaintStyle.Stroke,
StrokeWidth = 2, StrokeWidth = 2,
IsAntialias = true 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(); canvas.Restore();
// Draw scrollbar if needed // Draw scrollbar if needed
var totalHeight = _lines.Count * FontSize * LineHeight; var totalHeight = _lines.Count * fontSize * lineHeight;
if (totalHeight > contentRect.Height) if (totalHeight > contentRect.Height)
{ {
DrawScrollbar(canvas, bounds, contentRect.Height, totalHeight); DrawScrollbar(canvas, bounds, contentRect.Height, totalHeight);
@@ -641,8 +874,9 @@ public class SkiaEditor : SkiaView
{ {
var scrollbarWidth = 6f; var scrollbarWidth = 6f;
var scrollbarMargin = 2f; var scrollbarMargin = 2f;
var paddingTop = (float)Padding.Top;
var scrollbarHeight = Math.Max(20, viewHeight * (viewHeight / contentHeight)); 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 using var paint = new SKPaint
{ {
@@ -663,9 +897,11 @@ public class SkiaEditor : SkiaView
private void EnsureCursorVisible() private void EnsureCursorVisible()
{ {
var (line, col) = GetLineColumn(_cursorPosition); 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 cursorY = line * lineSpacing;
var viewHeight = Bounds.Height - Padding * 2; var viewHeight = Bounds.Height - (float)(Padding.Top + Padding.Bottom);
if (cursorY < _scrollOffsetY) if (cursorY < _scrollOffsetY)
{ {
@@ -685,13 +921,16 @@ public class SkiaEditor : SkiaView
// Use screen coordinates for proper hit detection // Use screen coordinates for proper hit detection
var screenBounds = ScreenBounds; var screenBounds = ScreenBounds;
var contentX = e.X - screenBounds.Left - Padding; var paddingLeft = (float)Padding.Left;
var contentY = e.Y - screenBounds.Top - Padding + _scrollOffsetY; 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); 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 line = _lines[clickedLine];
var clickedCol = 0; var clickedCol = 0;
@@ -743,13 +982,16 @@ public class SkiaEditor : SkiaView
// Calculate position from mouse coordinates // Calculate position from mouse coordinates
var screenBounds = ScreenBounds; var screenBounds = ScreenBounds;
var contentX = e.X - screenBounds.Left - Padding; var paddingLeft = (float)Padding.Left;
var contentY = e.Y - screenBounds.Top - Padding + _scrollOffsetY; 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); 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 line = _lines[clickedLine];
var clickedCol = 0; var clickedCol = 0;
@@ -963,9 +1205,11 @@ public class SkiaEditor : SkiaView
public override void OnScroll(ScrollEventArgs e) 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 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); var maxScroll = Math.Max(0, totalHeight - viewHeight);
_scrollOffsetY = Math.Clamp(_scrollOffsetY - e.DeltaY * 3, 0, maxScroll); _scrollOffsetY = Math.Clamp(_scrollOffsetY - e.DeltaY * 3, 0, maxScroll);
@@ -1073,8 +1317,11 @@ public class SkiaEditor : SkiaView
{ {
if (AutoSize) if (AutoSize)
{ {
var lineSpacing = FontSize * LineHeight; var fontSize = (float)FontSize;
var height = Math.Max(lineSpacing + Padding * 2, _lines.Count * lineSpacing + Padding * 2); 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( return new SKSize(
availableSize.Width < float.MaxValue ? availableSize.Width : 200, availableSize.Width < float.MaxValue ? availableSize.Width : 200,
(float)Math.Min(height, availableSize.Height < float.MaxValue ? availableSize.Height : 200)); (float)Math.Min(height, availableSize.Height < float.MaxValue ? availableSize.Height : 200));

View File

@@ -347,6 +347,38 @@ public class SkiaLabel : SkiaView
#endregion #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 #region Events
/// <summary> /// <summary>
@@ -362,12 +394,179 @@ public class SkiaLabel : SkiaView
Tapped?.Invoke(this, EventArgs.Empty); 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) public override void OnPointerReleased(PointerEventArgs e)
{ {
base.OnPointerReleased(e); base.OnPointerReleased(e);
if (_isSelecting && _selectionLength == 0)
{
// No drag happened, it's a tap
OnTapped(); 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 #endregion
#region Private Methods #region Private Methods
@@ -533,10 +732,50 @@ public class SkiaLabel : SkiaView
_ => bounds.MidY - textBounds.MidY _ => 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); DrawTextWithSpacing(canvas, displayText, x, y, paint);
DrawTextDecorations(canvas, paint, x, y, textBounds); 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) private void DrawMultiLineText(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds, string text)
{ {
float lineHeight = (float)(FontSize * LineHeight); float lineHeight = (float)(FontSize * LineHeight);

View File

@@ -1,8 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // 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 Microsoft.Maui.Platform.Linux.Rendering;
using SkiaSharp;
namespace Microsoft.Maui.Platform; namespace Microsoft.Maui.Platform;
@@ -26,13 +27,13 @@ public class SkiaSearchBar : SkiaView
set => _entry.Placeholder = value; set => _entry.Placeholder = value;
} }
public SKColor TextColor public Color TextColor
{ {
get => _entry.TextColor; get => _entry.TextColor;
set => _entry.TextColor = value; set => _entry.TextColor = value;
} }
public SKColor PlaceholderColor public Color PlaceholderColor
{ {
get => _entry.PlaceholderColor; get => _entry.PlaceholderColor;
set => _entry.PlaceholderColor = value; set => _entry.PlaceholderColor = value;
@@ -55,10 +56,10 @@ public class SkiaSearchBar : SkiaView
_entry = new SkiaEntry _entry = new SkiaEntry
{ {
Placeholder = "Search...", Placeholder = "Search...",
EntryBackgroundColor = SKColors.Transparent, EntryBackgroundColor = Colors.Transparent,
BackgroundColor = SKColors.Transparent, BackgroundColor = SKColors.Transparent,
BorderColor = SKColors.Transparent, BorderColor = Colors.Transparent,
FocusedBorderColor = SKColors.Transparent, FocusedBorderColor = Colors.Transparent,
BorderWidth = 0 BorderWidth = 0
}; };