T06a forms text: text input core, fields, numbers, masks, pin, inline, groups, tags, pairs, path, regex, expression, hotkey, mention

This commit is contained in:
ZacharyZhang-NY committed 2026-09-25 05:38:24 -04:00
1 parent 817da41c22
commit ec5713c10b
41 files changed
+6233 -45

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
use ely_gpui_component::{
forms::{EmailInput, InlineEdit, MaskedInput, PhoneInput, PinInput, UrlInput},
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use super::text::field;
use crate::{
probe::probe,
ui::{section, specimen, specimens},
};
pub fn email_url(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let email = field("input-email", window, cx, |input| {
input.placeholder("name@example.com")
});
let url = field("input-url", window, cx, |input| {
input.placeholder("https://")
});
section(
"EmailInput / UrlInput",
"Checked once you leave the field; a quiet note says what fits.",
cx,
)
.child(
specimens()
.child(specimen(
"email",
probe("email", div().w(px(280.0)).child(EmailInput::new(&email))),
cx,
))
.child(specimen(
"url",
div().w(px(280.0)).child(UrlInput::new(&url)),
cx,
)),
)
}
pub fn phone_masked(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let phone = field("input-phone", window, cx, |input| input);
let card = field("input-card", window, cx, |input| input);
let country = window.use_keyed_state("phone-country", cx, |_, _| "US");
let current = *country.read(cx);
section(
"PhoneInput / MaskedInput",
"Typing fits the pattern; literals appear on their own.",
cx,
)
.child(
specimens()
.child(specimen(
"phone",
probe(
"phone",
div()
.w(px(300.0))
.child(PhoneInput::new("phone", &phone, current).on_country(
move |code, _, cx| {
country.update(cx, |country, cx| {
*country = code;
cx.notify();
})
},
)),
),
cx,
))
.child(specimen(
"masked: aa-9999",
probe(
"masked",
div()
.w(px(200.0))
.child(MaskedInput::new("card", &card, "aa-9999")),
),
cx,
)),
)
}
pub fn pin(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let entered = window.use_keyed_state("pin-entered", cx, |_, _| SharedString::from("waiting"));
let note = entered.read(cx).clone();
section(
"PinInput / OTPInput",
"Six boxes, one field underneath: type, paste, or backspace through them.",
cx,
)
.child(probe(
"pin",
PinInput::new("otp", 6).on_complete(move |code, _, cx| {
let code = SharedString::from(format!("entered {code}"));
entered.update(cx, |entered, cx| {
*entered = code;
cx.notify();
})
}),
))
.child(Caption::new(format!("Code: {note}")))
}
pub fn inline_edit(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let title = window.use_keyed_state("inline-title", cx, |_, _| {
SharedString::from("Quarterly planning")
});
let now = title.read(cx).clone();
section(
"InlineEdit / EditableText",
"Click the text to edit. Enter or leaving keeps it; Escape puts it back.",
cx,
)
.child(probe(
"inline",
div()
.w(px(280.0))
.child(InlineEdit::new("title", now).on_commit(move |text, _, cx| {
let text = text.clone();
title.update(cx, |title, cx| {
*title = text;
cx.notify();
})
})),
))
}
+137
View File
@@ -0,0 +1,137 @@
mod formats;
mod numbers;
mod rich;
mod text;
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
use super::Page;
use crate::script::Step;
pub const PAGE: Page = Page {
number: 6,
slug: "forms",
title: "Forms",
summary: "Text, choices, dates, colors, files: every way to say what you mean.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::Rest,
Step::Click("input-name"),
Step::Key("secondary-a"),
Step::Type("Ada Lovelace"),
Step::Key("shift-left"),
Step::Key("shift-left"),
Step::Key("shift-left"),
Step::Wait(300),
Step::Shot("typed"),
Step::Click("textarea"),
Step::Key("secondary-a"),
Step::Type("First line of notes"),
Step::Key("enter"),
Step::Type("Second line, longer, so the words wrap when they reach the edge of the box"),
Step::Wait(300),
Step::Shot("textarea"),
Step::Click("password"),
Step::Key("secondary-a"),
Step::Type("hunter2"),
Step::Wait(200),
Step::Shot("password"),
Step::Click("search"),
Step::Wait(300),
Step::Shot("search-history"),
Step::Click("number"),
Step::Key("up"),
Step::Key("up"),
Step::Wait(300),
Step::Shot("number"),
Step::DownAt("scrub", 12.0, 14.0),
Step::DragTo("scrub", 72.0, 14.0),
Step::UpAt("scrub", 72.0, 14.0),
Step::Wait(300),
Step::Shot("scrubbed"),
Step::Click("email"),
Step::Key("secondary-a"),
Step::Type("ada@example"),
Step::Click("phone"),
Step::Key("secondary-a"),
Step::Type("4155550132"),
Step::Click("masked"),
Step::Key("secondary-a"),
Step::Type("ab1234"),
Step::Wait(300),
Step::Shot("formats"),
Step::Click("pin"),
Step::Key("secondary-a"),
Step::Type("4821"),
Step::Wait(300),
Step::Shot("pin"),
Step::Click("inline"),
Step::Key("secondary-a"),
Step::Type("Q3 planning"),
Step::Key("enter"),
Step::Wait(300),
Step::Shot("inline"),
Step::Click("mention"),
Step::Key("secondary-a"),
Step::Type("Ask @gr"),
Step::Wait(300),
Step::Shot("mention"),
Step::Key("enter"),
Step::Type("about #rel"),
Step::Key("down"),
Step::Key("enter"),
Step::Wait(300),
Step::Shot("mentioned"),
Step::Click("tags"),
Step::Type("motion,design"),
Step::Key("enter"),
Step::Wait(300),
Step::Shot("tags"),
Step::Click("regex"),
Step::Key("secondary-a"),
Step::Type("^(ab+"),
Step::Wait(300),
Step::Shot("regex"),
Step::Click("expression"),
Step::Key("secondary-a"),
Step::Type("price * qty + max(2, 4)"),
Step::Wait(300),
Step::Shot("expression"),
Step::Click("hotkey"),
Step::Key("cmd-shift-k"),
Step::Wait(300),
Step::Shot("hotkey"),
Step::DownAt("path", 380.0, 16.0),
Step::UpAt("path", 380.0, 16.0),
Step::Wait(1200),
Step::CancelPanel,
Step::Wait(600),
Step::Rest,
];
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(text::input(window, cx))
.child(text::text_area(window, cx))
.child(text::password(window, cx))
.child(text::search(window, cx))
.child(numbers::number_input(window, cx))
.child(numbers::scrub(window, cx))
.child(numbers::money(window, cx))
.child(formats::email_url(window, cx))
.child(formats::phone_masked(window, cx))
.child(formats::pin(window, cx))
.child(formats::inline_edit(window, cx))
.child(text::group(window, cx))
.child(rich::mention(window, cx))
.child(rich::tags(window, cx))
.child(rich::rows(cx))
.child(rich::path(window, cx))
.child(rich::regex(window, cx))
.child(rich::expression(window, cx))
.child(rich::hotkey(window, cx))
.into_any_element()
}
+133
View File
@@ -0,0 +1,133 @@
use ely_gpui_component::forms::{NumberInput, ScrubInput, UnitInput};
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::{
probe::probe,
ui::{code, section, specimen, specimens},
};
fn number(key: &'static str, initial: f64, window: &mut Window, cx: &mut App) -> Entity<f64> {
window.use_keyed_state(key, cx, move |_, _| initial)
}
fn store(state: Entity<f64>) -> impl Fn(f64, &mut Window, &mut App) + 'static {
move |value, _, cx| {
state.update(cx, |state, cx| {
*state = value;
cx.notify();
})
}
}
pub fn number_input(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let seats = number("number-seats", 4.0, window, cx);
let now = *seats.read(cx);
section(
"NumberInput",
"Arrow keys or the stepper move it; drag the grip sideways to scrub.",
cx,
)
.child(probe(
"number",
div().w(px(220.0)).child(
NumberInput::new("seats", now)
.range(1.0, 99.0)
.on_change(store(seats)),
),
))
}
pub fn scrub(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let x = number("scrub-x", 120.0, window, cx);
let opacity = number("scrub-opacity", 0.8, window, cx);
let (x_now, opacity_now) = (*x.read(cx), *opacity.read(cx));
section(
"ScrubInput",
"Drag the label sideways, as in a design tool. The field still types.",
cx,
)
.child(probe(
"scrub",
div()
.flex()
.flex_col()
.gap_2()
.w(px(260.0))
.child(
ScrubInput::new("scrub-x", "X", x_now)
.range(-2000.0, 2000.0)
.on_change(store(x)),
)
.child(
ScrubInput::new("scrub-opacity", "Opacity", opacity_now)
.range(0.0, 1.0)
.step(0.01)
.precision(2)
.on_change(store(opacity)),
),
))
}
pub fn money(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let price = number("number-price", 1249.5, window, cx);
let tax = number("number-tax", 8.0, window, cx);
let gap = number("number-gap", 16.0, window, cx);
let unit = window.use_keyed_state("number-unit", cx, |_, _| SharedString::from("px"));
let (price_now, tax_now, gap_now, unit_now) = (
*price.read(cx),
*tax.read(cx),
*gap.read(cx),
unit.read(cx).clone(),
);
section(
"CurrencyInput / PercentInput / UnitInput",
"The same number field with a symbol, a percent, or a unit you click through.",
cx,
)
.child(
specimens()
.child(specimen(
"currency",
div().w(px(200.0)).child(
NumberInput::new("price", price_now)
.currency("USD")
.range(0.0, 1_000_000.0)
.on_change(store(price)),
),
cx,
))
.child(specimen(
"percent",
div().w(px(160.0)).child(
NumberInput::new("tax", tax_now)
.percent()
.range(0.0, 100.0)
.on_change(store(tax)),
),
cx,
))
.child(specimen(
"unit",
probe(
"unit",
div().w(px(180.0)).child(
UnitInput::new("gap", gap_now, ["px", "%", "em"], unit_now)
.range(0.0, 400.0)
.on_change(store(gap))
.on_unit(move |next, _, cx| {
let next = next.clone();
unit.update(cx, |unit, cx| {
*unit = next;
cx.notify();
})
}),
),
),
cx,
)),
)
.child(code(
"NumberInput::new(id, value).currency(\"USD\") and .percent()",
cx,
))
}
+174
View File
@@ -0,0 +1,174 @@
use ely_gpui_component::{
forms::{
ExpressionInput, HotkeyInput, KeyValueInput, ListInput, MentionInput, PathInput,
RegexInput, TagInput, expression_highlights, mention_highlights, regex_highlights,
},
typography::Caption,
};
use gpui::{App, IntoElement, Keystroke, ParentElement, SharedString, Styled, Window, div, px};
use super::text::field;
use crate::{
probe::probe,
ui::{code, section},
};
pub fn mention(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let message = field("input-mention", window, cx, |input| {
input
.multi_line(2, 5)
.placeholder("Write a note. @ for people, # for tags.")
.highlighter(mention_highlights)
});
section(
"MentionInput / HashtagInput",
"@ and # open suggestions at the caret. Arrows choose, Enter picks.",
cx,
)
.child(probe(
"mention",
div().w(px(420.0)).child(
MentionInput::new("mentions", &message)
.trigger('@', ["ada", "alan", "grace", "linus"])
.trigger('#', ["release", "research", "design", "docs"]),
),
))
}
pub fn tags(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let tags = window.use_keyed_state("tags", cx, |_, _| {
vec![SharedString::from("rust"), SharedString::from("gpui")]
});
let now = tags.read(cx).clone();
section(
"TagInput / TokenInput",
"Enter or a comma adds a chip; Backspace on an empty field drops the last.",
cx,
)
.child(probe(
"tags",
div().w(px(420.0)).child(
TagInput::new("topic-tags", now).on_change(move |next, _, cx| {
tags.update(cx, |tags, cx| {
*tags = next;
cx.notify();
})
}),
),
))
}
pub fn rows(cx: &App) -> impl IntoElement + use<> {
section(
"KeyValueInput / ListInput",
"Rows you add and remove. Each row reports as you type.",
cx,
)
.child(
div()
.flex()
.gap_8()
.child(
div().w(px(380.0)).child(
KeyValueInput::new(
"env",
[
("RUST_LOG".to_string(), "info".to_string()),
("PORT".to_string(), "8080".to_string()),
],
)
.on_change(|pairs, _, _| log::info!("gallery: {} pairs", pairs.len())),
),
)
.child(
div().w(px(240.0)).child(
ListInput::new("hosts", ["ely.dev".to_string()])
.placeholder("Host")
.on_change(|items, _, _| log::info!("gallery: {} hosts", items.len())),
),
),
)
}
pub fn path(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let folder = field("input-path", window, cx, |input| {
input.placeholder("~/Projects")
});
section(
"PathInput",
"Type a path, or Browse with the system's dialog.",
cx,
)
.child(probe(
"path",
div()
.w(px(420.0))
.child(PathInput::new("path-browse", &folder).directories()),
))
}
pub fn regex(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let pattern = field("input-regex", window, cx, |input| {
input.placeholder("^[a-z]+$").highlighter(regex_highlights)
});
section(
"RegexInput",
"Colored by part as you type; a broken pattern says why.",
cx,
)
.child(probe(
"regex",
div().w(px(420.0)).child(RegexInput::new(&pattern)),
))
}
pub fn expression(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let formula = field("input-formula", window, cx, |input| {
input
.placeholder("price * qty")
.highlighter(expression_highlights)
});
section(
"ExpressionInput",
"A formula, colored and evaluated as you type.",
cx,
)
.child(probe(
"expression",
div().w(px(420.0)).child(
ExpressionInput::new(&formula)
.variable("price", 12.5)
.variable("qty", 3.0),
),
))
.child(code(
"price = 12.5, qty = 3; abs sqrt round floor ceil min max",
cx,
))
}
pub fn hotkey(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let recorded = window.use_keyed_state("hotkey", cx, |_, _| Keystroke::parse("cmd-k").ok());
let now = recorded.read(cx).clone();
section(
"HotkeyInput / ShortcutRecorder",
"Focus it and press a shortcut; it keeps that one and lets go.",
cx,
)
.child(probe(
"hotkey",
div()
.w(px(260.0))
.child(
HotkeyInput::new("palette-key", now).on_change(move |next, _, cx| {
recorded.update(cx, |recorded, cx| {
*recorded = next;
cx.notify();
})
}),
),
))
.child(Caption::new(
"Records any key with its modifiers; Tab still moves focus.",
))
}
+166
View File
@@ -0,0 +1,166 @@
use ely_gpui_component::{
buttons::IconButton,
forms::{Input, InputAddon, InputGroup, PasswordInput, SearchInput, TextInput},
primitives::IconName,
theme::ControlSize,
typography::Caption,
};
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::{
probe::probe,
ui::{code, section, specimen, specimens},
};
pub fn field(
key: &'static str,
window: &mut Window,
cx: &mut App,
build: impl FnOnce(TextInput) -> TextInput,
) -> Entity<TextInput> {
window.use_keyed_state(key, cx, move |window, cx| build(TextInput::new(window, cx)))
}
pub fn input(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let name = field("input-name", window, cx, |input| {
input.placeholder("Full name")
});
let small = field("input-small", window, cx, |input| {
input.placeholder("Small")
});
let large = field("input-large", window, cx, |input| {
input.placeholder("Large")
});
let clear = field("input-clear", window, cx, |input| {
input.placeholder("Clearable")
});
section(
"Input / TextField / ClearableInput",
"One line. Undo, word jumps, IME and the clipboard come built in.",
cx,
)
.child(probe(
"input-name",
div().w(px(320.0)).child(Input::new(&name)),
))
.child(
specimens()
.child(specimen(
"small",
div()
.w(px(200.0))
.child(Input::new(&small).size(ControlSize::Sm)),
cx,
))
.child(specimen(
"large",
div()
.w(px(240.0))
.child(Input::new(&large).size(ControlSize::Lg)),
cx,
))
.child(specimen(
"clearable",
div().w(px(240.0)).child(Input::new(&clear).clearable()),
cx,
)),
)
}
pub fn text_area(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let notes = field("textarea-notes", window, cx, |input| {
input
.multi_line(3, 6)
.placeholder("Notes grow from three lines to six, then scroll.")
});
section(
"TextArea / AutoResizeInput",
"Several lines. It grows to its content, then scrolls.",
cx,
)
.child(probe(
"textarea",
div().w(px(420.0)).child(Input::new(&notes)),
))
.child(code("TextInput::new(window, cx).multi_line(3, 6)", cx))
}
pub fn password(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let secret = field("input-password", window, cx, |input| {
input.masked().placeholder("Password")
});
section(
"PasswordInput",
"Bullets by default; the eye shows the text. Masked text never reaches the clipboard.",
cx,
)
.child(probe(
"password",
div().w(px(320.0)).child(PasswordInput::new(&secret)),
))
}
pub fn search(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let query = field("input-search", window, cx, |input| {
input.placeholder("Search docs")
});
let picked = window.use_keyed_state("search-picked", cx, |_, _| {
SharedString::from("nothing yet")
});
let note = picked.read(cx).clone();
section(
"SearchInput",
"A magnifier, a clear button, and recent searches while it is empty.",
cx,
)
.child(probe(
"search",
div().w(px(320.0)).child(
SearchInput::new("docs-search", &query)
.history(["dock layout", "sticky header", "spring easing"])
.on_pick(move |query, _, cx| {
let query = query.clone();
picked.update(cx, |picked, cx| {
*picked = query;
cx.notify();
})
}),
),
))
.child(Caption::new(format!("Last recent search: {note}")))
}
pub fn group(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let site = field("input-site", window, cx, |input| {
input.placeholder("ely.dev")
});
let share = field("input-share", window, cx, |input| input.placeholder("0"));
section(
"InputGroup / InputAddon",
"Addons join the field's frame: a scheme before, a unit or a control after.",
cx,
)
.child(
specimens()
.child(specimen(
"text addons",
div().w(px(320.0)).child(
InputGroup::new(&site)
.before(InputAddon::text("https://"))
.after(InputAddon::text(".com")),
),
cx,
))
.child(specimen(
"control addon",
div().w(px(200.0)).child(
InputGroup::new(&share).after(InputAddon::new(
IconButton::new("group-copy", IconName::Copy)
.size(ControlSize::Sm)
.tooltip("Copy"),
)),
),
cx,
)),
)
}