Files

802 lines
25 KiB
C#
Raw Permalink Normal View History

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
2026-01-16 04:40:02 +00:00
using System;
using System.Collections.Generic;
2026-01-16 04:40:02 +00:00
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
2026-01-17 15:19:03 +00:00
using Microsoft.Maui.Platform.Linux;
using Microsoft.Maui.Platform.Linux.Rendering;
using Microsoft.Maui.Platform.Linux.Services;
2026-01-16 04:40:02 +00:00
using SkiaSharp;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Skia-rendered text entry control with full XAML styling and data binding support.
2026-01-17 02:23:05 +00:00
/// Implements IInputContext for IME (Input Method Editor) support.
/// </summary>
public partial class SkiaEntry : SkiaView, IInputContext
{
#region BindableProperties
/// <summary>
/// Bindable property for Text.
/// </summary>
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));
/// <summary>
/// Bindable property for Placeholder.
/// </summary>
public static readonly BindableProperty PlaceholderProperty =
BindableProperty.Create(
nameof(Placeholder),
typeof(string),
typeof(SkiaEntry),
"",
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for PlaceholderColor.
2026-01-17 01:43:42 +00:00
/// Default is null to match MAUI Entry.PlaceholderColor (falls back to platform default).
/// </summary>
public static readonly BindableProperty PlaceholderColorProperty =
BindableProperty.Create(
nameof(PlaceholderColor),
2026-01-16 04:40:02 +00:00
typeof(Color),
typeof(SkiaEntry),
2026-01-17 01:43:42 +00:00
null,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for TextColor.
2026-01-17 01:43:42 +00:00
/// Default is null to match MAUI Entry.TextColor (falls back to platform default).
/// </summary>
public static readonly BindableProperty TextColorProperty =
BindableProperty.Create(
nameof(TextColor),
2026-01-16 04:40:02 +00:00
typeof(Color),
typeof(SkiaEntry),
2026-01-17 01:43:42 +00:00
null,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
2026-01-16 04:40:02 +00:00
/// Bindable property for EntryBackgroundColor (specific to entry, separate from base BackgroundColor).
/// </summary>
public static readonly BindableProperty EntryBackgroundColorProperty =
BindableProperty.Create(
nameof(EntryBackgroundColor),
2026-01-16 04:40:02 +00:00
typeof(Color),
typeof(SkiaEntry),
2026-01-17 08:06:22 +00:00
Colors.Transparent,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for BorderColor.
/// </summary>
public static readonly BindableProperty BorderColorProperty =
BindableProperty.Create(
nameof(BorderColor),
2026-01-16 04:40:02 +00:00
typeof(Color),
typeof(SkiaEntry),
2026-01-16 04:40:02 +00:00
Color.FromRgb(0xBD, 0xBD, 0xBD),
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for FocusedBorderColor.
/// </summary>
public static readonly BindableProperty FocusedBorderColorProperty =
BindableProperty.Create(
nameof(FocusedBorderColor),
2026-01-16 04:40:02 +00:00
typeof(Color),
typeof(SkiaEntry),
2026-01-16 04:40:02 +00:00
Color.FromRgb(0x21, 0x96, 0xF3),
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for SelectionColor.
/// </summary>
public static readonly BindableProperty SelectionColorProperty =
BindableProperty.Create(
nameof(SelectionColor),
2026-01-16 04:40:02 +00:00
typeof(Color),
typeof(SkiaEntry),
2026-01-16 04:40:02 +00:00
Color.FromRgba(0x21, 0x96, 0xF3, 0x80),
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for CursorColor.
/// </summary>
public static readonly BindableProperty CursorColorProperty =
BindableProperty.Create(
nameof(CursorColor),
2026-01-16 04:40:02 +00:00
typeof(Color),
typeof(SkiaEntry),
2026-01-16 04:40:02 +00:00
Color.FromRgb(0x21, 0x96, 0xF3),
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for FontFamily.
2026-01-17 01:43:42 +00:00
/// Default is empty string to match MAUI Entry.FontFamily (falls back to platform default).
/// </summary>
public static readonly BindableProperty FontFamilyProperty =
BindableProperty.Create(
nameof(FontFamily),
typeof(string),
typeof(SkiaEntry),
2026-01-17 01:43:42 +00:00
string.Empty,
propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure());
/// <summary>
/// Bindable property for FontSize.
/// </summary>
public static readonly BindableProperty FontSizeProperty =
BindableProperty.Create(
nameof(FontSize),
2026-01-16 04:40:02 +00:00
typeof(double),
typeof(SkiaEntry),
2026-01-16 04:40:02 +00:00
14.0,
propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure());
/// <summary>
/// Bindable property for CornerRadius.
/// </summary>
public static readonly BindableProperty CornerRadiusProperty =
BindableProperty.Create(
nameof(CornerRadius),
2026-01-16 04:40:02 +00:00
typeof(double),
typeof(SkiaEntry),
2026-01-16 04:40:02 +00:00
4.0,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for BorderWidth.
/// </summary>
public static readonly BindableProperty BorderWidthProperty =
BindableProperty.Create(
nameof(BorderWidth),
2026-01-16 04:40:02 +00:00
typeof(double),
typeof(SkiaEntry),
2026-01-16 04:40:02 +00:00
1.0,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for Padding.
/// </summary>
public static readonly BindableProperty PaddingProperty =
BindableProperty.Create(
nameof(Padding),
2026-01-16 04:40:02 +00:00
typeof(Thickness),
typeof(SkiaEntry),
2026-01-16 04:40:02 +00:00
new Thickness(12, 8),
propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure());
/// <summary>
/// Bindable property for IsPassword.
/// </summary>
public static readonly BindableProperty IsPasswordProperty =
BindableProperty.Create(
nameof(IsPassword),
typeof(bool),
typeof(SkiaEntry),
false,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for PasswordChar.
/// </summary>
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());
/// <summary>
/// Bindable property for MaxLength.
/// </summary>
public static readonly BindableProperty MaxLengthProperty =
BindableProperty.Create(
nameof(MaxLength),
typeof(int),
typeof(SkiaEntry),
0);
2026-01-17 08:51:13 +00:00
/// <summary>
/// Bindable property for SelectAllOnDoubleClick.
/// When true, double-clicking selects all text instead of just the word.
/// </summary>
public static readonly BindableProperty SelectAllOnDoubleClickProperty =
BindableProperty.Create(
nameof(SelectAllOnDoubleClick),
typeof(bool),
typeof(SkiaEntry),
false);
/// <summary>
/// Bindable property for IsReadOnly.
/// </summary>
public static readonly BindableProperty IsReadOnlyProperty =
BindableProperty.Create(
nameof(IsReadOnly),
typeof(bool),
typeof(SkiaEntry),
false,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for HorizontalTextAlignment.
/// </summary>
public static readonly BindableProperty HorizontalTextAlignmentProperty =
BindableProperty.Create(
nameof(HorizontalTextAlignment),
typeof(TextAlignment),
typeof(SkiaEntry),
TextAlignment.Start,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for VerticalTextAlignment.
2026-01-17 01:43:42 +00:00
/// Default is Start to match MAUI Entry.VerticalTextAlignment.
/// </summary>
public static readonly BindableProperty VerticalTextAlignmentProperty =
BindableProperty.Create(
nameof(VerticalTextAlignment),
typeof(TextAlignment),
typeof(SkiaEntry),
2026-01-17 01:43:42 +00:00
TextAlignment.Start,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for ShowClearButton.
/// </summary>
public static readonly BindableProperty ShowClearButtonProperty =
BindableProperty.Create(
nameof(ShowClearButton),
typeof(bool),
typeof(SkiaEntry),
false,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for CharacterSpacing.
/// </summary>
public static readonly BindableProperty CharacterSpacingProperty =
BindableProperty.Create(
nameof(CharacterSpacing),
2026-01-16 04:40:02 +00:00
typeof(double),
typeof(SkiaEntry),
0.0,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
/// Bindable property for FontAttributes.
/// </summary>
public static readonly BindableProperty FontAttributesProperty =
BindableProperty.Create(
nameof(FontAttributes),
typeof(FontAttributes),
typeof(SkiaEntry),
FontAttributes.None,
propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure());
/// <summary>
/// Bindable property for ReturnType.
/// </summary>
public static readonly BindableProperty ReturnTypeProperty =
BindableProperty.Create(
nameof(ReturnType),
typeof(ReturnType),
typeof(SkiaEntry),
ReturnType.Default);
/// <summary>
/// Bindable property for ReturnCommand.
/// </summary>
public static readonly BindableProperty ReturnCommandProperty =
BindableProperty.Create(
nameof(ReturnCommand),
typeof(System.Windows.Input.ICommand),
typeof(SkiaEntry),
null);
/// <summary>
/// Bindable property for ReturnCommandParameter.
/// </summary>
public static readonly BindableProperty ReturnCommandParameterProperty =
BindableProperty.Create(
nameof(ReturnCommandParameter),
typeof(object),
typeof(SkiaEntry),
null);
/// <summary>
/// Bindable property for Keyboard.
/// </summary>
public static readonly BindableProperty KeyboardProperty =
BindableProperty.Create(
nameof(Keyboard),
typeof(Keyboard),
typeof(SkiaEntry),
Keyboard.Default);
/// <summary>
/// Bindable property for ClearButtonVisibility.
/// </summary>
public static readonly BindableProperty ClearButtonVisibilityProperty =
BindableProperty.Create(
nameof(ClearButtonVisibility),
typeof(ClearButtonVisibility),
typeof(SkiaEntry),
2026-01-16 04:40:02 +00:00
ClearButtonVisibility.Never,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
2026-01-16 04:40:02 +00:00
/// <summary>
/// Bindable property for IsTextPredictionEnabled.
/// </summary>
public static readonly BindableProperty IsTextPredictionEnabledProperty =
BindableProperty.Create(
nameof(IsTextPredictionEnabled),
typeof(bool),
typeof(SkiaEntry),
true);
/// <summary>
/// Bindable property for IsSpellCheckEnabled.
/// </summary>
public static readonly BindableProperty IsSpellCheckEnabledProperty =
BindableProperty.Create(
nameof(IsSpellCheckEnabled),
typeof(bool),
typeof(SkiaEntry),
true);
#endregion
#region Properties
/// <summary>
/// Gets or sets the text content.
/// </summary>
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
/// <summary>
/// Gets or sets the placeholder text.
/// </summary>
public string Placeholder
{
get => (string)GetValue(PlaceholderProperty);
set => SetValue(PlaceholderProperty, value);
}
/// <summary>
2026-01-17 01:43:42 +00:00
/// Gets or sets the placeholder color. Null means platform default (gray).
/// </summary>
2026-01-17 01:43:42 +00:00
public Color? PlaceholderColor
{
2026-01-17 01:43:42 +00:00
get => (Color?)GetValue(PlaceholderColorProperty);
set => SetValue(PlaceholderColorProperty, value);
}
/// <summary>
2026-01-17 01:43:42 +00:00
/// Gets or sets the text color. Null means platform default (black).
/// </summary>
2026-01-17 01:43:42 +00:00
public Color? TextColor
{
2026-01-17 01:43:42 +00:00
get => (Color?)GetValue(TextColorProperty);
set => SetValue(TextColorProperty, value);
}
/// <summary>
/// Gets or sets the entry background color.
/// </summary>
2026-01-16 04:40:02 +00:00
public Color EntryBackgroundColor
{
2026-01-16 04:40:02 +00:00
get => (Color)GetValue(EntryBackgroundColorProperty);
set => SetValue(EntryBackgroundColorProperty, value);
}
/// <summary>
/// Gets or sets the border color.
/// </summary>
2026-01-16 04:40:02 +00:00
public Color BorderColor
{
2026-01-16 04:40:02 +00:00
get => (Color)GetValue(BorderColorProperty);
set => SetValue(BorderColorProperty, value);
}
/// <summary>
/// Gets or sets the focused border color.
/// </summary>
2026-01-16 04:40:02 +00:00
public Color FocusedBorderColor
{
2026-01-16 04:40:02 +00:00
get => (Color)GetValue(FocusedBorderColorProperty);
set => SetValue(FocusedBorderColorProperty, value);
}
/// <summary>
/// Gets or sets the selection color.
/// </summary>
2026-01-16 04:40:02 +00:00
public Color SelectionColor
{
2026-01-16 04:40:02 +00:00
get => (Color)GetValue(SelectionColorProperty);
set => SetValue(SelectionColorProperty, value);
}
/// <summary>
/// Gets or sets the cursor color.
/// </summary>
2026-01-16 04:40:02 +00:00
public Color CursorColor
{
2026-01-16 04:40:02 +00:00
get => (Color)GetValue(CursorColorProperty);
set => SetValue(CursorColorProperty, value);
}
/// <summary>
/// Gets or sets the font family.
/// </summary>
public string FontFamily
{
get => (string)GetValue(FontFamilyProperty);
set => SetValue(FontFamilyProperty, value);
}
/// <summary>
/// Gets or sets the font size.
/// </summary>
2026-01-16 04:40:02 +00:00
public double FontSize
{
2026-01-16 04:40:02 +00:00
get => (double)GetValue(FontSizeProperty);
set => SetValue(FontSizeProperty, value);
}
/// <summary>
/// Gets or sets the corner radius.
/// </summary>
2026-01-16 04:40:02 +00:00
public double CornerRadius
{
2026-01-16 04:40:02 +00:00
get => (double)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
/// <summary>
/// Gets or sets the border width.
/// </summary>
2026-01-16 04:40:02 +00:00
public double BorderWidth
{
2026-01-16 04:40:02 +00:00
get => (double)GetValue(BorderWidthProperty);
set => SetValue(BorderWidthProperty, value);
}
/// <summary>
/// Gets or sets the padding.
/// </summary>
2026-01-16 04:40:02 +00:00
public Thickness Padding
{
2026-01-16 04:40:02 +00:00
get => (Thickness)GetValue(PaddingProperty);
set => SetValue(PaddingProperty, value);
}
/// <summary>
/// Gets or sets whether this is a password field.
/// </summary>
public bool IsPassword
{
get => (bool)GetValue(IsPasswordProperty);
set => SetValue(IsPasswordProperty, value);
}
/// <summary>
/// Gets or sets the password masking character.
/// </summary>
public char PasswordChar
{
get => (char)GetValue(PasswordCharProperty);
set => SetValue(PasswordCharProperty, value);
}
/// <summary>
/// Gets or sets the maximum text length. 0 = unlimited.
/// </summary>
public int MaxLength
{
get => (int)GetValue(MaxLengthProperty);
set => SetValue(MaxLengthProperty, value);
}
2026-01-17 08:51:13 +00:00
/// <summary>
/// Gets or sets whether double-clicking selects all text instead of just the word.
/// Useful for URL bars and similar inputs.
/// </summary>
public bool SelectAllOnDoubleClick
{
get => (bool)GetValue(SelectAllOnDoubleClickProperty);
set => SetValue(SelectAllOnDoubleClickProperty, value);
}
/// <summary>
/// Gets or sets whether the entry is read-only.
/// </summary>
public bool IsReadOnly
{
get => (bool)GetValue(IsReadOnlyProperty);
set => SetValue(IsReadOnlyProperty, value);
}
/// <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 whether to show the clear button.
/// </summary>
public bool ShowClearButton
{
get => (bool)GetValue(ShowClearButtonProperty);
set => SetValue(ShowClearButtonProperty, value);
}
/// <summary>
/// Gets or sets the character spacing.
/// </summary>
2026-01-16 04:40:02 +00:00
public double CharacterSpacing
{
2026-01-16 04:40:02 +00:00
get => (double)GetValue(CharacterSpacingProperty);
set => SetValue(CharacterSpacingProperty, value);
}
2026-01-16 04:40:02 +00:00
/// <summary>
/// Gets or sets the font attributes (bold, italic).
/// </summary>
public FontAttributes FontAttributes
{
get => (FontAttributes)GetValue(FontAttributesProperty);
set => SetValue(FontAttributesProperty, value);
}
/// <summary>
/// Gets or sets the return key type for the soft keyboard.
/// </summary>
public ReturnType ReturnType
{
get => (ReturnType)GetValue(ReturnTypeProperty);
set => SetValue(ReturnTypeProperty, value);
}
/// <summary>
/// Gets or sets the command to execute when the return key is pressed.
/// </summary>
public System.Windows.Input.ICommand? ReturnCommand
{
get => (System.Windows.Input.ICommand?)GetValue(ReturnCommandProperty);
set => SetValue(ReturnCommandProperty, value);
}
/// <summary>
/// Gets or sets the parameter for the return command.
/// </summary>
public object? ReturnCommandParameter
{
get => GetValue(ReturnCommandParameterProperty);
set => SetValue(ReturnCommandParameterProperty, value);
}
/// <summary>
/// Gets or sets the keyboard type for this entry.
/// </summary>
public Keyboard Keyboard
{
get => (Keyboard)GetValue(KeyboardProperty);
set => SetValue(KeyboardProperty, value);
}
/// <summary>
/// Gets or sets when the clear button is visible.
/// </summary>
public ClearButtonVisibility ClearButtonVisibility
{
get => (ClearButtonVisibility)GetValue(ClearButtonVisibilityProperty);
set => SetValue(ClearButtonVisibilityProperty, value);
}
/// <summary>
/// Gets or sets the cursor position.
/// </summary>
public int CursorPosition
{
get => _cursorPosition;
set
{
_cursorPosition = Math.Clamp(value, 0, Text.Length);
ResetCursorBlink();
Invalidate();
}
}
/// <summary>
/// Gets or sets the selection length.
/// </summary>
public int SelectionLength
{
get => _selectionLength;
set
{
_selectionLength = value;
Invalidate();
}
}
2026-01-16 04:40:02 +00:00
/// <summary>
/// Gets or sets whether text prediction is enabled.
/// Note: This is a hint to the input system; actual behavior depends on platform support.
/// </summary>
public bool IsTextPredictionEnabled
{
get => (bool)GetValue(IsTextPredictionEnabledProperty);
set => SetValue(IsTextPredictionEnabledProperty, value);
}
/// <summary>
/// Gets or sets whether spell checking is enabled.
/// Note: This is a hint to the input system; actual behavior depends on platform support.
/// </summary>
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;
2026-01-17 02:23:05 +00:00
// IME (Input Method Editor) support
private string _preEditText = string.Empty;
private int _preEditCursorPosition;
private IInputMethodService? _inputMethodService;
/// <summary>
/// Event raised when text changes.
/// </summary>
public event EventHandler<TextChangedEventArgs>? TextChanged;
/// <summary>
/// Event raised when Enter is pressed.
/// </summary>
public event EventHandler? Completed;
public SkiaEntry()
{
IsFocusable = true;
2026-01-17 02:23:05 +00:00
// Get IME service from factory
_inputMethodService = InputMethodServiceFactory.Instance;
}
2026-01-16 04:40:02 +00:00
/// <summary>
/// Converts a MAUI Color to SkiaSharp SKColor for rendering.
/// </summary>
2026-01-17 01:43:42 +00:00
private static SKColor ToSKColor(Color? color)
2026-01-16 04:40:02 +00:00
{
if (color == null) return SKColors.Transparent;
2026-01-17 03:36:37 +00:00
return color.ToSKColor();
2026-01-16 04:40:02 +00:00
}
2026-01-17 01:43:42 +00:00
/// <summary>
/// Gets the effective text color (platform default black if null).
/// </summary>
private SKColor GetEffectiveTextColor()
{
2026-01-17 03:36:37 +00:00
return TextColor != null ? ToSKColor(TextColor) : SkiaTheme.TextPrimarySK;
2026-01-17 01:43:42 +00:00
}
/// <summary>
/// Gets the effective placeholder color (platform default gray if null).
/// </summary>
private SKColor GetEffectivePlaceholderColor()
{
2026-01-17 03:36:37 +00:00
return PlaceholderColor != null ? ToSKColor(PlaceholderColor) : SkiaTheme.TextDisabledSK;
2026-01-17 01:43:42 +00:00
}
/// <summary>
/// Gets the effective font family (platform default "Sans" if empty).
/// </summary>
private string GetEffectiveFontFamily()
{
return string.IsNullOrEmpty(FontFamily) ? "Sans" : FontFamily;
}
2026-01-17 02:23:05 +00:00
/// <summary>
/// Determines if text should be rendered right-to-left based on FlowDirection.
/// </summary>
private bool IsRightToLeft()
{
return FlowDirection == FlowDirection.RightToLeft;
}
/// <summary>
/// Gets the horizontal alignment accounting for FlowDirection.
/// </summary>
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);
}
/// <summary>
/// Event args for text changed events.
/// </summary>
public class TextChangedEventArgs : EventArgs
{
public string OldTextValue { get; }
public string NewTextValue { get; }
public TextChangedEventArgs(string oldText, string newText)
{
OldTextValue = oldText;
NewTextValue = newText;
}
}