T06b forms selection: checkbox, radio, cards, switch, select, combobox, multi select, cascader, transfer list, list box, chips, rating, sliders, knob, stepper

This commit is contained in:
ZacharyZhang-NY committed 2026-09-25 06:41:35 -04:00
1 parent ec5713c10b
commit e39d623b58
36 files changed
+5170 -198

No files matched your search

+214
View File
@@ -0,0 +1,214 @@
use ely_gpui_component::{
forms::{
CheckState, Checkbox, CheckboxCard, CheckboxGroup, Choice, RadioCard, RadioGroup, Switch,
},
primitives::IconName,
};
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::{
probe::probe,
ui::{code, section, specimen},
};
pub(super) fn keep<T: 'static>(
key: &'static str,
init: impl FnOnce() -> T,
window: &mut Window,
cx: &mut App,
) -> Entity<T> {
window.use_keyed_state(key, cx, move |_, _| init())
}
pub(super) fn set<T: 'static>(state: &Entity<T>, value: T, cx: &mut App) {
state.update(cx, |state, cx| {
*state = value;
cx.notify();
});
}
fn values(list: &[&'static str]) -> Vec<SharedString> {
list.iter()
.map(|value| SharedString::from(*value))
.collect()
}
pub fn checkboxes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let channels = [
Choice::new("email", "Email"),
Choice::new("push", "Push"),
Choice::new("sms", "Text message").disabled(),
];
let open: Vec<SharedString> = channels
.iter()
.filter(|choice| !choice.disabled)
.map(|choice| choice.value.clone())
.collect();
let on = keep("check-channels", || values(&["email"]), window, cx);
let now = on.read(cx).clone();
let parent = match now.len() {
0 => CheckState::Off,
n if n == open.len() => CheckState::On,
_ => CheckState::Mixed,
};
let (all, some) = (on.clone(), on.clone());
section(
"Checkbox / CheckboxGroup",
"On, off, or mixed when a parent's children disagree. The check pops in.",
cx,
)
.child(probe(
"checks",
div()
.flex()
.flex_col()
.gap_2()
.w(px(280.0))
.child(
Checkbox::new("check-all", parent)
.label("All notifications")
.on_change(move |on, _, cx| {
set(&all, if on { open.clone() } else { Vec::new() }, cx)
}),
)
.child(
div().pl_6().child(
CheckboxGroup::new("check-group", channels)
.selected(now)
.on_change(move |next, _, cx| set(&some, next.to_vec(), cx)),
),
),
))
.child(code("Checkbox::new(id, CheckState::Mixed)", cx))
}
pub fn radios(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let plan = keep("radio-plan", || SharedString::from("pro"), window, cx);
let density = keep(
"radio-density",
|| SharedString::from("standard"),
window,
cx,
);
let (plan_now, density_now) = (plan.read(cx).clone(), density.read(cx).clone());
section(
"Radio / RadioGroup",
"One Tab stop per group; the arrow keys move the choice.",
cx,
)
.child(
div()
.flex()
.flex_col()
.gap_6()
.child(specimen(
"plan",
probe(
"radios",
RadioGroup::new(
"radio-plan",
[
Choice::new("free", "Free"),
Choice::new("pro", "Pro"),
Choice::new("team", "Team"),
Choice::new("enterprise", "Enterprise").disabled(),
],
)
.selected(plan_now)
.on_change(move |value, _, cx| set(&plan, value.clone(), cx)),
),
cx,
))
.child(specimen(
"density, in a row",
RadioGroup::new(
"radio-density",
[
Choice::new("compact", "Compact"),
Choice::new("standard", "Standard"),
Choice::new("comfortable", "Comfortable"),
],
)
.horizontal()
.selected(density_now)
.on_change(move |value, _, cx| set(&density, value.clone(), cx)),
cx,
)),
)
}
pub fn cards(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let space = keep("card-space", || SharedString::from("personal"), window, cx);
let backup = keep("card-backup", || true, window, cx);
let (space_now, backup_now) = (space.read(cx).clone(), *backup.read(cx));
let (personal, team) = (space.clone(), space);
section(
"CheckboxCard / RadioCard",
"Larger targets that explain a choice. The chosen border turns to ink.",
cx,
)
.child(probe(
"cards",
div()
.flex()
.gap_3()
.w(px(640.0))
.child(
div().flex_1().child(
RadioCard::new("card-personal", space_now == "personal", "Personal")
.description("Just you. Notes stay on this Mac.")
.icon(IconName::User)
.on_select(move |_, cx| set(&personal, "personal".into(), cx)),
),
)
.child(
div().flex_1().child(
RadioCard::new("card-team", space_now == "team", "Team")
.description("Shared with everyone you invite.")
.icon(IconName::Users)
.on_select(move |_, cx| set(&team, "team".into(), cx)),
),
),
))
.child(
div().w(px(640.0)).child(
CheckboxCard::new("card-backup", backup_now, "Back up nightly")
.description("Copies change sets to the cloud at 2 AM.")
.icon(IconName::Cloud)
.on_change(move |on, _, cx| set(&backup, on, cx)),
),
)
}
pub fn switches(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let wifi = keep("switch-wifi", || true, window, cx);
let sync = keep("switch-sync", || false, window, cx);
let (wifi_now, sync_now) = (*wifi.read(cx), *sync.read(cx));
section(
"Switch / Toggle",
"Takes effect at once. The thumb slides with a little overshoot.",
cx,
)
.child(probe(
"switches",
div()
.flex()
.flex_col()
.gap_3()
.child(
Switch::new("switch-wifi", wifi_now)
.label("Wi-Fi")
.on_change(move |on, _, cx| set(&wifi, on, cx)),
)
.child(
Switch::new("switch-sync", sync_now)
.label("Sync over cellular")
.on_change(move |on, _, cx| set(&sync, on, cx)),
)
.child(
Switch::new("switch-locked", true)
.label("Managed by your admin")
.disabled(true),
),
))
}
+85
View File
@@ -1,7 +1,10 @@
mod choices;
mod formats;
mod numbers;
mod pickers;
mod rich;
mod text;
mod values;
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
@@ -109,6 +112,74 @@ const SCRIPT: &[Step] = &[
Step::Wait(1200),
Step::CancelPanel,
Step::Wait(600),
Step::DownAt("checks", 40.0, 66.0),
Step::UpAt("checks", 40.0, 66.0),
Step::Wait(300),
Step::Shot("checks"),
Step::DownAt("radios", 30.0, 66.0),
Step::UpAt("radios", 30.0, 66.0),
Step::Wait(400),
Step::Shot("radios"),
Step::DownAt("cards", 480.0, 30.0),
Step::UpAt("cards", 480.0, 30.0),
Step::Wait(400),
Step::Shot("cards"),
Step::DownAt("switches", 16.0, 39.0),
Step::UpAt("switches", 16.0, 39.0),
Step::Wait(600),
Step::Shot("switches"),
Step::Click("select"),
Step::Wait(300),
Step::Key("down"),
Step::Wait(200),
Step::Shot("select-open"),
Step::Key("enter"),
Step::Wait(300),
Step::Click("combobox"),
Step::Key("secondary-a"),
Step::Type("lis"),
Step::Wait(300),
Step::Shot("combobox"),
Step::Key("enter"),
Step::Wait(200),
Step::Click("multi"),
Step::Wait(300),
Step::Key("down"),
Step::Key("enter"),
Step::Wait(300),
Step::Shot("multi"),
Step::Key("escape"),
Step::Click("cascader"),
Step::Wait(300),
Step::Key("down"),
Step::Key("right"),
Step::Wait(300),
Step::Shot("cascader"),
Step::Key("escape"),
Step::DownAt("transfer", 60.0, 45.0),
Step::UpAt("transfer", 60.0, 45.0),
Step::Wait(200),
Step::DownAt("transfer", 280.0, 134.0),
Step::UpAt("transfer", 280.0, 134.0),
Step::Wait(300),
Step::Shot("transfer"),
Step::DownAt("chips", 120.0, 14.0),
Step::UpAt("chips", 120.0, 14.0),
Step::DownAt("rating", 79.0, 13.0),
Step::UpAt("rating", 79.0, 13.0),
Step::Wait(300),
Step::Shot("chips-rating"),
Step::DownAt("slider", 130.0, 8.0),
Step::DragTo("slider", 220.0, 8.0),
Step::UpAt("slider", 220.0, 8.0),
Step::Wait(200),
Step::DownAt("knob", 24.0, 24.0),
Step::DragTo("knob", 24.0, -36.0),
Step::UpAt("knob", 24.0, -36.0),
Step::DownAt("stepper", 76.0, 16.0),
Step::UpAt("stepper", 76.0, 16.0),
Step::Wait(300),
Step::Shot("values"),
Step::Rest,
];
@@ -133,5 +204,19 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
.child(rich::regex(window, cx))
.child(rich::expression(window, cx))
.child(rich::hotkey(window, cx))
.child(choices::checkboxes(window, cx))
.child(choices::radios(window, cx))
.child(choices::cards(window, cx))
.child(choices::switches(window, cx))
.child(pickers::select(window, cx))
.child(pickers::list_box(window, cx))
.child(pickers::combobox(window, cx))
.child(pickers::multi_select(window, cx))
.child(pickers::cascader(window, cx))
.child(pickers::transfer(window, cx))
.child(values::chips(window, cx))
.child(values::rating(window, cx))
.child(values::sliders(window, cx))
.child(values::dials(window, cx))
.into_any_element()
}
+235
View File
@@ -0,0 +1,235 @@
use ely_gpui_component::{
forms::{Cascade, Cascader, Choice, Combobox, ListBox, MultiSelect, Select, TransferList},
primitives::IconName,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use super::{
choices::{keep, set},
text::field,
};
use crate::{
probe::probe,
ui::{blocked, section, specimen, specimens},
};
fn cities() -> Vec<Choice> {
[
("ams", "Amsterdam"),
("ber", "Berlin"),
("cph", "Copenhagen"),
("lis", "Lisbon"),
("lon", "London"),
("osl", "Oslo"),
("par", "Paris"),
("vie", "Vienna"),
]
.into_iter()
.map(|(value, label)| Choice::new(value, label))
.collect()
}
pub fn select(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let view = keep("select-view", || SharedString::from("list"), window, cx);
let now = view.read(cx).clone();
section(
"Select / NativeSelect",
"A button that opens its list. Arrows move, Enter picks, Escape closes.",
cx,
)
.child(
specimens()
.child(specimen(
"view",
probe(
"select",
div().w(px(240.0)).child(
Select::new(
"select-view",
[
Choice::new("list", "List").icon(IconName::List),
Choice::new("board", "Board").icon(IconName::Kanban),
Choice::new("calendar", "Calendar").icon(IconName::Calendar),
Choice::new("gallery", "Gallery")
.icon(IconName::LayoutGrid)
.disabled(),
],
)
.selected(now)
.on_change(move |value, _, cx| set(&view, value.clone(), cx)),
),
),
cx,
))
.child(specimen(
"disabled",
div().w(px(200.0)).child(
Select::new("select-off", [Choice::new("utc", "UTC")])
.selected("utc")
.disabled(true),
),
cx,
)),
)
.child(blocked(
"NativeSelect: gpui 0.2.2 has no native popup menu, so Select draws its own.",
cx,
))
}
pub fn list_box(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let picked = keep(
"listbox-cities",
|| vec![SharedString::from("lis")],
window,
cx,
);
let now = picked.read(cx).clone();
section(
"ListBox",
"A list chosen from in place. Focus it and use the arrows, Space or Enter.",
cx,
)
.child(
div().w(px(240.0)).child(
ListBox::new("listbox-cities", cities())
.selected(now)
.on_change(move |next, _, cx| set(&picked, next.to_vec(), cx)),
),
)
}
pub fn combobox(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let city = field("input-city", window, cx, |input| {
input.placeholder("Search cities")
});
let tag = field("input-tag", window, cx, |input| {
input.placeholder("Any label")
});
let chosen = keep("combobox-city", || None::<SharedString>, window, cx);
let now = chosen.read(cx).clone();
let labels: Vec<Choice> = ["design", "docs", "infra", "motion", "release"]
.into_iter()
.map(|label| Choice::new(label, label))
.collect();
let mut combo = Combobox::new("combobox-city", &city, cities())
.on_change(move |value, _, cx| set(&chosen, Some(value.clone()), cx));
if let Some(value) = now {
combo = combo.selected(value);
}
section(
"Combobox / Autocomplete",
"Type to filter. A combobox keeps a listed value; an autocomplete keeps any text.",
cx,
)
.child(
specimens()
.child(specimen(
"combobox",
probe("combobox", div().w(px(260.0)).child(combo)),
cx,
))
.child(specimen(
"autocomplete",
div()
.w(px(260.0))
.child(Combobox::new("combobox-tag", &tag, labels).free()),
cx,
)),
)
}
pub fn multi_select(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let picked = keep(
"multi-cities",
|| vec![SharedString::from("ber"), SharedString::from("osl")],
window,
cx,
);
let now = picked.read(cx).clone();
section(
"MultiSelect",
"Several values as chips. The list stays open while you pick; Backspace drops the last.",
cx,
)
.child(probe(
"multi",
div().w(px(360.0)).child(
MultiSelect::new("multi-cities", cities())
.selected(now)
.placeholder("Pick cities")
.on_change(move |next, _, cx| set(&picked, next.to_vec(), cx)),
),
))
}
pub fn cascader(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let path = keep(
"cascade-office",
|| {
vec![
SharedString::from("eu"),
SharedString::from("pt"),
SharedString::from("lis"),
]
},
window,
cx,
);
let now = path.read(cx).clone();
let world = [
Cascade::new("am", "Americas")
.child(
Cascade::new("us", "United States")
.child(Cascade::new("nyc", "New York"))
.child(Cascade::new("sfo", "San Francisco")),
)
.child(Cascade::new("br", "Brazil").child(Cascade::new("sao", "São Paulo"))),
Cascade::new("eu", "Europe")
.child(
Cascade::new("pt", "Portugal")
.child(Cascade::new("lis", "Lisbon"))
.child(Cascade::new("opo", "Porto")),
)
.child(Cascade::new("de", "Germany").child(Cascade::new("ber", "Berlin"))),
Cascade::new("as", "Asia")
.child(Cascade::new("jp", "Japan").child(Cascade::new("tyo", "Tokyo"))),
];
section(
"CascadeSelect / Cascader",
"Each column opens the next. Right and Left walk the levels; a leaf picks the path.",
cx,
)
.child(probe(
"cascader",
div().w(px(320.0)).child(
Cascader::new("cascade-office", world)
.selected(now)
.on_change(move |next, _, cx| set(&path, next.to_vec(), cx)),
),
))
}
pub fn transfer(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let chosen = keep(
"transfer-cities",
|| vec![SharedString::from("par")],
window,
cx,
);
let now = chosen.read(cx).clone();
section(
"TransferList",
"Mark rows on one side, then move them across.",
cx,
)
.child(probe(
"transfer",
div().w(px(560.0)).child(
TransferList::new("transfer-cities", cities())
.chosen(now)
.titles("Cities", "On the tour")
.on_change(move |next, _, cx| set(&chosen, next.to_vec(), cx)),
),
))
}
+176
View File
@@ -0,0 +1,176 @@
use ely_gpui_component::forms::{Choice, ChoiceChips, Knob, RangeSlider, Rating, Slider, Stepper};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use super::choices::{keep, set};
use crate::{
probe::probe,
ui::{section, specimen, specimens},
};
pub fn chips(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let sort = keep("chips-sort", || vec![SharedString::from("new")], window, cx);
let teams = keep(
"chips-teams",
|| vec![SharedString::from("design")],
window,
cx,
);
let (sort_now, teams_now) = (sort.read(cx).clone(), teams.read(cx).clone());
section(
"ChoiceChips / FilterChips",
"Pills for a few options. Filter chips toggle and show a check.",
cx,
)
.child(
div()
.flex()
.flex_col()
.gap_6()
.child(specimen(
"choice",
probe(
"chips",
ChoiceChips::new(
"chips-sort",
[
Choice::new("new", "Newest"),
Choice::new("top", "Popular"),
Choice::new("old", "Oldest"),
],
)
.selected(sort_now)
.on_change(move |next, _, cx| set(&sort, next.to_vec(), cx)),
),
cx,
))
.child(specimen(
"filter",
ChoiceChips::new(
"chips-teams",
[
Choice::new("design", "Design"),
Choice::new("eng", "Engineering"),
Choice::new("research", "Research"),
Choice::new("ops", "Operations").disabled(),
],
)
.multiple()
.selected(teams_now)
.on_change(move |next, _, cx| set(&teams, next.to_vec(), cx)),
cx,
)),
)
}
pub fn rating(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let stars = keep("rating-stars", || 3u8, window, cx);
let now = *stars.read(cx);
section(
"Rating",
"Hover previews, a click sets it, and clicking the same star clears it.",
cx,
)
.child(
specimens()
.child(specimen(
format!("{now} of 5"),
probe(
"rating",
Rating::new("rating-stars", now)
.on_change(move |next, _, cx| set(&stars, next, cx)),
),
cx,
))
.child(specimen(
"read only",
Rating::new("rating-fixed", 4).disabled(true),
cx,
)),
)
}
pub fn sliders(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let volume = keep("slider-volume", || 40.0, window, cx);
let level = keep("slider-level", || 60.0, window, cx);
let price = keep("slider-price", || (20.0, 80.0), window, cx);
let (volume_now, level_now, (low, high)) = (*volume.read(cx), *level.read(cx), *price.read(cx));
section(
"Slider / RangeSlider / VerticalSlider",
"Drag a thumb, click the track, or use the arrows; Page keys jump ten steps.",
cx,
)
.child(
specimens()
.child(specimen(
format!("volume {volume_now:.0}"),
probe(
"slider",
div().w(px(320.0)).child(
Slider::new("slider-volume", volume_now)
.on_change(move |next, _, cx| set(&volume, next, cx)),
),
),
cx,
))
.child(specimen(
format!("price {low:.0} to {high:.0}"),
div().w(px(320.0)).child(
RangeSlider::new("slider-price", low, high)
.step(5.0)
.on_change(move |next, _, cx| set(&price, next, cx)),
),
cx,
))
.child(specimen(
format!("level {level_now:.0}"),
div().h(px(140.0)).child(
Slider::new("slider-level", level_now)
.vertical()
.step(5.0)
.on_change(move |next, _, cx| set(&level, next, cx)),
),
cx,
)),
)
}
pub fn dials(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let gain = keep("knob-gain", || 65.0, window, cx);
let pan = keep("knob-pan", || 0.0, window, cx);
let guests = keep("stepper-guests", || 2.0, window, cx);
let (gain_now, pan_now, guests_now) = (*gain.read(cx), *pan.read(cx), *guests.read(cx));
section(
"Knob / Dial / Stepper",
"Drag a knob up or down, or use the arrows. The stepper counts without typing.",
cx,
)
.child(
specimens()
.child(specimen(
format!("gain {gain_now:.0}"),
probe(
"knob",
Knob::new("knob-gain", gain_now)
.on_change(move |next, _, cx| set(&gain, next, cx)),
),
cx,
))
.child(specimen(
format!("pan {pan_now:.0}"),
Knob::new("knob-pan", pan_now)
.range(-50.0, 50.0)
.on_change(move |next, _, cx| set(&pan, next, cx)),
cx,
))
.child(specimen(
"guests",
probe(
"stepper",
Stepper::new("stepper-guests", guests_now)
.range(1.0, 10.0)
.on_change(move |next, _, cx| set(&guests, next, cx)),
),
cx,
)),
)
}