T06a forms text: text input core, fields, numbers, masks, pin, inline, groups, tags, pairs, path, regex, expression, hotkey, mention
This commit is contained in:
41 files changed
+6233
-45
No files matched your search
@@ -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();
|
||||
})
|
||||
})),
|
||||
))
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
@@ -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.",
|
||||
))
|
||||
}
|
||||
@@ -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(¬es)),
|
||||
))
|
||||
.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,
|
||||
)),
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod buttons;
|
||||
mod forms;
|
||||
mod layout;
|
||||
mod primitives;
|
||||
mod shell;
|
||||
@@ -25,6 +26,7 @@ pub const ALL: &[Page] = &[
|
||||
layout::PAGE,
|
||||
shell::PAGE,
|
||||
buttons::PAGE,
|
||||
forms::PAGE,
|
||||
theme::PAGE,
|
||||
];
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ pub enum Step {
|
||||
DragTo(&'static str, f32, f32),
|
||||
UpAt(&'static str, f32, f32),
|
||||
Key(&'static str),
|
||||
/// Types text into whatever holds focus, one key at a time.
|
||||
Type(&'static str),
|
||||
Wait(u64),
|
||||
Shot(&'static str),
|
||||
/// Photographs a window a demo opened, by its key.
|
||||
@@ -39,6 +41,8 @@ pub enum Step {
|
||||
ShotPopup(&'static str),
|
||||
/// Posts a key to AppKit itself; only `escape` is known.
|
||||
NativeKey(&'static str),
|
||||
/// Cancels the open file panel, whose content runs out of process.
|
||||
CancelPanel,
|
||||
}
|
||||
|
||||
const FRAME: Duration = Duration::from_millis(120);
|
||||
@@ -125,9 +129,24 @@ pub async fn play(
|
||||
Step::Key(stroke) => {
|
||||
let stroke =
|
||||
Keystroke::parse(stroke).with_context(|| format!("bad keystroke {stroke}"))?;
|
||||
window.update(cx, |_, window, cx| {
|
||||
window.dispatch_keystroke(stroke, cx);
|
||||
})?;
|
||||
press(window.into(), stroke, cx)?;
|
||||
}
|
||||
Step::Type(text) => {
|
||||
for ch in text.chars() {
|
||||
let key = match ch {
|
||||
' ' => "space".to_string(),
|
||||
ch if ch.is_ascii_uppercase() => {
|
||||
format!("shift-{}", ch.to_ascii_lowercase())
|
||||
}
|
||||
ch => ch.to_string(),
|
||||
};
|
||||
let stroke =
|
||||
Keystroke::parse(&key).with_context(|| format!("bad key {key}"))?;
|
||||
press(window.into(), stroke, cx)?;
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(16))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Step::Wait(ms) => {
|
||||
cx.background_executor()
|
||||
@@ -148,9 +167,7 @@ pub async fn play(
|
||||
Step::KeyWindow(key, stroke) => {
|
||||
let stroke =
|
||||
Keystroke::parse(stroke).with_context(|| format!("bad keystroke {stroke}"))?;
|
||||
opened(key, cx)?.update(cx, |_, window, cx| {
|
||||
window.dispatch_keystroke(stroke, cx);
|
||||
})?;
|
||||
press(opened(key, cx)?, stroke, cx)?;
|
||||
}
|
||||
Step::CloseNative(key) => {
|
||||
let number = opened(key, cx)?.update(cx, |_, window, _| number(window))??;
|
||||
@@ -166,6 +183,7 @@ pub async fn play(
|
||||
let number = window.update(cx, |_, window, _| number(window))??;
|
||||
post_escape(number)?;
|
||||
}
|
||||
Step::CancelPanel => cancel_panel()?,
|
||||
Step::ExpectClosed(key) => {
|
||||
let handle = opened(key, cx)?;
|
||||
let open = cx.update(|cx| cx.windows().contains(&handle))?;
|
||||
@@ -184,6 +202,13 @@ pub async fn play(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Types a keystroke without holding the root view, so the window can redraw.
|
||||
fn press(window: gpui::AnyWindowHandle, stroke: Keystroke, cx: &mut AsyncApp) -> Result<()> {
|
||||
window.update(cx, |_, window, cx| {
|
||||
window.dispatch_keystroke(stroke, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn send(
|
||||
window: WindowHandle<Gallery>,
|
||||
kind: Mouse,
|
||||
@@ -263,6 +288,28 @@ fn post_escape(_: u32) -> Result<()> {
|
||||
anyhow::bail!("native keys need macOS")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn cancel_panel() -> Result<()> {
|
||||
use cocoa::{
|
||||
appkit::NSApp,
|
||||
base::{BOOL, NO, id, nil},
|
||||
};
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
unsafe {
|
||||
let key: id = msg_send![NSApp(), keyWindow];
|
||||
anyhow::ensure!(key != nil, "no key window to cancel");
|
||||
let panel: BOOL = msg_send![key, isKindOfClass: class!(NSSavePanel)];
|
||||
anyhow::ensure!(panel != NO, "the key window is not a file panel");
|
||||
let _: () = msg_send![key, cancel: nil];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn cancel_panel() -> Result<()> {
|
||||
anyhow::bail!("file panels need macOS")
|
||||
}
|
||||
|
||||
/// Runs `performClose:` on a window, which asks before closing.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn perform_close(number: u32) -> Result<()> {
|
||||
|
||||
Reference in new issue
Block a user