diff --git a/demo/Ursa.Demo/Pages/IPv4BoxDemo.axaml b/demo/Ursa.Demo/Pages/IPv4BoxDemo.axaml
new file mode 100644
index 0000000..0d674dd
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/IPv4BoxDemo.axaml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/Ursa.Demo/Pages/IPv4BoxDemo.axaml.cs b/demo/Ursa.Demo/Pages/IPv4BoxDemo.axaml.cs
new file mode 100644
index 0000000..c865da8
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/IPv4BoxDemo.axaml.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Net;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Ursa.Demo.Pages;
+
+public partial class IPv4BoxDemo : UserControl
+{
+ public IPv4BoxDemo()
+ {
+ InitializeComponent();
+ DataContext = new IPv4DemoViewMode();
+ }
+}
+
+public partial class IPv4DemoViewMode: ObservableObject
+{
+ [ObservableProperty]
+ private IPAddress? _address;
+
+ public void ChangeAddress()
+ {
+ long l = Random.Shared.NextInt64(0x00000000FFFFFFFF);
+ Address = new IPAddress(l);
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/Ursa.Demo.csproj b/demo/Ursa.Demo/Ursa.Demo.csproj
index 272045d..101380b 100644
--- a/demo/Ursa.Demo/Ursa.Demo.csproj
+++ b/demo/Ursa.Demo/Ursa.Demo.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/demo/Ursa.Demo/Views/MainWindow.axaml b/demo/Ursa.Demo/Views/MainWindow.axaml
index a5c4284..613691a 100644
--- a/demo/Ursa.Demo/Views/MainWindow.axaml
+++ b/demo/Ursa.Demo/Views/MainWindow.axaml
@@ -29,6 +29,9 @@
+
+
+
diff --git a/src/Ursa.Themes.Semi/Controls/IPv4Box.axaml b/src/Ursa.Themes.Semi/Controls/IPv4Box.axaml
new file mode 100644
index 0000000..4a55d21
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Controls/IPv4Box.axaml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml
index 8b9ac73..f0db1d0 100644
--- a/src/Ursa.Themes.Semi/Controls/_index.axaml
+++ b/src/Ursa.Themes.Semi/Controls/_index.axaml
@@ -3,5 +3,6 @@
+
diff --git a/src/Ursa.Themes.Semi/Themes/Dark/IPv4Box.axaml b/src/Ursa.Themes.Semi/Themes/Dark/IPv4Box.axaml
new file mode 100644
index 0000000..b567dd0
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Themes/Dark/IPv4Box.axaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml b/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml
index 8b9ac73..f0db1d0 100644
--- a/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml
+++ b/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml
@@ -3,5 +3,6 @@
+
diff --git a/src/Ursa.Themes.Semi/Themes/Light/IPv4Box.axaml b/src/Ursa.Themes.Semi/Themes/Light/IPv4Box.axaml
new file mode 100644
index 0000000..5c3fb6b
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Themes/Light/IPv4Box.axaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml
index 8b9ac73..f0db1d0 100644
--- a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml
+++ b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml
@@ -3,5 +3,6 @@
+
diff --git a/src/Ursa.Themes.Semi/Themes/Shared/IPv4Box.axaml b/src/Ursa.Themes.Semi/Themes/Shared/IPv4Box.axaml
new file mode 100644
index 0000000..3f808ba
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Themes/Shared/IPv4Box.axaml
@@ -0,0 +1,8 @@
+
+
+ 32
+ 24
+ 40
+ 1
+ 3
+
diff --git a/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml b/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml
index 8b9ac73..f0db1d0 100644
--- a/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml
+++ b/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml
@@ -3,5 +3,6 @@
+
diff --git a/src/Ursa/Controls/IPv4Box.cs b/src/Ursa/Controls/IPv4Box.cs
new file mode 100644
index 0000000..f70b8eb
--- /dev/null
+++ b/src/Ursa/Controls/IPv4Box.cs
@@ -0,0 +1,580 @@
+using System.Diagnostics;
+using System.Net;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Input.Platform;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
+// ReSharper disable InconsistentNaming
+
+namespace Ursa.Controls;
+
+public enum IPv4BoxInputMode
+{
+ Normal,
+ // In fast mode, automatically move to next session after 3 digits input.
+ Fast,
+}
+
+[TemplatePart(PART_FirstTextPresenter, typeof(TextPresenter))]
+[TemplatePart(PART_SecondTextPresenter, typeof(TextPresenter))]
+[TemplatePart(PART_ThirdTextPresenter, typeof(TextPresenter))]
+[TemplatePart(PART_FourthTextPresenter, typeof(TextPresenter))]
+public class IPv4Box: TemplatedControl
+{
+ public const string PART_FirstTextPresenter = "PART_FirstTextPresenter";
+ public const string PART_SecondTextPresenter = "PART_SecondTextPresenter";
+ public const string PART_ThirdTextPresenter = "PART_ThirdTextPresenter";
+ public const string PART_FourthTextPresenter = "PART_FourthTextPresenter";
+ private TextPresenter? _firstText;
+ private TextPresenter? _secondText;
+ private TextPresenter? _thirdText;
+ private TextPresenter? _fourthText;
+ private byte? _firstByte;
+ private byte? _secondByte;
+ private byte? _thirdByte;
+ private byte? _fourthByte;
+ private readonly TextPresenter?[] _presenters = new TextPresenter?[4];
+ private TextPresenter? _currentActivePresenter;
+
+ public static readonly StyledProperty IPAddressProperty = AvaloniaProperty.Register(
+ nameof(IPAddress));
+ public IPAddress? IPAddress
+ {
+ get => GetValue(IPAddressProperty);
+ set => SetValue(IPAddressProperty, value);
+ }
+
+ public static readonly StyledProperty TextAlignmentProperty =
+ TextBox.TextAlignmentProperty.AddOwner();
+ public TextAlignment TextAlignment
+ {
+ get => GetValue(TextAlignmentProperty);
+ set => SetValue(TextAlignmentProperty, value);
+ }
+
+ public static readonly StyledProperty SelectionBrushProperty =
+ TextBox.SelectionBrushProperty.AddOwner();
+ public IBrush? SelectionBrush
+ {
+ get => GetValue(SelectionBrushProperty);
+ set => SetValue(SelectionBrushProperty, value);
+ }
+
+ public static readonly StyledProperty SelectionForegroundBrushProperty =
+ TextBox.SelectionForegroundBrushProperty.AddOwner();
+ public IBrush? SelectionForegroundBrush
+ {
+ get => GetValue(SelectionForegroundBrushProperty);
+ set => SetValue(SelectionForegroundBrushProperty, value);
+ }
+
+ public static readonly StyledProperty CaretBrushProperty = TextBox.CaretBrushProperty.AddOwner();
+ public IBrush? CaretBrush
+ {
+ get => GetValue(CaretBrushProperty);
+ set => SetValue(CaretBrushProperty, value);
+ }
+
+ public static readonly StyledProperty ShowLeadingZeroProperty = AvaloniaProperty.Register(
+ nameof(ShowLeadingZero));
+ public bool ShowLeadingZero
+ {
+ get => GetValue(ShowLeadingZeroProperty);
+ set => SetValue(ShowLeadingZeroProperty, value);
+ }
+
+ public static readonly StyledProperty InputModeProperty = AvaloniaProperty.Register(
+ nameof(InputMode));
+ public IPv4BoxInputMode InputMode
+ {
+ get => GetValue(InputModeProperty);
+ set => SetValue(InputModeProperty, value);
+ }
+
+ static IPv4Box()
+ {
+ ShowLeadingZeroProperty.Changed.AddClassHandler((o, e) => o.OnFormatChange(e));
+ IPAddressProperty.Changed.AddClassHandler((o, e) => o.OnIPChanged(e));
+ }
+
+ #region Overrides
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+ _firstText = e.NameScope.Find(PART_FirstTextPresenter);
+ _secondText = e.NameScope.Find(PART_SecondTextPresenter);
+ _thirdText = e.NameScope.Find(PART_ThirdTextPresenter);
+ _fourthText = e.NameScope.Find(PART_FourthTextPresenter);
+ _presenters[0] = _firstText;
+ _presenters[1] = _secondText;
+ _presenters[2] = _thirdText;
+ _presenters[3] = _fourthText;
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ if (_currentActivePresenter is null) return;
+ var keymap = AvaloniaLocator.Current.GetRequiredService();
+ bool Match(List gestures) => gestures.Any(g => g.Matches(e));
+ if (e.Key == Key.Enter)
+ {
+ ParseBytes(ShowLeadingZero);
+ SetIPAddressInternal();
+ base.OnKeyDown(e);
+ return;
+ }
+ if (Match(keymap.SelectAll))
+ {
+ _currentActivePresenter.SelectionStart = 0;
+ _currentActivePresenter.SelectionEnd = _currentActivePresenter.Text?.Length ?? 0;
+ return;
+ }
+ else if (Match(keymap.Copy))
+ {
+ Copy();
+ }
+ else if (Match(keymap.Paste))
+ {
+ Paste();
+ }
+ if (e.Key == Key.Tab)
+ {
+ _currentActivePresenter?.HideCaret();
+ _currentActivePresenter.ClearSelection();
+ if (Equals(_currentActivePresenter, _fourthText))
+ {
+ base.OnKeyDown(e);
+ return;
+ }
+ MoveToNextPresenter(_currentActivePresenter, true);
+ _currentActivePresenter?.ShowCaret();
+ e.Handled = true;
+ }
+ else if (e.Key == Key.OemPeriod || e.Key == Key.Decimal)
+ {
+ if (string.IsNullOrEmpty(_currentActivePresenter.Text))
+ {
+ base.OnKeyDown(e);
+ return;
+ }
+ _currentActivePresenter?.HideCaret();
+ _currentActivePresenter.ClearSelection();
+ if (Equals(_currentActivePresenter, _fourthText))
+ {
+ base.OnKeyDown(e);
+ return;
+ }
+ MoveToNextPresenter(_currentActivePresenter, false);
+ _currentActivePresenter?.ShowCaret();
+ _currentActivePresenter.MoveCaretToStart();
+ e.Handled = true;
+ }
+ else if (e.Key == Key.Back)
+ {
+ DeleteImplementation(_currentActivePresenter);
+ }
+ else if (e.Key == Key.Right )
+ {
+ OnPressRightKey();
+ }
+ else if (e.Key == Key.Left)
+ {
+ OnPressLeftKey();
+ }
+ else
+ {
+ base.OnKeyDown(e);
+ }
+ }
+
+ protected override void OnTextInput(TextInputEventArgs e)
+ {
+ if (e.Handled) return;
+ string? s = e.Text;
+ if (string.IsNullOrEmpty(s)) return;
+ if (!char.IsNumber(s![0])) return;
+ if (_currentActivePresenter != null)
+ {
+ int index = _currentActivePresenter.CaretIndex;
+ string? oldText = _currentActivePresenter.Text;
+ if (oldText is null)
+ {
+ _currentActivePresenter.Text = s;
+ _currentActivePresenter.MoveCaretHorizontal();
+ }
+ else
+ {
+ _currentActivePresenter.DeleteSelection();
+ _currentActivePresenter.ClearSelection();
+ oldText = _currentActivePresenter.Text;
+
+ string newText = string.IsNullOrEmpty(oldText)
+ ? s
+ : oldText?.Substring(0, index) + s + oldText?.Substring(Math.Min(index, oldText.Length));
+ if (newText.Length > 3)
+ {
+ newText = newText.Substring(0, 3);
+ }
+ _currentActivePresenter.Text = newText;
+ _currentActivePresenter.MoveCaretHorizontal();
+ if (_currentActivePresenter.CaretIndex == 3 && InputMode == IPv4BoxInputMode.Fast)
+ {
+ _currentActivePresenter.HideCaret();
+ bool success = MoveToNextPresenter(_currentActivePresenter, true);
+ _currentActivePresenter.ShowCaret();
+ if (success)
+ {
+ _currentActivePresenter.SelectAll();
+ _currentActivePresenter.MoveCaretToStart();
+ }
+ }
+ }
+ }
+ }
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ Point position = e.GetPosition(_firstText);
+ foreach (var presenter in _presenters)
+ {
+ if (presenter?.Bounds.Contains(position)??false)
+ {
+ if (e.ClickCount == 1)
+ {
+ presenter.ShowCaret();
+ _currentActivePresenter = presenter;
+ var caretPosition = position.WithX(position.X - presenter.Bounds.X);
+ presenter.MoveCaretToPoint(caretPosition);
+ }
+ else if (e.ClickCount == 2)
+ {
+ presenter.SelectAll();
+ presenter.MoveCaretToEnd();
+ }
+ }
+ else
+ {
+ presenter?.HideCaret();
+ presenter.ClearSelection();
+ }
+ }
+ Debug.WriteLine(_currentActivePresenter?.Name);
+ base.OnPointerPressed(e);
+ }
+
+ protected override void OnLostFocus(RoutedEventArgs e)
+ {
+ foreach (var pre in _presenters)
+ {
+ pre?.HideCaret();
+ pre.ClearSelection();
+ }
+ _currentActivePresenter = null;
+ ParseBytes(ShowLeadingZero);
+ SetIPAddressInternal();
+ }
+
+ protected override void OnGotFocus(GotFocusEventArgs e)
+ {
+ _currentActivePresenter = _firstText;
+ if (_currentActivePresenter is null)
+ {
+ base.OnGotFocus(e);
+ return;
+ }
+ _currentActivePresenter.ShowCaret();
+ _currentActivePresenter.MoveCaretToStart();
+ base.OnGotFocus(e);
+ }
+ #endregion
+
+ private void OnFormatChange(AvaloniaPropertyChangedEventArgs arg)
+ {
+ bool showLeadingZero = arg.GetNewValue();
+ ParseBytes(showLeadingZero);
+ }
+
+ private void OnIPChanged(AvaloniaPropertyChangedEventArgs arg)
+ {
+ IPAddress? address = arg.GetNewValue();
+ if (address is null)
+ {
+ foreach (var presenter in _presenters)
+ {
+ if(presenter!=null) presenter.Text = string.Empty;
+ }
+ ParseBytes(ShowLeadingZero);
+ }
+ else
+ {
+ var sections = address.ToString().Split('.');
+ for (int i = 0; i < 4; i++)
+ {
+ var presenter = _presenters[i];
+ if (presenter != null)
+ {
+ presenter.Text = sections[i];
+ }
+ }
+ ParseBytes(ShowLeadingZero);
+ }
+ }
+
+ private void ParseBytes(bool showLeadingZero)
+ {
+ string format = showLeadingZero ? "D3" : "";
+ if (string.IsNullOrEmpty(_firstText?.Text) && string.IsNullOrEmpty(_secondText?.Text) && string.IsNullOrEmpty(_thirdText?.Text) && string.IsNullOrEmpty(_fourthText?.Text))
+ {
+ _firstByte = null;
+ _secondByte = null;
+ _thirdByte = null;
+ _fourthByte = null;
+ return;
+ }
+ _firstByte = byte.TryParse(_firstText?.Text, out byte b1) ? b1 : (byte)0;
+ _secondByte = byte.TryParse(_secondText?.Text, out byte b2) ? b2 : (byte)0;
+ _thirdByte = byte.TryParse(_thirdText?.Text, out byte b3) ? b3 : (byte)0;
+ _fourthByte = byte.TryParse(_fourthText?.Text, out byte b4) ? b4 : (byte)0;
+ if (_firstText != null) _firstText.Text = _firstByte?.ToString(format);
+ if (_secondText != null) _secondText.Text = _secondByte?.ToString(format);
+ if (_thirdText != null) _thirdText.Text = _thirdByte?.ToString(format);
+ if (_fourthText != null) _fourthText.Text = _fourthByte?.ToString(format);
+ }
+
+
+
+ private bool MoveToNextPresenter(TextPresenter? presenter, bool selectAllAfterMove)
+ {
+ if (presenter is null) return false;
+ if (Equals(presenter, _fourthText)) return false;
+ presenter.ClearSelection();
+ if (Equals(presenter, _firstText)) _currentActivePresenter = _secondText;
+ else if (Equals(presenter, _secondText)) _currentActivePresenter = _thirdText;
+ else if (Equals(presenter, _thirdText)) _currentActivePresenter = _fourthText;
+ if(selectAllAfterMove) _currentActivePresenter.SelectAll();
+ return true;
+ }
+
+ private bool MoveToPreviousTextPresenter(TextPresenter? presenter)
+ {
+ if (presenter is null) return false;
+ if (Equals(presenter, _firstText)) return false;
+ presenter.ClearSelection();
+ if (Equals(presenter, _fourthText)) _currentActivePresenter = _thirdText;
+ else if (Equals(presenter, _thirdText)) _currentActivePresenter = _secondText;
+ else if (Equals(presenter, _secondText)) _currentActivePresenter = _firstText;
+ return true;
+ }
+
+ public void Clear()
+ {
+ foreach (var presenter in _presenters)
+ {
+ if (presenter != null) presenter.Text = null;
+ }
+
+ _firstByte = null;
+ _secondByte = null;
+ _thirdByte = null;
+ _fourthByte = null;
+ IPAddress = null;
+ }
+
+ private void SetIPAddressInternal()
+ {
+ if (_firstByte is null && _secondByte is null && _thirdByte is null && _fourthByte is null)
+ {
+ IPAddress = null;
+ return;
+ }
+ long address = 0;
+ address += _firstByte??0;
+ address += (_secondByte??0) << 8;
+ address += (_thirdByte??0) << 16;
+ address += ((long?)_fourthByte ?? 0) << 24;
+ try
+ {
+ IPAddress = new IPAddress(address);
+ }
+ catch
+ {
+ IPAddress = null;
+ }
+ }
+
+ private void DeleteImplementation(TextPresenter? presenter)
+ {
+ if(presenter is null) return;
+ var oldText = presenter.Text;
+ if (presenter.SelectionStart != presenter.SelectionEnd)
+ {
+ presenter.DeleteSelection();
+ presenter.ClearSelection();
+ }
+ else if (string.IsNullOrWhiteSpace(oldText) || presenter.CaretIndex == 0)
+ {
+ presenter.HideCaret();
+ MoveToPreviousTextPresenter(presenter);
+ if (_currentActivePresenter != null)
+ {
+ _currentActivePresenter.ShowCaret();
+ _currentActivePresenter.MoveCaretToEnd();
+ }
+ }
+ else
+ {
+ int index = presenter.CaretIndex;
+ string newText = oldText?.Substring(0, index - 1) + oldText?.Substring(Math.Min(index, oldText.Length));
+ presenter.MoveCaretHorizontal(LogicalDirection.Backward);
+ presenter.Text = newText;
+ }
+ }
+
+ private void OnPressRightKey()
+ {
+ if (_currentActivePresenter is null) return;
+ if (_currentActivePresenter.IsTextSelected())
+ {
+ int end = _currentActivePresenter.SelectionEnd;
+ _currentActivePresenter.ClearSelection();
+ _currentActivePresenter.MoveCaretToTextPosition(end);
+ return;
+ }
+ if (_currentActivePresenter.CaretIndex >= _currentActivePresenter.Text?.Length)
+ {
+ _currentActivePresenter.HideCaret();
+ bool success = MoveToNextPresenter(_currentActivePresenter, false);
+ _currentActivePresenter.ClearSelection();
+ _currentActivePresenter.ShowCaret();
+ if (success)
+ {
+ _currentActivePresenter.MoveCaretToStart();
+ }
+ }
+ else
+ {
+ _currentActivePresenter.ClearSelection();
+ _currentActivePresenter.CaretIndex++;
+ }
+ }
+
+ private void OnPressLeftKey()
+ {
+ if (_currentActivePresenter is null) return;
+ if (_currentActivePresenter.IsTextSelected())
+ {
+ int start = _currentActivePresenter.SelectionStart;
+ _currentActivePresenter.ClearSelection();
+ _currentActivePresenter.MoveCaretToTextPosition(start);
+ return;
+ }
+ if (_currentActivePresenter.CaretIndex == 0)
+ {
+ _currentActivePresenter.HideCaret();
+ bool success = MoveToPreviousTextPresenter(_currentActivePresenter);
+ _currentActivePresenter.ClearSelection();
+ _currentActivePresenter.ShowCaret();
+ if (success)
+ {
+ _currentActivePresenter.MoveCaretToEnd();
+ }
+ }
+ else
+ {
+ _currentActivePresenter.ClearSelection();
+ _currentActivePresenter.CaretIndex--;
+ }
+ }
+
+ public async void Copy()
+ {
+ string s = string.Join(".", _firstText?.Text, _secondText?.Text, _thirdText?.Text, _fourthText?.Text);
+ IClipboard? clipboard = AvaloniaLocator.Current.GetService();
+ clipboard?.SetTextAsync(s);
+ }
+
+ public static KeyGesture? CopyKeyGesture { get; } = AvaloniaLocator.Current.GetService()?.Copy.FirstOrDefault();
+ public static KeyGesture? PasteKeyGesture { get; } = AvaloniaLocator.Current.GetService()?.Paste.FirstOrDefault();
+ public static KeyGesture? CutKeyGesture { get; } = AvaloniaLocator.Current.GetService()?.Cut.FirstOrDefault();
+
+ public async void Paste()
+ {
+ IClipboard? clipboard = AvaloniaLocator.Current.GetService();
+ if (clipboard is null) return;
+ string s = await clipboard.GetTextAsync();
+ if (IPAddress.TryParse(s, out var address))
+ {
+ IPAddress = address;
+ }
+ }
+
+ public async void Cut()
+ {
+ IClipboard? clipboard = AvaloniaLocator.Current.GetService();
+ if(clipboard is null) return;
+ string s = string.Join(".", _firstText?.Text, _secondText?.Text, _thirdText?.Text, _fourthText?.Text);
+ await clipboard.SetTextAsync(s);
+ Clear();
+ }
+}
+
+public static class TextPresenterHelper
+{
+ public static void MoveCaretToStart(this TextPresenter? presenter)
+ {
+ if (presenter is null) return;
+ presenter.MoveCaretToTextPosition(0);
+ }
+
+ public static void MoveCaretToEnd(this TextPresenter? presenter)
+ {
+ if(presenter is null) return;
+ presenter.MoveCaretToTextPosition(presenter.Text?.Length ?? 0);
+ }
+
+ public static void ClearSelection(this TextPresenter? presenter)
+ {
+ if (presenter is null) return;
+ presenter.SelectionStart = 0;
+ presenter.SelectionEnd = 0;
+ }
+
+ public static void SelectAll(this TextPresenter? presenter)
+ {
+ if(presenter is null) return;
+ if(presenter.Text is null) return;
+ presenter.SelectionStart = 0;
+ presenter.SelectionEnd = presenter.Text.Length;
+ }
+
+ public static void DeleteSelection(this TextPresenter? presenter)
+ {
+ if (presenter is null) return;
+ int selectionStart = presenter.SelectionStart;
+ int selectionEnd = presenter.SelectionEnd;
+ if (selectionStart != selectionEnd)
+ {
+ var start = Math.Min(selectionStart, selectionEnd);
+ var end = Math.Max(selectionStart, selectionEnd);
+ var text = presenter.Text;
+
+ string newText = text is null
+ ? string.Empty
+ : text.Substring(0, start) + text.Substring(Math.Min(end, text.Length));
+ presenter.Text = newText;
+ presenter.MoveCaretToTextPosition(start);
+ }
+ }
+
+ public static bool IsTextSelected(this TextPresenter? presenter)
+ {
+ if (presenter is null) return false;
+ return presenter.SelectionStart != presenter.SelectionEnd;
+ }
+}
\ No newline at end of file
diff --git a/src/Ursa/Ursa.csproj b/src/Ursa/Ursa.csproj
index c6df5d3..fb43200 100644
--- a/src/Ursa/Ursa.csproj
+++ b/src/Ursa/Ursa.csproj
@@ -12,4 +12,8 @@
+
+
+
+