Editor and Search
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user