gui-cs / Terminal.Gui

Cross Platform Terminal UI toolkit for .NET
MIT License
9.51k stars 679 forks source link

Add `NumericUpDown` View #3261

Closed tig closed 2 weeks ago

tig commented 6 months ago

We need the equivalent of

image

https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.numericupdown?view=windowsdesktop-8.0

See Adornments Editor etc... for why this is pretty desperately needed.

As I've had shower thoughts about this, ideas I've had:

dodexahedron commented 6 months ago

NumericUpDown controls are nice to have for sure!

I have some initial thoughts but I'll come back to this later after I've thought about them a bit. But I'll put a couple quickies here while it's fresh.

Underlying storage should probably be type-anbositc, using the various interfaces in System.Math.Numerics. Then it won't matter what type the number is, and the user can use whatever fits their needs. Optionally, we could provide convenience implementations such as for int, double, and decimal, but that's just icing.

Presentation is culture-specific and should be treated accordingly, with at least some flexibility for the user to customize it, and sane defaults like just not formatting it at all unless an appropriate ICustomFormatter or standard numeric format string is supplied.

The control itself could possibly be composed of a TextField or TextValidateField and two Buttons, to minimize code duplication, leaving only the unique behaviors of the new control to be implemented.

(ew - fixed bad formatting from emailed comment)

dodexahedron commented 6 months ago

Going to make comments expanding on my earlier musing as separate posts for clarity...

First, expanding on the type-agnostic thing...

This is something that we can do thanks to work done in .net 7 and up and which would have been much more difficult, requiring a bunch of work and wrappers and such, in earlier versions. They added a whole bunch of interfaces such as INumber, which basically groups a bunch of other new interfaces (and some old ones) under one roof, but which enables fully generic math. They declared those interfaces on the various primitives, as well, meaning you can now implement a generic with a type parameter constraint of INumber<T> and be able to perform all the same operations on T that you'd expect to be able to perform on those primitives, just by using that interface. There are also some more specific interfaces added, as well, for cases where they may be relevant, but that one takes care of a very broad range of possibilities already.

Further, they implemented it in a smart way and, even though you're using an interface to interact with T, boxing isn't happening at runtime.

The original blog post for the feature, when .net 7 was released, is here: https://devblogs.microsoft.com/dotnet/dotnet-7-generic-math/

All the standard numeric types plus even char, int128, BigInteger, and System.Numerics.Complex, among others, already have the new interfaces declared, right out of the box, starting from .net 7!

dodexahedron commented 6 months ago

For presentation, I don't know if there's really much more to say than I already did, but here are more words anyway:

As I'm sure everyone is already perfectly aware, different cultures format numbers differently when it comes to things like the symbols for decimal, thousand/other grouping separators, and currency symbols, even if they speak the same language.

The very simplest thing to do is to simply ignore formatting and always just present numbers as the executing system naturally converts them to strings. That's really bare-bones, though, and I think it's a reasonable user expectation for a numeric control to be able to handle different formatting options. Otherwise, why bother using it over a TextField, right?

That can of course be done many different ways, such as providing a property for format string that simply passes the user-supplied format string to the Format method on the numeric type, but that places some burden on us and also makes it difficult for the user to control exactly which culture rules are used for that formatting.

I'd suggest that we provide two properties:

  1. A property of type IFormatProvider that is, if defined, passed to the T.ToString() method, for display, and otherwise uses the current UI culture if not provided.
  2. A property of type string that is passed as the format string to the T.ToString() method. If null or empty, that just means they'd get default ToString behavior if the default IFormatProvider is in use or, if an IFormatProvider was provided, whatever behavior is dictated by that type.

Those two properties are orthogonal and neither, one or the other, or both are all perfectly valid and don't place any burden on us to support.

These properties would be used both for parsing the values, on user-initiated text change (or maybe focus change?), as well as for output text formatting.

That provides pretty much infinite flexibility to the consumer while requiring like 4 total extra lines of code for us, across the whole thing (and maybe not even that many, actually).

But that question above is a good one to discuss: If the user changes text, when do we raise the associated Changing/Changed events? I basically see three options:

  1. When the text loses focus (including in a ButtonDown event of either of the buttons, before further events are raised from there).
  2. In real-time, on each change to the text.
  3. Either of the above, dictated by a settable property on the control, so the user can pick their desired behavior.

Option 1 seems to be the most typical in other UI libraries, and avoids some annoying pitfalls of option 2.

Option 2 I think is bad default behavior, and requires a bunch of special case handling to keep it from rejecting intermediate changes that might not be immediately parsable as T. That raises the question of where we draw the line for responsibility of input validation. I'd argue that, in any case of real-time handling (including option 2), we'd need to just be hands-off and require that the user deal with it via the appropriate XChanging event, making any subsequent runtime problems theirs to deal with. Attempting to insert ourselves into the process in any way both makes work for us and restricts what the user can do, which very quickly makes doing it this way pointless.

Option 3 is of course more flexible, but still has option 2's problems, if that's chosen by the consumer, and is also more work for us.

My vote, obviously, would be for option 1, unless someone has any other ideas.

dodexahedron commented 6 months ago

Actually, I do have another idea for that, but it can be done with any of those options:

Perform validation separately from actually updating the underlying T value.

For example, the current text can be parsed on every change, in real-time, to determine if it's valid, but allow all changes to be accepted and only attempt to parse it and update the actual underlying value on the focus change, as in option 1 above.

That'd let people do things like have the color change or do other validation-related activities in the UI as the text is modified, but wouldn't result in temporarily invalid values being rejected by default.

dodexahedron commented 6 months ago

And for the third part of my initial comment:

That was just a thought based on how similar controls are composed in some other UI frameworks, but we can of course do anything from making it a completely unique View type to making it literally a composition of other View types and anything in between.

I don't have a strong opinion on that one, though I'd suggest we look at how other frameworks implement the underlying code (in any strategy) before we settle on a final answer, since we can probably learn from what others have tried.

dodexahedron commented 6 months ago

One thing I would like to suggest and request, though, is that any work to add new view types like this be held off for now.

The event refactor gets more and more complicated as things are added and as they keep diverging from each other, which is part of the core goal of that work - to unify and standardize event handling. Making them more formally "correct" is just a happy side benefit of that work.

If we at least put that freeze in effect until event refactoring can be done, I can re-branch that and get back to work on it right after I finish killing our copies of the Drawing primitives, which won't take much longer. And I can also go into it with the foreknowledge that we want to add NumericUpDown and take that into account if or when necessary while doing that.

tig commented 4 months ago

To be able to quickly test various use cases in the new Content Scrolling PR:

...I really needed this view. So I built a prototype w/in the Buttons Scenario.

It supports mouse and keyboard (cursorup/down) and numeric types, but only one digit of precision and the value field is readonly.

Once @dodexahedron is ready with the event refactor we can work on enhancing this and making it part of the library.

c6nN6BT 1

dodexahedron commented 2 months ago

Just going through some old items in my queue...

Once @dodexahedron is ready with the event refactor we can work on enhancing this and making it part of the library.

Getting closer to that.

Unless something more pressing comes up, I intend to hit that after the interface work, as it will build on that as well.