// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux;
using Microsoft.Maui.Platform.Linux.Rendering;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
///
/// Skia-rendered multiline text editor control with full XAML styling support.
/// Implements IInputContext for IME (Input Method Editor) support.
///
public partial class SkiaEditor : SkiaView, IInputContext
{
#region BindableProperties
///
/// Bindable property for Text.
///
public static readonly BindableProperty TextProperty =
BindableProperty.Create(
nameof(Text),
typeof(string),
typeof(SkiaEditor),
"",
BindingMode.OneWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).OnTextPropertyChanged((string)o, (string)n));
///
/// Bindable property for Placeholder.
///
public static readonly BindableProperty PlaceholderProperty =
BindableProperty.Create(
nameof(Placeholder),
typeof(string),
typeof(SkiaEditor),
"",
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
///
/// Bindable property for TextColor.
/// Default is null to match MAUI Editor.TextColor (falls back to platform default).
///
public static readonly BindableProperty TextColorProperty =
BindableProperty.Create(
nameof(TextColor),
typeof(Color),
typeof(SkiaEditor),
null,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
///
/// Bindable property for PlaceholderColor.
/// Default is null to match MAUI Editor.PlaceholderColor (falls back to platform default).
///
public static readonly BindableProperty PlaceholderColorProperty =
BindableProperty.Create(
nameof(PlaceholderColor),
typeof(Color),
typeof(SkiaEditor),
null,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
///
/// Bindable property for BorderColor.
///
public static readonly BindableProperty BorderColorProperty =
BindableProperty.Create(
nameof(BorderColor),
typeof(Color),
typeof(SkiaEditor),
Colors.Transparent,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
///
/// Bindable property for SelectionColor.
///
public static readonly BindableProperty SelectionColorProperty =
BindableProperty.Create(
nameof(SelectionColor),
typeof(Color),
typeof(SkiaEditor),
Color.FromRgba(0x21, 0x96, 0xF3, 0x60),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
///
/// Bindable property for CursorColor.
///
public static readonly BindableProperty CursorColorProperty =
BindableProperty.Create(
nameof(CursorColor),
typeof(Color),
typeof(SkiaEditor),
Color.FromRgb(0x21, 0x96, 0xF3),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
///
/// Bindable property for FontFamily.
/// Default is empty string to match MAUI Editor.FontFamily (falls back to platform default).
///
public static readonly BindableProperty FontFamilyProperty =
BindableProperty.Create(
nameof(FontFamily),
typeof(string),
typeof(SkiaEditor),
string.Empty,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
///
/// Bindable property for FontSize.
///
public static readonly BindableProperty FontSizeProperty =
BindableProperty.Create(
nameof(FontSize),
typeof(double),
typeof(SkiaEditor),
14.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
///
/// Bindable property for LineHeight.
///
public static readonly BindableProperty LineHeightProperty =
BindableProperty.Create(
nameof(LineHeight),
typeof(double),
typeof(SkiaEditor),
1.4,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
///
/// Bindable property for CornerRadius.
///
public static readonly BindableProperty CornerRadiusProperty =
BindableProperty.Create(
nameof(CornerRadius),
typeof(double),
typeof(SkiaEditor),
4.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
///
/// Bindable property for Padding.
///
public static new readonly BindableProperty PaddingProperty =
BindableProperty.Create(
nameof(Padding),
typeof(Thickness),
typeof(SkiaEditor),
new Thickness(12),
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
///
/// Bindable property for IsReadOnly.
///
public static readonly BindableProperty IsReadOnlyProperty =
BindableProperty.Create(
nameof(IsReadOnly),
typeof(bool),
typeof(SkiaEditor),
false,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
///
/// Bindable property for MaxLength.
///
public static readonly BindableProperty MaxLengthProperty =
BindableProperty.Create(
nameof(MaxLength),
typeof(int),
typeof(SkiaEditor),
-1,
BindingMode.TwoWay);
///
/// Bindable property for AutoSize.
///
public static readonly BindableProperty AutoSizeProperty =
BindableProperty.Create(
nameof(AutoSize),
typeof(bool),
typeof(SkiaEditor),
false,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
///
/// Bindable property for FontAttributes.
///
public static readonly BindableProperty FontAttributesProperty =
BindableProperty.Create(
nameof(FontAttributes),
typeof(FontAttributes),
typeof(SkiaEditor),
FontAttributes.None,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
///
/// Bindable property for CharacterSpacing.
///
public static readonly BindableProperty CharacterSpacingProperty =
BindableProperty.Create(
nameof(CharacterSpacing),
typeof(double),
typeof(SkiaEditor),
0.0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
///
/// Bindable property for IsTextPredictionEnabled.
///
public static readonly BindableProperty IsTextPredictionEnabledProperty =
BindableProperty.Create(
nameof(IsTextPredictionEnabled),
typeof(bool),
typeof(SkiaEditor),
true);
///
/// Bindable property for IsSpellCheckEnabled.
///
public static readonly BindableProperty IsSpellCheckEnabledProperty =
BindableProperty.Create(
nameof(IsSpellCheckEnabled),
typeof(bool),
typeof(SkiaEditor),
true);
///
/// Bindable property for SelectionLength.
///
public static readonly BindableProperty SelectionLengthProperty =
BindableProperty.Create(
nameof(SelectionLength),
typeof(int),
typeof(SkiaEditor),
0,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
///
/// Bindable property for CursorPosition.
///
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));
///
/// Bindable property for HorizontalTextAlignment.
///
public static readonly BindableProperty HorizontalTextAlignmentProperty =
BindableProperty.Create(
nameof(HorizontalTextAlignment),
typeof(TextAlignment),
typeof(SkiaEditor),
TextAlignment.Start,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
///
/// Bindable property for VerticalTextAlignment.
///
public static readonly BindableProperty VerticalTextAlignmentProperty =
BindableProperty.Create(
nameof(VerticalTextAlignment),
typeof(TextAlignment),
typeof(SkiaEditor),
TextAlignment.Start,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
///
/// Bindable property for background color exposed for MAUI binding.
///
public static readonly BindableProperty EditorBackgroundColorProperty =
BindableProperty.Create(
nameof(EditorBackgroundColor),
typeof(Color),
typeof(SkiaEditor),
Colors.Transparent,
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
#endregion
#region Color Conversion Helper
///
/// Converts a MAUI Color to SkiaSharp SKColor.
///
private static SKColor ToSKColor(Color? color) => TextRenderingHelper.ToSKColor(color, SKColors.Transparent);
///
/// Gets the effective text color (platform default black if null).
///
private SKColor GetEffectiveTextColor()
{
return TextColor != null ? ToSKColor(TextColor) : SkiaTheme.TextPrimarySK;
}
///
/// Gets the effective placeholder color (platform default gray if null).
///
private SKColor GetEffectivePlaceholderColor()
{
return PlaceholderColor != null ? ToSKColor(PlaceholderColor) : SkiaTheme.TextPlaceholderSK;
}
///
/// Gets the effective font family (platform default "Sans" if empty).
///
private string GetEffectiveFontFamily() => TextRenderingHelper.GetEffectiveFontFamily(FontFamily);
///
/// Determines if text should be rendered right-to-left based on FlowDirection.
///
private bool IsRightToLeft()
{
return FlowDirection == FlowDirection.RightToLeft;
}
///
/// Gets the horizontal alignment accounting for FlowDirection.
///
private float GetEffectiveTextX(SKRect contentBounds, float textWidth)
{
bool isRtl = IsRightToLeft();
return HorizontalTextAlignment switch
{
TextAlignment.Start => isRtl ? contentBounds.Right - textWidth : contentBounds.Left,
TextAlignment.Center => contentBounds.MidX - textWidth / 2,
TextAlignment.End => isRtl ? contentBounds.Left : contentBounds.Right - textWidth,
_ => isRtl ? contentBounds.Right - textWidth : contentBounds.Left
};
}
#endregion
#region Properties
///
/// Gets or sets the text content.
///
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
///
/// Gets or sets the placeholder text.
///
public string Placeholder
{
get => (string)GetValue(PlaceholderProperty);
set => SetValue(PlaceholderProperty, value);
}
///
/// Gets or sets the text color. Null means platform default (black).
///
public Color? TextColor
{
get => (Color?)GetValue(TextColorProperty);
set => SetValue(TextColorProperty, value);
}
///
/// Gets or sets the placeholder color. Null means platform default (gray).
///
public Color? PlaceholderColor
{
get => (Color?)GetValue(PlaceholderColorProperty);
set => SetValue(PlaceholderColorProperty, value);
}
///
/// Gets or sets the border color.
///
public Color BorderColor
{
get => (Color)GetValue(BorderColorProperty);
set => SetValue(BorderColorProperty, value);
}
///
/// Gets or sets the selection color.
///
public Color SelectionColor
{
get => (Color)GetValue(SelectionColorProperty);
set => SetValue(SelectionColorProperty, value);
}
///
/// Gets or sets the cursor color.
///
public Color CursorColor
{
get => (Color)GetValue(CursorColorProperty);
set => SetValue(CursorColorProperty, value);
}
///
/// Gets or sets the font family.
///
public string FontFamily
{
get => (string)GetValue(FontFamilyProperty);
set => SetValue(FontFamilyProperty, value);
}
///
/// Gets or sets the font size.
///
public double FontSize
{
get => (double)GetValue(FontSizeProperty);
set => SetValue(FontSizeProperty, value);
}
///
/// Gets or sets the line height multiplier.
///
public double LineHeight
{
get => (double)GetValue(LineHeightProperty);
set => SetValue(LineHeightProperty, value);
}
///
/// Gets or sets the corner radius.
///
public double CornerRadius
{
get => (double)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
///
/// Gets or sets the padding.
///
public new Thickness Padding
{
get => (Thickness)GetValue(PaddingProperty);
set => SetValue(PaddingProperty, value);
}
///
/// Gets or sets whether the editor is read-only.
///
public bool IsReadOnly
{
get => (bool)GetValue(IsReadOnlyProperty);
set => SetValue(IsReadOnlyProperty, value);
}
///
/// Gets or sets the maximum length. -1 for unlimited.
///
public int MaxLength
{
get => (int)GetValue(MaxLengthProperty);
set => SetValue(MaxLengthProperty, value);
}
///
/// Gets or sets whether the editor auto-sizes to content.
///
public bool AutoSize
{
get => (bool)GetValue(AutoSizeProperty);
set => SetValue(AutoSizeProperty, value);
}
///
/// Gets or sets the font attributes (Bold, Italic, etc.).
///
public FontAttributes FontAttributes
{
get => (FontAttributes)GetValue(FontAttributesProperty);
set => SetValue(FontAttributesProperty, value);
}
///
/// Gets or sets the character spacing.
///
public double CharacterSpacing
{
get => (double)GetValue(CharacterSpacingProperty);
set => SetValue(CharacterSpacingProperty, value);
}
///
/// Gets or sets whether text prediction is enabled.
///
public bool IsTextPredictionEnabled
{
get => (bool)GetValue(IsTextPredictionEnabledProperty);
set => SetValue(IsTextPredictionEnabledProperty, value);
}
///
/// Gets or sets whether spell check is enabled.
///
public bool IsSpellCheckEnabled
{
get => (bool)GetValue(IsSpellCheckEnabledProperty);
set => SetValue(IsSpellCheckEnabledProperty, value);
}
///
/// Gets or sets the cursor position.
///
public int CursorPosition
{
get => _cursorPosition;
set
{
var newValue = Math.Clamp(value, 0, (Text ?? "").Length);
if (_cursorPosition != newValue)
{
_cursorPosition = newValue;
SetValue(CursorPositionProperty, newValue);
EnsureCursorVisible();
Invalidate();
}
}
}
///
/// Gets or sets the selection length.
///
public int SelectionLength
{
get => _selectionLength;
set
{
if (_selectionLength != value)
{
_selectionLength = value;
SetValue(SelectionLengthProperty, value);
Invalidate();
}
}
}
///
/// Gets or sets the horizontal text alignment.
///
public TextAlignment HorizontalTextAlignment
{
get => (TextAlignment)GetValue(HorizontalTextAlignmentProperty);
set => SetValue(HorizontalTextAlignmentProperty, value);
}
///
/// Gets or sets the vertical text alignment.
///
public TextAlignment VerticalTextAlignment
{
get => (TextAlignment)GetValue(VerticalTextAlignmentProperty);
set => SetValue(VerticalTextAlignmentProperty, value);
}
///
/// Gets or sets the editor background color (MAUI-exposed property).
///
public Color EditorBackgroundColor
{
get => (Color)GetValue(EditorBackgroundColorProperty);
set => SetValue(EditorBackgroundColorProperty, value);
}
#endregion
private void OnCursorPositionPropertyChanged(int newValue)
{
var clampedValue = Math.Clamp(newValue, 0, (Text ?? "").Length);
if (_cursorPosition != clampedValue)
{
_cursorPosition = clampedValue;
EnsureCursorVisible();
Invalidate();
}
}
private int _cursorPosition;
private int _selectionStart = -1;
private int _selectionLength;
private float _scrollOffsetY;
private bool _cursorVisible = true;
private DateTime _lastCursorBlink = DateTime.Now;
private List _lines = new() { "" };
private float _wrapWidth = 0; // Available width for word wrapping
private bool _isSelecting; // For mouse-based text selection
private DateTime _lastClickTime = DateTime.MinValue;
private float _lastClickX;
private float _lastClickY;
private const double DoubleClickThresholdMs = 400;
// IME (Input Method Editor) support
private string _preEditText = string.Empty;
private int _preEditCursorPosition;
private IInputMethodService? _inputMethodService;
///
/// Event raised when text changes.
///
public event EventHandler? TextChanged;
///
/// Event raised when editing is completed.
///
public event EventHandler? Completed;
public SkiaEditor()
{
IsFocusable = true;
// Get IME service from factory
_inputMethodService = InputMethodServiceFactory.Instance;
}
private void OnTextPropertyChanged(string oldText, string newText)
{
var text = newText ?? "";
if (MaxLength > 0 && text.Length > MaxLength)
{
text = text.Substring(0, MaxLength);
SetValue(TextProperty, text);
return;
}
UpdateLines();
_cursorPosition = Math.Min(_cursorPosition, text.Length);
_scrollOffsetY = 0; // Reset scroll when text changes externally
_selectionLength = 0;
TextChanged?.Invoke(this, EventArgs.Empty);
Invalidate();
}
}