Skip to content

Commit

Permalink
feat(textbox): add support for text selection
Browse files Browse the repository at this point in the history
  • Loading branch information
ramezgerges committed Nov 14, 2023
1 parent e8fd2fb commit 2324b9e
Show file tree
Hide file tree
Showing 10 changed files with 428 additions and 122 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ private void UseDevice(PointerPoint pointer, Gdk.Device device)

var pointerPoint = new Windows.UI.Input.PointerPoint(
frameId: time,
timestamp: time,
timestamp: time * (ulong)TimeSpan.TicksPerMillisecond, // time is in ms, timestamp is in ticks
device: pointerDevice,
pointerId: pointerId,
rawPosition: rawPosition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public double BaselineOffset
}
}
#endif
#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__
#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || false || __NETSTD_REFERENCE__ || __MACOS__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")]
public global::Windows.UI.Xaml.Media.SolidColorBrush SelectionHighlightColor
{
Expand Down Expand Up @@ -265,7 +265,7 @@ public bool IsTextTrimmed
typeof(global::Windows.UI.Xaml.Controls.TextBlock),
new Windows.UI.Xaml.FrameworkPropertyMetadata(default(global::Windows.UI.Xaml.OpticalMarginAlignment)));
#endif
#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || __SKIA__ || __NETSTD_REFERENCE__ || __MACOS__
#if __ANDROID__ || __IOS__ || IS_UNIT_TESTS || __WASM__ || false || __NETSTD_REFERENCE__ || __MACOS__
[global::Uno.NotImplemented("__ANDROID__", "__IOS__", "IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__", "__MACOS__")]
public static global::Windows.UI.Xaml.DependencyProperty SelectionHighlightColorProperty { get; } =
Windows.UI.Xaml.DependencyProperty.Register(
Expand Down
5 changes: 5 additions & 0 deletions src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ static TextBlock()
}
}

public TextBlock()
{
_hyperlinks.CollectionChanged += HyperlinksOnCollectionChanged;
}

/// <summary>
/// Finds a private constructor that allows for the specification of MaxLines.
/// </summary>
Expand Down
153 changes: 108 additions & 45 deletions src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ public partial class TextBlock : DependencyObject

private Run _reusableRun;
private bool _skipInlinesChangedTextSetter;
private bool _subscribeToPointerEvents;
private bool _isPressed;
private (int start, int end) Selection
{
get => _selection;
set
{
_selection = value;
#if __SKIA__
Inlines.Selection = (Math.Min(value.start, value.end), Math.Max(value.start, value.end));
#endif
}
}

#if !UNO_REFERENCE_API
public TextBlock()
Expand Down Expand Up @@ -93,6 +106,10 @@ public InlineCollection Inlines
{
_inlines = new InlineCollection(this);
UpdateInlines(Text);

#if __SKIA__
SetupInlines();
#endif
}

return _inlines;
Expand Down Expand Up @@ -251,6 +268,8 @@ protected virtual void OnTextChanged(string oldValue, string newValue)
{
UpdateInlines(newValue);

Selection = (0, 0);

OnTextChangedPartial();
InvalidateTextBlock();
}
Expand Down Expand Up @@ -474,10 +493,17 @@ public bool IsTextSelectionEnabled
typeof(TextBlock),
new FrameworkPropertyMetadata(
defaultValue: false,
propertyChangedCallback: (s, e) => ((TextBlock)s).OnIsTextSelectionEnabledChangedPartial()
propertyChangedCallback: (s, _) => ((TextBlock)s).OnIsTextSelectionEnabledChanged()
)
);

private void OnIsTextSelectionEnabledChanged()
{
Selection = (0, 0);

OnIsTextSelectionEnabledChangedPartial();
}

partial void OnIsTextSelectionEnabledChangedPartial();

#endregion
Expand Down Expand Up @@ -807,7 +833,7 @@ private void UpdateHyperlinks() { } // Events are subscribed in Hyperlink's ctor
#else
private static readonly PointerEventHandler OnPointerPressed = (object sender, PointerRoutedEventArgs e) =>
{
if (sender is not TextBlock { HasHyperlink: true } that)
if (sender is not TextBlock that)
{
return;
}
Expand All @@ -818,26 +844,43 @@ private void UpdateHyperlinks() { } // Events are subscribed in Hyperlink's ctor
return;
}

var hyperlink = that.FindHyperlinkAt(point.Position);
if (hyperlink is null)
that._isPressed = true;
if (that.IsTextSelectionEnabled)
{
return;
var index = that.Inlines.GetIndexAt(point.Position, false);
that.Selection = (index, index);
}

if (!that.CapturePointer(e.Pointer))
if (that.FindHyperlinkAt(point.Position) is Hyperlink hyperlink)
{
return;
}
if (!that.CapturePointer(e.Pointer))
{
return;
}

hyperlink.SetPointerPressed(e.Pointer);
e.Handled = true;
that.CompleteGesture(); // Make sure to mute Tapped
hyperlink.SetPointerPressed(e.Pointer);
e.Handled = true;
that.CompleteGesture(); // Make sure to mute Tapped
}
};

private static readonly PointerEventHandler OnPointerReleased = (object sender, PointerRoutedEventArgs e) =>
{
if (sender is TextBlock that
&& that.IsCaptured(e.Pointer))
if (sender is not TextBlock that)
{
return;
}


if (that._isPressed && that.IsTextSelectionEnabled && that.FindHyperlinkAt(e.GetCurrentPoint(that).Position) is Hyperlink hyperlink)
{
// if we release on a hyperlink, we don't select anything
that.Selection = (0, 0);
}

that._isPressed = false;

if (that.IsCaptured(e.Pointer))
{
// On UWP we don't get the Tapped event, so make sure to abort it.
that.CompleteGesture();
Expand Down Expand Up @@ -869,7 +912,7 @@ private void UpdateHyperlinks() { } // Events are subscribed in Hyperlink's ctor

private static readonly PointerEventHandler OnPointerMoved = (sender, e) =>
{
if (sender is not TextBlock { HasHyperlink: true } that)
if (sender is not TextBlock that)
{
return;
}
Expand All @@ -883,6 +926,12 @@ private void UpdateHyperlinks() { } // Events are subscribed in Hyperlink's ctor
that._hyperlinkOver = hyperlink;
hyperlink?.SetPointerOver(e.Pointer);
}

if (that._isPressed && that.IsTextSelectionEnabled)
{
var index = that.Inlines.GetIndexAt(point.Position, false);
that.Selection = (that.Selection.start, index);
}
};

private static readonly PointerEventHandler OnPointerEntered = (sender, e) =>
Expand Down Expand Up @@ -932,8 +981,15 @@ private bool AbortHyperlinkCaptures(Pointer pointer)
return aborted;
}

private readonly List<(int start, int end, Hyperlink hyperlink)> _hyperlinks =
new List<(int start, int end, Hyperlink hyperlink)>();
private readonly ObservableCollection<(int start, int end, Hyperlink hyperlink)> _hyperlinks = new();
private (int start, int end) _selection;

private void HyperlinksOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => RecalculateSubscribeToPointerEvents();

private void RecalculateSubscribeToPointerEvents()
{
SubscribeToPointerEvents = HasHyperlink || IsTextSelectionEnabled;
}

private void UpdateHyperlinks()
{
Expand All @@ -943,13 +999,6 @@ private void UpdateHyperlinks()
{
if (HasHyperlink)
{
RemoveHandler(PointerPressedEvent, OnPointerPressed);
RemoveHandler(PointerReleasedEvent, OnPointerReleased);
RemoveHandler(PointerMovedEvent, OnPointerMoved);
RemoveHandler(PointerEnteredEvent, OnPointerEntered);
RemoveHandler(PointerExitedEvent, OnPointerExit);
RemoveHandler(PointerCaptureLostEvent, OnPointerCaptureLost);

// Make sure to clear the pressed state of removed hyperlinks
foreach (var hyperlink in _hyperlinks)
{
Expand All @@ -963,7 +1012,6 @@ private void UpdateHyperlinks()
return;
}

var previousHasHyperlinks = HasHyperlink;
var previousHyperLinks = _hyperlinks.Select(h => h.hyperlink).ToList();
_hyperlinkOver = null;
_hyperlinks.Clear();
Expand All @@ -990,27 +1038,6 @@ private void UpdateHyperlinks()
{
removed.AbortAllPointerState();
}

// Update events subscriptions if needed
// Note: we subscribe to those events only if needed as they increase marshaling on Android and WASM
if (HasHyperlink && !previousHasHyperlinks)
{
InsertHandler(PointerPressedEvent, OnPointerPressed);
InsertHandler(PointerReleasedEvent, OnPointerReleased);
InsertHandler(PointerMovedEvent, OnPointerMoved);
InsertHandler(PointerEnteredEvent, OnPointerEntered);
InsertHandler(PointerExitedEvent, OnPointerExit);
InsertHandler(PointerCaptureLostEvent, OnPointerCaptureLost);
}
else if (!HasHyperlink && previousHasHyperlinks)
{
RemoveHandler(PointerPressedEvent, OnPointerPressed);
RemoveHandler(PointerReleasedEvent, OnPointerReleased);
RemoveHandler(PointerMovedEvent, OnPointerMoved);
RemoveHandler(PointerEnteredEvent, OnPointerEntered);
RemoveHandler(PointerExitedEvent, OnPointerExit);
RemoveHandler(PointerCaptureLostEvent, OnPointerCaptureLost);
}
}

private bool HasHyperlink
Expand All @@ -1025,6 +1052,42 @@ private bool HasHyperlink
}
}

private bool SubscribeToPointerEvents
{
get => _subscribeToPointerEvents;
set
{
if (_subscribeToPointerEvents == value)
{
return;
}

_subscribeToPointerEvents = value;

// Update events subscriptions if needed
// Note: we subscribe to those events only if needed as they increase marshaling on Android and WASM
if (value)
{
InsertHandler(PointerPressedEvent, OnPointerPressed);
InsertHandler(PointerReleasedEvent, OnPointerReleased);
InsertHandler(PointerMovedEvent, OnPointerMoved);
InsertHandler(PointerEnteredEvent, OnPointerEntered);
InsertHandler(PointerExitedEvent, OnPointerExit);
InsertHandler(PointerCaptureLostEvent, OnPointerCaptureLost);
}
else
{
RemoveHandler(PointerPressedEvent, OnPointerPressed);
RemoveHandler(PointerReleasedEvent, OnPointerReleased);
RemoveHandler(PointerMovedEvent, OnPointerMoved);
RemoveHandler(PointerEnteredEvent, OnPointerEntered);
RemoveHandler(PointerExitedEvent, OnPointerExit);
RemoveHandler(PointerCaptureLostEvent, OnPointerCaptureLost);
}
}
}


#if !__SKIA__
private Hyperlink FindHyperlinkAt(Point point)
{
Expand Down
5 changes: 5 additions & 0 deletions src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.iOSmacOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,10 @@ private CGRect GetDrawRect(CGRect rect)

return rect;
}

public TextBlock()
{
_hyperlinks.CollectionChanged += HyperlinksOnCollectionChanged;
}
}
}
Loading

0 comments on commit 2324b9e

Please sign in to comment.