AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
26.04k stars 2.25k forks source link

Lightweight text entry control #13276

Open stevemonaco opened 1 year ago

stevemonaco commented 1 year ago

Is your feature request related to a problem? Please describe.

TextBox is a heavy control for building your own text entry controls on top of. There's a lot of flexibility in its dependency properties and that requires many layout controls. This is undesirable for simpler, single line text entry controls.

Describe the solution you'd like

A lightweight TextBoxBase / TextEntryBase that is more appropriate to derive from and build on top of, specifically for single line controls. I didn't see anything in the repo (Avalonia.Controls/Primitives) that was relevant.

Describe alternatives you've considered

  1. Theming TextBox to effectively ignore dependency properties and remove more expensive layout components. It feels a bit hackish like a particularly egregious Liskov Substitution Principle violation.

  2. Copying the implementation from TextBox.cs and its dependencies in preparation for trimming down the surface area. At the moment, you must copy the following classes from Avalonia to reproduce TextBox roughly 1:1 in the project:

TextBox - To be replaced by custom control
TextBoxTextInputMethodClient: TextInputMethodClient - Painful. TextBox relies upon it and the class is internal so copying is necessary.
TextPresenter - Painful. TextPresenter.GetCursorRectangle is required by TextBoxTextInputMethodClient and is marked internal, so copying is necessary. Doesn't seem to be a way around.

TextBoxAutomationPeer - Straightforward, should be reimplemented anyways as the ctor needs the custom control.
StringUtils - Straightforward, marked internal.
StringBuilderCache - Straightforward, marked internal.
EnumExtensions - Straightforward, .HasAllFlags is marked internal.

Some of these, especially the latter, might go away once features get trimmed. But it's a painful starting point.

Additional context

I'm creating a custom numeric input control similar to NumericUpDown where I don't need most of the TextBox feature set, but I do need tight control over mouse dragging. eg. Dragging up increases the number and dragging down decreases it.

maxkatz6 commented 1 year ago

So far in my experience, when I need to show lots of text entry controls at once, the best approach is to use TextBlock, which then is replaced by TextBox on focus. The same approach is used in DataGrid in both WPF/Avalonia, and now it is in TreeDataGrid. It also can be applied to NumericUpDown.

But if you are reimplementing TextBox to be more lightweight, we need to understand what exactly is making TextBox "heavy". Is it data validation support, IME, automation, undo/redo...

stevemonaco commented 1 year ago

Most layout-related performance issues could mostly be addressed through a new ControlTheme for TextBox (which I'd do on my end) to remove layout components that are unnecessary for said control. The TextBlock swap trick is nice, but does add complexity. Especially if the number of controls is not extreme.

Data validation, IME, automation, undo/redo, cut/copy/paste, caret navigation, selection, etc are all expected parts of single line input controls and changes to those would require modifications to components deeper than TextBox. So this issue is more about finding some means to reduce the wide-ranging API surface of TextBox dependency properties to build custom controls on top of.

Many properties fit niche scenarios: MaxLines, password-related / masking properties, etc, but don't necessarily impact layout. Others like TextWrapping, Watermark, UseFloatingWatermark, InnerLeftContent, and InnerRightContent imply that any text input control should support those in order to be compliant and these require additional layout. I'm on the fence with ScrollViewer being a required TemplatePart for a lighter text entry control.

I would probably start by removing the above subset in the new control/primitive, but it may be too opinion-based for Avalonia to design and maintain. If that can't happen, then the next best thing would be making TextPresenter.GetCursorRectangle (or equivalent) public. (Re)Implementing both TextInputMethodClient and TextPresenter seems a bit much before you can even touch the custom control which uses them.

maxkatz6 commented 1 year ago

Properties itself shouldn't really make performance noticeably worse, especially when kept in default value. But styles using them could. Especially selectors like these: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml#L226-L249 Unused template parts also add to overall complexity just by existing there. This is where x:Load from UWP could help. But before that, only a text presenter should be required from the whole template.

If that can't happen, then the next best thing would be making TextPresenter.GetCursorRectangle (or equivalent) public

I don't have an opinion on these, @Gillibald. TextInputMethodClient was designed to be implemented for each custom text entry control. Controls like AvaloniaEdit also implement their own. It should be possible to reimplement TextBox-like control if one needs.

stevemonaco commented 1 year ago

To clarify, I'm not concerned about the existence of dependency properties themselves having a significant performance impact. It's the extra layout controls necessary to support the broader API surface. The FluentTheme TextBox looks to have two ContentPresenters, a Grid, two TextBlocks, and probably a Panel in excess that need to be available to support the properties I mentioned before that will never be used. Maybe the DockPanel, too. Plus all of the TemplateBindings and also the conditional styling as you mentioned.

So there are two straightforward approaches to reduce layout controls as I see it. The TextBlock swap is a possible third, but manages the problem at a higher level.

  1. Create a new text entry control with a new ControlTheme. Run into the pain points I mentioned in the opening comment.
  2. Create a new ControlTheme for TextBox that chooses to not represent properties and remove the not-to-be-supported layout controls. This breaks expectations for a large amount of the TextBox API surface.

Further cons on approach 2. There doesn't seem to be a way to communicate unsupported properties within a ControlTemplate for the TargetType and the user must thoroughly examine the style instead of it being available at-a-glance. In my experience, this examination first happens in the middle of a debugging session because a selector hasn't worked as expected. Maybe some possibility for a hypothetical <Setter Property="InnerContentTemplate" Value="{x:NotSupported}" /> or {NotSupportedBinding} that can log or exception at debug-time if it's ever set again? Custom Setters seemed difficult, but something explicit like <NotSupported Property="InnerContentTemplate" /> seems preferable if no other XAML dialect has solved this issue. Getting off-track here though.

timunie commented 1 year ago

off-topic: I think our highest bottleneck is that we don't have virtualized textboxes yet.

maxkatz6 commented 1 year ago

Virtualized textboxes would help with huge texts, but it's pretty much the opposite of being lightweight, as virtualization adds complexity on its own.

Gillibald commented 1 year ago

I don't understand the issue here. The TextBox itself just requires a control template with a ScrollViewer that contains a TextPresenter. That is it. Nothing more.

workgroupengineering commented 1 year ago

In this experimental branch I tried to reduce the allocation of TextPresenter.CarretBrush, but I am unable to measure the effectiveness. The benchmark is probably wrong.

@stevemonaco Can you test if it improves something in your use case?

stevemonaco commented 1 year ago

I don't understand the issue here. The TextBox itself just requires a control template with a ScrollViewer that contains a TextPresenter. That is it. Nothing more.

I'm aware. However, if you are faithful to the vast number of dependency properties TextBox has, you won't be faithfully theming the control and that's confusing to other devs. So I want to implement my own custom control which is a trimmed-down TextBox primitive in terms of properties (or have Avalonia provide one). This is made difficult because of several pieces (TextBoxTextInputMethodClient and TextPresenter.GetCursorRectangle) are marked internal and can't be accessed in user apps. Particularly, TextPresenter.GetCursorRectangle could be made public as there's no other way to get those bounds, AFAIK.

@stevemonaco Can you test if it improves something in your use case?

Hi, this is unfortunately unrelated to my issue. It is nice to see improvements in other areas though.

robloo commented 1 year ago

WPF has more primitive "base class" text controls. Porting some of that over might help with the ask here.

TextBoxBase -> TextBox exists in WPF. I thought there was some sort of TextContainer-type control as well (https://github.com/AvaloniaUI/Avalonia/issues/7502#issuecomment-1075941003)?