Skip to content

Touch Improvements to TextBox#20848

Open
emmauss wants to merge 17 commits intomasterfrom
textbox_touch_improvement
Open

Touch Improvements to TextBox#20848
emmauss wants to merge 17 commits intomasterfrom
textbox_touch_improvement

Conversation

@emmauss
Copy link
Contributor

@emmauss emmauss commented Mar 9, 2026

What does the pull request do?

This PR adds a lot of improvements for touch controls for textbox. Pointer handling for Touch input has been reworked to feel more natural for touch based devices. New behavior closely matches Android's default touch behavior for text inputs.
The following changes and improvements have been made;

  1. Scroll gesture has priority if its possible to scroll, otherwise moving the pointer across the textbox will move the caret position.
  2. Holding on text will select the word and show selection handles.
  3. Selection handles are larger.
  4. Holding on a selected text region will show the textbox's context menu.
  5. Selection handle control theme has been improved. Default theme uses combined geometry instead of a geometry group. PathIcon replaced with Image
  6. An indicator element is now part of the selection handle control theme. Caret or selection position is now set based on the indicator position, and not the whole handle position.
  7. SelectionHandles properly hide when out of bounds in the textbox. Behavior is more accurate now with the use of the presenter's transformed Clip region.
  8. Text Selector Layer is now below the PopupOverlayLayer,allowing flyouts to show above the selection handles on embedded devices.
  9. Scroll gesture has been updated to send gesture event only when the touch pointer actually moves.
  10. Selection Handles properly detects and adapt to RTL text.

What is the current behavior?

What is the updated/expected behavior with this PR?

How was the solution implemented (if it's not obvious)?

Checklist

Breaking changes

TextSelectionHandle control theme is updated.

Obsoletions / Deprecations

Fixed issues

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0063110-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@emmauss emmauss force-pushed the textbox_touch_improvement branch from 344b20e to c07d2c3 Compare March 13, 2026 10:30
@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0063361-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0063371-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@emmauss emmauss force-pushed the textbox_touch_improvement branch from bb3444b to 9823926 Compare March 15, 2026 22:17
@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0063515-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0063538-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0063549-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@emmauss emmauss force-pushed the textbox_touch_improvement branch from 1523fc7 to 45b9fe2 Compare March 18, 2026 12:33
@emmauss emmauss marked this pull request as ready for review March 18, 2026 12:37
@emmauss emmauss changed the title [WIP] Touch Improvements to TextBox Touch Improvements to TextBox Mar 18, 2026
@emmauss emmauss requested a review from MrJul March 18, 2026 12:41
@avaloniaui-bot
Copy link

You can test this PR using the following package version. 12.0.999-cibuild0063613-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]


public void Dispose()
{
_sinceLastSample.Stop();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: it's not technically required to Stop a Stopwatch, they don't do anything until measured.


private void UpdateHandleClasses()
{
Classes.Remove("caret");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Standard classes should not be set directly, these are theme-specific. However, I see no issues in converting those to pseudo-classes instead.

flyout.Hide();
private List<Visual> _attachedVisuals = new List<Visual>();
private TextPresenter? _presenter;
private object _lock = new object();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lock should not be needed. Everything here is dispatcher-bound, and accessing the controls from another thread is not supported.

{
if (visual is Layoutable layoutable)
{
layoutable.EffectiveViewportChanged += Visual_EffectiveViewportChanged;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to listen to every parent? First, this is usually very brittle (it won't work correctly when the visual tree changes). Plus, if the viewport for the presenter itself hasn't actually changed, why is the invalidation needed?

/// <summary>
/// The radius for touch input. Used to determine if selection should change from moving a touch pointer.
/// </summary>
private readonly static int s_touchRadius = (int)((AvaloniaLocator.Current?.GetService<IPlatformSettings>()?.GetTapSize(PointerType.Touch).Height ?? 10) / 2) + 5;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not access the locator in a static initializer. There is no guarantee that the locator will be correctly set, the runtime is free to run static initializers at any point. Consider making that lazy instead.

private static readonly bool s_shouldWrapAroundSelection;

private const int ContextMenuPadding = 16;
private static bool s_isInTouchMode;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is static but updated by instance specific code. Consider making it non-static.

if(!e.Handled)
{
// Gesture may cause an overscroll so we mark the event as handled if it did.
e.Handled = canXScroll || canYScroll;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the logic here, won't that prevent scroll chaining in this case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed while testing, scroll chaining does not work correctly anymore.

@MrJul
Copy link
Member

MrJul commented Mar 19, 2026

There's a regression when a selection is active with the context menu open. Previously, double-tapping another word would select it in this case. Now, you need 3 taps instead (one to close the popup, then two for the normal double-tap selection to activate).

Please add a test for this once fixed.

@MrJul
Copy link
Member

MrJul commented Mar 19, 2026

I've tested this with a touch screen, and overall, the changes are quite good :)
However, there are some regressions here.

We need more unit tests overall for this. This is not the easiest area to test, but we should be able to validate most of the behaviors added by this PR. They are too easy to break with future changes otherwise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants