diff --git a/AUTHORS b/AUTHORS index dfa6b9f233..9217880595 100644 --- a/AUTHORS +++ b/AUTHORS @@ -21,3 +21,4 @@ Laura Gallo Tim Murison Manmeet Singh Simon Fell +Sean Cohan diff --git a/CHANGELOG.md b/CHANGELOG.md index 49df2b4eff..449f3fb21a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ You can find its changes [documented below](#070---2021-01-01). ### Added +- OptionFormatter TextBox Formatter ([#2193] by [@meettitan]) - Strikethrough rich text attribute ([#1953] by [@jenra-uwu]) - System fonts loaded so that SVG images render text ([#1850] by [@DrGabble]) - Add `scroll()` method in WidgetExt ([#1600] by [@totsteps]) diff --git a/druid/src/text/format.rs b/druid/src/text/format.rs index 5c1affc29d..47c7a5def8 100644 --- a/druid/src/text/format.rs +++ b/druid/src/text/format.rs @@ -225,3 +225,57 @@ impl Default for ParseFormatter { ParseFormatter::new() } } + +/// A naive [`Formatter`] for optional types that implement [`FromStr`]. +/// +/// Maps empty strings to None +/// Some types that impl [`FromStr`], such as integer values, do not allow empty strings as valid values. +/// In these cases, a user cannot remove all text from the text box. +/// To allow users to remove all text, an option can be used to account for empty strings. +/// +/// [`Formatter`]: Formatter +/// [`FromStr`]: std::str::FromStr +/// ``` +/// use druid::text::OptionFormatter; +/// use druid::{Data, Lens, Widget, WidgetExt}; +/// use druid::widget::TextBox; +/// #[derive(Data, Clone, Lens)] +/// struct State { +/// float: Option +/// } +/// +/// fn optional_float_text_box() -> impl Widget { +/// TextBox::new().with_formatter(OptionFormatter).lens(State::float) +/// } +/// ``` +pub struct OptionFormatter; + +impl Formatter> for OptionFormatter +where + T: std::fmt::Display + FromStr, + ::Err: std::error::Error + 'static, +{ + fn format(&self, value: &Option) -> String { + match value { + None => String::new(), + Some(value) => value.to_string(), + } + } + + fn validate_partial_input(&self, input: &str, _sel: &Selection) -> Validation { + if input.is_empty() { + return Validation::success(); + } + match input.parse::() { + Ok(_) => Validation::success(), + Err(e) => Validation::failure(e), + } + } + + fn value(&self, input: &str) -> Result, ValidationError> { + if input.is_empty() { + return Ok(None); + } + Some(input.parse::().map_err(ValidationError::new)).transpose() + } +} diff --git a/druid/src/text/mod.rs b/druid/src/text/mod.rs index 6e82322fb8..8dfe9952ed 100644 --- a/druid/src/text/mod.rs +++ b/druid/src/text/mod.rs @@ -43,7 +43,9 @@ pub use self::attribute::{Attribute, AttributeSpans, Link}; pub use self::backspace::offset_for_delete_backwards; pub use self::editable_text::{EditableText, EditableTextCursor, StringCursor}; pub use self::font_descriptor::FontDescriptor; -pub use self::format_priv::{Formatter, ParseFormatter, Validation, ValidationError}; +pub use self::format_priv::{ + Formatter, OptionFormatter, ParseFormatter, Validation, ValidationError, +}; pub use self::layout::{LayoutMetrics, TextLayout}; pub use self::movement::movement; pub use input_component::{EditSession, TextComponent};