Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Formatter trait for validating & formatting text #1377

Merged
merged 5 commits into from
Dec 30, 2020
Merged

Conversation

cmyr
Copy link
Member

@cmyr cmyr commented Nov 6, 2020

This is a mechanism for handling conversions back and forth
between values and their textual representations. Its major
use case is managing user input in text fields when the input
is intended to represent some other value type.

This patch includes the trait, a simple implementation based
on FromStr (intended as a replacement for the Parse widget)
and a new ValueTextBox widget that uses some Formatter to manage
a TextBox.

The behaviour of the ValueTextBox differs from the normal TextBox
in one very important way; it does not attempt to update the data
until editing has 'completed'. Editing is completed when the user
presses return or attempts to navigate away from the textbox;
the user can also manually cancel editing by pressing esc, which
discards the edit.

These interactions (especially the hard-wired key handling) feels
a bit brittle, and I'd like to try and find a more systematic
approach.

This is very strongly inspired by Cocoa's Formatter class.

update:

This patch led me down a rabbit hole, because it encounters a bunch of situations that are hard to deal with given druid's current data model. In this particular case, this is related to error handling; the solution here is not satisfactory, (it involves setting a callback on the textbox and then manually sending commands as errors occur) but it makes error handling at least possible, until we're able to come up with a more robust, longer-term solution.

I intend to write up the problems i encountered and how I'm thinking about them in a subsequent document.

This now replaces the parse example with an example package, validation, which can be run with,

cargo run --package=validation

2020-11-25 16 26 25

progress on #1140

@JAicewizard
Copy link
Contributor

I feel that format_for_editing might be a bit useless of a method. If someone really wanted to have a different formatting for editing they could just as well use some kind of internal state for that. It might also be that the user wants to format the same value differently for 2 labels even if both are not editable.

I also dont think that Formatter is the right name, it does way more then just formatting. Parser sounds more accurate to me but that might clash with .parse.

@cmyr cmyr force-pushed the value-formatter branch 3 times, most recently from 67cd98c to feb602c Compare November 25, 2020 19:59
@cmyr cmyr marked this pull request as ready for review November 25, 2020 21:19
Copy link
Contributor

@JAicewizard JAicewizard left a comment

Choose a reason for hiding this comment

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

My previous comment still stands.

In addition to that I think it might be useful to have some easy "undo", but that might be a validation error?

///
/// [`Formatter`]: Formatter
pub struct Validation {
result: Result<(), ValidationError>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Make this an option? Seems more appropriate since this is an optional variable and not directly the result of a function.

Would also be nice if it had just a little documentation on what this does.

Copy link
Member Author

Choose a reason for hiding this comment

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

This whole Validation struct is sort of an augmented result; it includes not only the Result itself, but also additional information that exists both in the success and failure cases. It would definitely be possible to store this as an Option, but I think keeping it as a Result more clearly reflects the semantics of the program.

}

/// Create a `Validation` with an error indicating the failure reason.
pub fn failure(err: impl std::error::Error + 'static) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would also be nice if it had just a little documentation on what this does. Does this result in the edit being undone? Does something happen to the selection? Does the user see an error indicating they typed something wrong? (probably not the later 2 but it should be clear)

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a function that creates a Validation object with a given inner error. In the context of text editing, how the textbox reacts when validation fails is dependent on how that text box is configured.

@JAicewizard
Copy link
Contributor

I forgot to put this in, but I would also change the name of the example, then we keep the 'validation' name open for a more simplified example.

@cmyr cmyr mentioned this pull request Dec 16, 2020
Copy link
Member Author

@cmyr cmyr left a comment

Choose a reason for hiding this comment

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

I feel that format_for_editing might be a bit useless of a method. If someone really wanted to have a different formatting for editing they could just as well use some kind of internal state for that. It might also be that the user wants to format the same value differently for 2 labels even if both are not editable.

format_for_editing: I can see how this method might seem a bit superfluous, but I think it will be used in common cases; for instance it is common to edit a currency value, but to not want to let the user edit the currency symbol, and I can't think of a better way of doing this. If I'm totally honest, this is pretty much directly a copy of the API of the Cocoa Formatter class, and I sort of trust that they thought this through and decided this was the best option.

Different formats for same value: this is the beauty of our approach here; you can format the same value in different ways by providing different Formatter instances.

I also dont think that Formatter is the right name, it does way more then just formatting. Parser sounds more accurate to me but that might clash with .parse.

This is a challenge; the Formatter does both formatting and validation. You can think of it as a type that handles the conversion between a value and a string; I think that Parser suffers a similar problem in that it only captures half of the job. You could imagine other names like Stringify or something, but that's also pretty incomplete. 😒

///
/// [`Formatter`]: Formatter
pub struct Validation {
result: Result<(), ValidationError>,
Copy link
Member Author

Choose a reason for hiding this comment

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

This whole Validation struct is sort of an augmented result; it includes not only the Result itself, but also additional information that exists both in the success and failure cases. It would definitely be possible to store this as an Option, but I think keeping it as a Result more clearly reflects the semantics of the program.

}

/// Create a `Validation` with an error indicating the failure reason.
pub fn failure(err: impl std::error::Error + 'static) -> Self {
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a function that creates a Validation object with a given inner error. In the context of text editing, how the textbox reacts when validation fails is dependent on how that text box is configured.

This is a mechanism for handling conversions back and forth
between values and their textual representations. Its major
use case is managing user input in text fields when the input
is intended to represent some other value type.

This patch includes the trait, a simple implementation based
on `FromStr` (intended as a replacement for the `Parse` widget)
and a new ValueTextBox widget that uses some Formatter to manage
a TextBox.

The behaviour of the ValueTextBox differs from the normal TextBox
in one very important way; it does not attempt to update the data
until editing has 'completed'. Editing is completed when the user
presses <return> or attempts to navigate away from the textbox;
the user can also manually cancel editing by pressing <esc>, which
discards the edit.

These interactions (especially the hard-wired key handling) feels
a bit brittle, and I'd like to try and find a more systematic
approach.

This is still a draft; there are a number of rough edges, and
other things I'm not totally happy with, including:

- it uses String everywhere; I might prefer Arc<String> at least
- there is no good mechanism for actually reporting errors back
up the tree
- the error type itself is a bit too fancy, and should be simplified
- I'd like to add a general purpose NumberFormatter
- I'd like to look into incorporating this into the existing TextBox,
including the idea of 'on_completion' and 'on_cancel' handling.

In any case, I'm happy with the progress here; at the very least this
is a much more robust and flexible alternative to the current Parse
widget.

I have updated `examples/parse.rs` to show off some of what is
possible with this API; that example should probably be renamed.
This had gotten well over 700 lines, and I think it makes sense
to start trying to split examples like this up.
This resolves a bunch of fixmes and loose ends
@JAicewizard
Copy link
Contributor

format_for_editing: I can see how this method might seem a bit superfluous, but I think it will be used in common cases; for instance it is common to edit a currency value, but to not want to let the user edit the currency symbol, and I can't think of a better way of doing this. If I'm totally honest, this is pretty much directly a copy of the API of the Cocoa Formatter class, and I sort of trust that they thought this through and decided this was the best option.

Different formats for same value: this is the beauty of our approach here; you can format the same value in different ways by providing different Formatter instances.

It almost seems like these are 2 completely separate concepts, one if of displaying text, like the Display trait but druid specific, and one is actually just a fancy lens specifically for text editing. It would be easier to implement if it was 2 separate traits that are used for separate purposes. One for textboxes and one for labels.

This also sparked the question in me: are custom lenses still useful, besides the derived ones? but thats something for zulip

This is a challenge; the Formatter does both formatting and validation. You can think of it as a type that handles the conversion between a value and a string; I think that Parser suffers a similar problem in that it only captures half of the job. You could imagine other names like Stringify or something, but that's also pretty incomplete. unamused

If the trait would be split like suggested above, maybe Editor?? If the trait would be split the editable one would allow you do edit the value(editing via text ofc)

@cmyr
Copy link
Member Author

cmyr commented Dec 29, 2020

The thing is, they do need to work together; in the case of a text editing box, you format the text that populates the box, and then you validate the input as it occurs, so I don't think it makes a lot of sense to separate them :(

Custom lenses are very useful; they're used heavily in runebender.

@JAicewizard
Copy link
Contributor

I mean split them in editable formatting + validation and normal formatting, not splitting them editable formatting + normal formatting and validating.

@cmyr
Copy link
Member Author

cmyr commented Dec 29, 2020

I agree that you shouldn't need to worry about the editing stuff if you don't need it; that's why there's a default impl of format_for_editing, and if you don't implement it it just reuses the normal format method.

It's also worth thinking of this as a very low-level trait, that ultimately you shouldn't need to implement manually much, because we should provide default formatters for common types of values that handle things like localization.

One way to think about this Formatter trait is that it is the absolutely most general interface that encompasses a very broad range of possible behaviour, and most of the details and nicer API should be filled in by specific formatters further up the stack.

@JAicewizard
Copy link
Contributor

ah ok then I misunderstood the goal I think

@cmyr
Copy link
Member Author

cmyr commented Dec 30, 2020

okay, going to merge this, can shake out problems as they arise.

@cmyr cmyr merged commit 6bb0f51 into master Dec 30, 2020
@cmyr cmyr deleted the value-formatter branch December 30, 2020 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants