Forms · NumberInput: blank shows a placeholder and commits only what is typed
This commit is contained in:
4 files changed
+101
-40
No files matched your search
+25
-40
@@ -65,6 +65,8 @@ struct Numeric {
|
||||
on_commit: Option<OnChange>,
|
||||
value: f64,
|
||||
limits: (f64, f64, f64, usize),
|
||||
/// Shows no number; an empty field commits nothing.
|
||||
blank: bool,
|
||||
_events: Subscription,
|
||||
}
|
||||
|
||||
@@ -122,6 +124,9 @@ fn numeric(id: &ElementId, window: &mut Window, cx: &mut App) -> Entity<Numeric>
|
||||
let (min, max, _, precision) = numeric.limits;
|
||||
match event {
|
||||
InputEvent::Blur | InputEvent::Submit => {
|
||||
if numeric.blank && input.read(cx).text().trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
let value = settle(numeric.current(cx), min, max, precision);
|
||||
numeric.value = value;
|
||||
show(
|
||||
@@ -156,6 +161,7 @@ fn numeric(id: &ElementId, window: &mut Window, cx: &mut App) -> Entity<Numeric>
|
||||
on_commit: None,
|
||||
value: 0.0,
|
||||
limits: (f64::MIN, f64::MAX, 1.0, 0),
|
||||
blank: false,
|
||||
_events: events,
|
||||
}
|
||||
})
|
||||
@@ -225,6 +231,7 @@ pub struct NumberInput {
|
||||
on_commit: Option<OnChange>,
|
||||
scrub_label: Option<SharedString>,
|
||||
label: SharedString,
|
||||
blank: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl NumberInput {
|
||||
@@ -243,6 +250,7 @@ impl NumberInput {
|
||||
on_commit: None,
|
||||
scrub_label: None,
|
||||
label: SharedString::default(),
|
||||
blank: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,6 +260,12 @@ impl NumberInput {
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows `placeholder`, not a number, as for a mixed selection; commits once typed in.
|
||||
pub fn blank(mut self, placeholder: impl Into<SharedString>) -> Self {
|
||||
self.blank = Some(placeholder.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn range(mut self, min: f64, max: f64) -> Self {
|
||||
assert!(min <= max, "number range {min}..{max} is empty");
|
||||
self.min = min;
|
||||
@@ -322,13 +336,21 @@ impl RenderOnce for NumberInput {
|
||||
numeric.on_commit = self.on_commit.clone();
|
||||
numeric.value = value;
|
||||
numeric.limits = (min, max, step, self.precision);
|
||||
numeric.blank = self.blank.is_some();
|
||||
});
|
||||
let input = state.read(cx).input.clone();
|
||||
if input.read(cx).label_text() != &self.label {
|
||||
let label = self.label.clone();
|
||||
input.update(cx, |input, cx| input.set_label(label, cx));
|
||||
}
|
||||
let shown = format::number(value, self.precision, Separators::EN);
|
||||
let placeholder = self.blank.clone().unwrap_or_default();
|
||||
if input.read(cx).placeholder_text() != &placeholder {
|
||||
input.update(cx, |input, cx| input.set_placeholder(placeholder, cx));
|
||||
}
|
||||
let shown = match self.blank {
|
||||
Some(_) => String::new(),
|
||||
None => format::number(value, self.precision, Separators::EN),
|
||||
};
|
||||
let editing = input.read(cx).focus().is_focused(window);
|
||||
if !editing && input.read(cx).text() != shown {
|
||||
input.update(cx, |input, cx| input.set_text(shown, cx));
|
||||
@@ -452,45 +474,8 @@ impl RenderOnce for NumberInput {
|
||||
}
|
||||
}
|
||||
|
||||
/// A label you drag sideways to change a number, beside its field.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ScrubInput {
|
||||
field: NumberInput,
|
||||
}
|
||||
|
||||
impl ScrubInput {
|
||||
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, value: f64) -> Self {
|
||||
let mut field = NumberInput::new(id, value);
|
||||
field.scrub_label = Some(label.into());
|
||||
Self { field }
|
||||
}
|
||||
|
||||
pub fn range(mut self, min: f64, max: f64) -> Self {
|
||||
self.field = self.field.range(min, max);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn step(mut self, step: f64) -> Self {
|
||||
self.field = self.field.step(step);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn precision(mut self, places: usize) -> Self {
|
||||
self.field = self.field.precision(places);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_change(mut self, handler: impl Fn(f64, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.field = self.field.on_change(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ScrubInput {
|
||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
self.field
|
||||
}
|
||||
}
|
||||
mod scrub;
|
||||
pub use scrub::ScrubInput;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,45 @@
|
||||
//! A number field with a label you drag.
|
||||
|
||||
use gpui::{App, ElementId, IntoElement, RenderOnce, SharedString, Window};
|
||||
|
||||
use super::NumberInput;
|
||||
|
||||
/// A label you drag sideways to change a number, beside its field.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ScrubInput {
|
||||
field: NumberInput,
|
||||
}
|
||||
|
||||
impl ScrubInput {
|
||||
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, value: f64) -> Self {
|
||||
let mut field = NumberInput::new(id, value);
|
||||
field.scrub_label = Some(label.into());
|
||||
Self { field }
|
||||
}
|
||||
|
||||
pub fn range(mut self, min: f64, max: f64) -> Self {
|
||||
self.field = self.field.range(min, max);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn step(mut self, step: f64) -> Self {
|
||||
self.field = self.field.step(step);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn precision(mut self, places: usize) -> Self {
|
||||
self.field = self.field.precision(places);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_change(mut self, handler: impl Fn(f64, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.field = self.field.on_change(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ScrubInput {
|
||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
self.field
|
||||
}
|
||||
}
|
||||
@@ -251,6 +251,36 @@ fn typing_commits_only_on_enter_and_steps(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
struct Blank {
|
||||
values: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Render for Blank {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
NumberInput::new("blank", 0.0)
|
||||
.blank("Mixed")
|
||||
.on_commit(move |value, _, cx| view.update(cx, |view, _| view.values.push(value)))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_blank_field_commits_only_what_is_typed(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (view, cx) = cx.add_window_view(|_, _| Blank { values: Vec::new() });
|
||||
cx.update(|window, cx| window.focus_next(cx));
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.update(|window, cx| window.focus_next(cx));
|
||||
assert!(
|
||||
view.read_with(cx, |view, _| view.values.is_empty()),
|
||||
"Enter and blur on an empty field commit nothing"
|
||||
);
|
||||
cx.update(|window, cx| window.focus_prev(cx));
|
||||
cx.simulate_input("0");
|
||||
cx.simulate_keystrokes("enter");
|
||||
assert_eq!(view.read_with(cx, |view, _| view.values.clone()), [0.0]);
|
||||
}
|
||||
|
||||
struct Pin {
|
||||
code: String,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user