// 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 text entry control with full XAML styling and data binding support. /// Implements IInputContext for IME (Input Method Editor) support. /// public partial class SkiaEntry : SkiaView, IInputContext { #region BindableProperties /// /// Bindable property for Text. /// public static readonly BindableProperty TextProperty = BindableProperty.Create( nameof(Text), typeof(string), typeof(SkiaEntry), "", BindingMode.OneWay, propertyChanged: (b, o, n) => ((SkiaEntry)b).OnTextPropertyChanged((string)o, (string)n)); /// /// Bindable property for Placeholder. /// public static readonly BindableProperty PlaceholderProperty = BindableProperty.Create( nameof(Placeholder), typeof(string), typeof(SkiaEntry), "", propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for PlaceholderColor. /// Default is null to match MAUI Entry.PlaceholderColor (falls back to platform default). /// public static readonly BindableProperty PlaceholderColorProperty = BindableProperty.Create( nameof(PlaceholderColor), typeof(Color), typeof(SkiaEntry), null, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for TextColor. /// Default is null to match MAUI Entry.TextColor (falls back to platform default). /// public static readonly BindableProperty TextColorProperty = BindableProperty.Create( nameof(TextColor), typeof(Color), typeof(SkiaEntry), null, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for EntryBackgroundColor (specific to entry, separate from base BackgroundColor). /// public static readonly BindableProperty EntryBackgroundColorProperty = BindableProperty.Create( nameof(EntryBackgroundColor), typeof(Color), typeof(SkiaEntry), Colors.Transparent, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for BorderColor. /// public static readonly BindableProperty BorderColorProperty = BindableProperty.Create( nameof(BorderColor), typeof(Color), typeof(SkiaEntry), Color.FromRgb(0xBD, 0xBD, 0xBD), propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for FocusedBorderColor. /// public static readonly BindableProperty FocusedBorderColorProperty = BindableProperty.Create( nameof(FocusedBorderColor), typeof(Color), typeof(SkiaEntry), Color.FromRgb(0x21, 0x96, 0xF3), propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for SelectionColor. /// public static readonly BindableProperty SelectionColorProperty = BindableProperty.Create( nameof(SelectionColor), typeof(Color), typeof(SkiaEntry), Color.FromRgba(0x21, 0x96, 0xF3, 0x80), propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for CursorColor. /// public static readonly BindableProperty CursorColorProperty = BindableProperty.Create( nameof(CursorColor), typeof(Color), typeof(SkiaEntry), Color.FromRgb(0x21, 0x96, 0xF3), propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for FontFamily. /// Default is empty string to match MAUI Entry.FontFamily (falls back to platform default). /// public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create( nameof(FontFamily), typeof(string), typeof(SkiaEntry), string.Empty, propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); /// /// Bindable property for FontSize. /// public static readonly BindableProperty FontSizeProperty = BindableProperty.Create( nameof(FontSize), typeof(double), typeof(SkiaEntry), 14.0, propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); /// /// Bindable property for CornerRadius. /// public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create( nameof(CornerRadius), typeof(double), typeof(SkiaEntry), 4.0, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for BorderWidth. /// public static readonly BindableProperty BorderWidthProperty = BindableProperty.Create( nameof(BorderWidth), typeof(double), typeof(SkiaEntry), 1.0, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for Padding. /// public static readonly BindableProperty PaddingProperty = BindableProperty.Create( nameof(Padding), typeof(Thickness), typeof(SkiaEntry), new Thickness(12, 8), propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); /// /// Bindable property for IsPassword. /// public static readonly BindableProperty IsPasswordProperty = BindableProperty.Create( nameof(IsPassword), typeof(bool), typeof(SkiaEntry), false, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for PasswordChar. /// public static readonly BindableProperty PasswordCharProperty = BindableProperty.Create( nameof(PasswordChar), typeof(char), typeof(SkiaEntry), '*', // Use asterisk for universal font compatibility propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for MaxLength. /// public static readonly BindableProperty MaxLengthProperty = BindableProperty.Create( nameof(MaxLength), typeof(int), typeof(SkiaEntry), 0); /// /// Bindable property for SelectAllOnDoubleClick. /// When true, double-clicking selects all text instead of just the word. /// public static readonly BindableProperty SelectAllOnDoubleClickProperty = BindableProperty.Create( nameof(SelectAllOnDoubleClick), typeof(bool), typeof(SkiaEntry), false); /// /// Bindable property for IsReadOnly. /// public static readonly BindableProperty IsReadOnlyProperty = BindableProperty.Create( nameof(IsReadOnly), typeof(bool), typeof(SkiaEntry), false, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for HorizontalTextAlignment. /// public static readonly BindableProperty HorizontalTextAlignmentProperty = BindableProperty.Create( nameof(HorizontalTextAlignment), typeof(TextAlignment), typeof(SkiaEntry), TextAlignment.Start, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for VerticalTextAlignment. /// Default is Start to match MAUI Entry.VerticalTextAlignment. /// public static readonly BindableProperty VerticalTextAlignmentProperty = BindableProperty.Create( nameof(VerticalTextAlignment), typeof(TextAlignment), typeof(SkiaEntry), TextAlignment.Start, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for ShowClearButton. /// public static readonly BindableProperty ShowClearButtonProperty = BindableProperty.Create( nameof(ShowClearButton), typeof(bool), typeof(SkiaEntry), false, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for CharacterSpacing. /// public static readonly BindableProperty CharacterSpacingProperty = BindableProperty.Create( nameof(CharacterSpacing), typeof(double), typeof(SkiaEntry), 0.0, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for FontAttributes. /// public static readonly BindableProperty FontAttributesProperty = BindableProperty.Create( nameof(FontAttributes), typeof(FontAttributes), typeof(SkiaEntry), FontAttributes.None, propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); /// /// Bindable property for ReturnType. /// public static readonly BindableProperty ReturnTypeProperty = BindableProperty.Create( nameof(ReturnType), typeof(ReturnType), typeof(SkiaEntry), ReturnType.Default); /// /// Bindable property for ReturnCommand. /// public static readonly BindableProperty ReturnCommandProperty = BindableProperty.Create( nameof(ReturnCommand), typeof(System.Windows.Input.ICommand), typeof(SkiaEntry), null); /// /// Bindable property for ReturnCommandParameter. /// public static readonly BindableProperty ReturnCommandParameterProperty = BindableProperty.Create( nameof(ReturnCommandParameter), typeof(object), typeof(SkiaEntry), null); /// /// Bindable property for Keyboard. /// public static readonly BindableProperty KeyboardProperty = BindableProperty.Create( nameof(Keyboard), typeof(Keyboard), typeof(SkiaEntry), Keyboard.Default); /// /// Bindable property for ClearButtonVisibility. /// public static readonly BindableProperty ClearButtonVisibilityProperty = BindableProperty.Create( nameof(ClearButtonVisibility), typeof(ClearButtonVisibility), typeof(SkiaEntry), ClearButtonVisibility.Never, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for IsTextPredictionEnabled. /// public static readonly BindableProperty IsTextPredictionEnabledProperty = BindableProperty.Create( nameof(IsTextPredictionEnabled), typeof(bool), typeof(SkiaEntry), true); /// /// Bindable property for IsSpellCheckEnabled. /// public static readonly BindableProperty IsSpellCheckEnabledProperty = BindableProperty.Create( nameof(IsSpellCheckEnabled), typeof(bool), typeof(SkiaEntry), true); #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 placeholder color. Null means platform default (gray). /// public Color? PlaceholderColor { get => (Color?)GetValue(PlaceholderColorProperty); set => SetValue(PlaceholderColorProperty, 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 entry background color. /// public Color EntryBackgroundColor { get => (Color)GetValue(EntryBackgroundColorProperty); set => SetValue(EntryBackgroundColorProperty, value); } /// /// Gets or sets the border color. /// public Color BorderColor { get => (Color)GetValue(BorderColorProperty); set => SetValue(BorderColorProperty, value); } /// /// Gets or sets the focused border color. /// public Color FocusedBorderColor { get => (Color)GetValue(FocusedBorderColorProperty); set => SetValue(FocusedBorderColorProperty, 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 corner radius. /// public double CornerRadius { get => (double)GetValue(CornerRadiusProperty); set => SetValue(CornerRadiusProperty, value); } /// /// Gets or sets the border width. /// public double BorderWidth { get => (double)GetValue(BorderWidthProperty); set => SetValue(BorderWidthProperty, value); } /// /// Gets or sets the padding. /// public Thickness Padding { get => (Thickness)GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); } /// /// Gets or sets whether this is a password field. /// public bool IsPassword { get => (bool)GetValue(IsPasswordProperty); set => SetValue(IsPasswordProperty, value); } /// /// Gets or sets the password masking character. /// public char PasswordChar { get => (char)GetValue(PasswordCharProperty); set => SetValue(PasswordCharProperty, value); } /// /// Gets or sets the maximum text length. 0 = unlimited. /// public int MaxLength { get => (int)GetValue(MaxLengthProperty); set => SetValue(MaxLengthProperty, value); } /// /// Gets or sets whether double-clicking selects all text instead of just the word. /// Useful for URL bars and similar inputs. /// public bool SelectAllOnDoubleClick { get => (bool)GetValue(SelectAllOnDoubleClickProperty); set => SetValue(SelectAllOnDoubleClickProperty, value); } /// /// Gets or sets whether the entry is read-only. /// public bool IsReadOnly { get => (bool)GetValue(IsReadOnlyProperty); set => SetValue(IsReadOnlyProperty, value); } /// /// 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 whether to show the clear button. /// public bool ShowClearButton { get => (bool)GetValue(ShowClearButtonProperty); set => SetValue(ShowClearButtonProperty, value); } /// /// Gets or sets the character spacing. /// public double CharacterSpacing { get => (double)GetValue(CharacterSpacingProperty); set => SetValue(CharacterSpacingProperty, value); } /// /// Gets or sets the font attributes (bold, italic). /// public FontAttributes FontAttributes { get => (FontAttributes)GetValue(FontAttributesProperty); set => SetValue(FontAttributesProperty, value); } /// /// Gets or sets the return key type for the soft keyboard. /// public ReturnType ReturnType { get => (ReturnType)GetValue(ReturnTypeProperty); set => SetValue(ReturnTypeProperty, value); } /// /// Gets or sets the command to execute when the return key is pressed. /// public System.Windows.Input.ICommand? ReturnCommand { get => (System.Windows.Input.ICommand?)GetValue(ReturnCommandProperty); set => SetValue(ReturnCommandProperty, value); } /// /// Gets or sets the parameter for the return command. /// public object? ReturnCommandParameter { get => GetValue(ReturnCommandParameterProperty); set => SetValue(ReturnCommandParameterProperty, value); } /// /// Gets or sets the keyboard type for this entry. /// public Keyboard Keyboard { get => (Keyboard)GetValue(KeyboardProperty); set => SetValue(KeyboardProperty, value); } /// /// Gets or sets when the clear button is visible. /// public ClearButtonVisibility ClearButtonVisibility { get => (ClearButtonVisibility)GetValue(ClearButtonVisibilityProperty); set => SetValue(ClearButtonVisibilityProperty, value); } /// /// Gets or sets the cursor position. /// public int CursorPosition { get => _cursorPosition; set { _cursorPosition = Math.Clamp(value, 0, Text.Length); ResetCursorBlink(); Invalidate(); } } /// /// Gets or sets the selection length. /// public int SelectionLength { get => _selectionLength; set { _selectionLength = value; Invalidate(); } } /// /// Gets or sets whether text prediction is enabled. /// Note: This is a hint to the input system; actual behavior depends on platform support. /// public bool IsTextPredictionEnabled { get => (bool)GetValue(IsTextPredictionEnabledProperty); set => SetValue(IsTextPredictionEnabledProperty, value); } /// /// Gets or sets whether spell checking is enabled. /// Note: This is a hint to the input system; actual behavior depends on platform support. /// public bool IsSpellCheckEnabled { get => (bool)GetValue(IsSpellCheckEnabledProperty); set => SetValue(IsSpellCheckEnabledProperty, value); } #endregion private int _cursorPosition; private int _selectionStart; private int _selectionLength; private float _scrollOffset; private DateTime _cursorBlinkTime = DateTime.UtcNow; private bool _cursorVisible = true; private bool _isSelecting; // For mouse-based text selection private DateTime _lastClickTime = DateTime.MinValue; private float _lastClickX; 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 Enter is pressed. /// public event EventHandler? Completed; public SkiaEntry() { IsFocusable = true; // Get IME service from factory _inputMethodService = InputMethodServiceFactory.Instance; } /// /// Converts a MAUI Color to SkiaSharp SKColor for rendering. /// private static SKColor ToSKColor(Color? color) { if (color == null) return SKColors.Transparent; return color.ToSKColor(); } /// /// 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.TextDisabledSK; } /// /// Gets the effective font family (platform default "Sans" if empty). /// private string GetEffectiveFontFamily() { return string.IsNullOrEmpty(FontFamily) ? "Sans" : 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 - _scrollOffset : contentBounds.Left - _scrollOffset, TextAlignment.Center => contentBounds.MidX - textWidth / 2, TextAlignment.End => isRtl ? contentBounds.Left - _scrollOffset : contentBounds.Right - textWidth - _scrollOffset, _ => isRtl ? contentBounds.Right - textWidth - _scrollOffset : contentBounds.Left - _scrollOffset }; } private void OnTextPropertyChanged(string oldText, string newText) { _cursorPosition = Math.Min(_cursorPosition, (newText ?? "").Length); _scrollOffset = 0; // Reset scroll when text changes externally _selectionLength = 0; TextChanged?.Invoke(this, new TextChangedEventArgs(oldText, newText ?? "")); Invalidate(); } private string GetDisplayText() { if (IsPassword && !string.IsNullOrEmpty(Text)) { return new string(PasswordChar, Text.Length); } return Text; } private SKFontStyle GetFontStyle() => TextRenderingHelper.GetFontStyle(FontAttributes); } /// /// Event args for text changed events. /// public class TextChangedEventArgs : EventArgs { public string OldTextValue { get; } public string NewTextValue { get; } public TextChangedEventArgs(string oldText, string newText) { OldTextValue = oldText; NewTextValue = newText; } }