Tooling · Workbench: the knobs seed the settings; knobs checked
The knobs join a layout::seeded seed, so a changed starting value, option list or range starts every setting over, and a renamed knob no longer fails. new() refuses no knobs, twin names and a number outside its range. Controls are keyed by knob name. The test presses keys without a refresh, so only a notify redraws, and walks the switch twice, the slider twice and a pick the owner takes away.
This commit is contained in:
2 files changed
+129
-42
No files matched your search
+86
-33
@@ -1,12 +1,13 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString,
|
AnyElement, App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement,
|
||||||
Styled, Window, div,
|
RenderOnce, SharedString, Styled, Window, div,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
forms::{Choice, Select, Slider, Switch},
|
forms::{Choice, Select, Slider, Switch},
|
||||||
|
layout::seeded::use_seeded,
|
||||||
theme::{ActiveTheme, Radius, TextSize},
|
theme::{ActiveTheme, Radius, TextSize},
|
||||||
typography::{format, tabular},
|
typography::{format, tabular},
|
||||||
};
|
};
|
||||||
@@ -14,7 +15,7 @@ use crate::{
|
|||||||
type Preview = Rc<dyn Fn(&Settings, &mut Window, &mut App) -> AnyElement>;
|
type Preview = Rc<dyn Fn(&Settings, &mut Window, &mut App) -> AnyElement>;
|
||||||
|
|
||||||
/// A property a playground edits: a switch, a choice among names, or a number in a range, each by its name.
|
/// A property a playground edits: a switch, a choice among names, or a number in a range, each by its name.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum Knob {
|
pub enum Knob {
|
||||||
Toggle(SharedString, bool),
|
Toggle(SharedString, bool),
|
||||||
Choice(SharedString, Vec<SharedString>, usize),
|
Choice(SharedString, Vec<SharedString>, usize),
|
||||||
@@ -96,7 +97,7 @@ impl Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A component beside the knobs that shape it: each knob edits one property as the preview redraws with the settings. Narrow, the knobs drop below.
|
/// A component beside the knobs that shape it: each knob edits one property as the preview redraws with the settings. The knobs are the owner's: when any changes, every setting starts over from them. Narrow, the knobs drop below.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Playground {
|
pub struct Playground {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
@@ -105,14 +106,32 @@ pub struct Playground {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Playground {
|
impl Playground {
|
||||||
|
/// Fails on no knobs, two knobs of one name, or a number that starts outside its range.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
id: impl Into<ElementId>,
|
id: impl Into<ElementId>,
|
||||||
knobs: impl IntoIterator<Item = Knob>,
|
knobs: impl IntoIterator<Item = Knob>,
|
||||||
preview: impl Fn(&Settings, &mut Window, &mut App) -> AnyElement + 'static,
|
preview: impl Fn(&Settings, &mut Window, &mut App) -> AnyElement + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let (id, knobs): (ElementId, Vec<Knob>) = (id.into(), knobs.into_iter().collect());
|
||||||
|
assert!(!knobs.is_empty(), "playground {id:?} has no knobs");
|
||||||
|
for (ix, knob) in knobs.iter().enumerate() {
|
||||||
|
assert!(
|
||||||
|
knobs[..ix]
|
||||||
|
.iter()
|
||||||
|
.all(|before| before.name() != knob.name()),
|
||||||
|
"playground {id:?} has two knobs named {}",
|
||||||
|
knob.name()
|
||||||
|
);
|
||||||
|
if let Knob::Number(name, value, (least, most), _) = knob {
|
||||||
|
assert!(
|
||||||
|
(*least..=*most).contains(value),
|
||||||
|
"playground {id:?}: {name} starts at {value}, outside {least}..={most}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id,
|
||||||
knobs: knobs.into_iter().collect(),
|
knobs,
|
||||||
preview: Rc::new(preview),
|
preview: Rc::new(preview),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,35 +140,37 @@ impl Playground {
|
|||||||
impl RenderOnce for Playground {
|
impl RenderOnce for Playground {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
assert!(!self.knobs.is_empty(), "playground {id:?} has no knobs");
|
let firsts = Settings(
|
||||||
let settings = window.use_keyed_state((id.clone(), "settings"), cx, {
|
self.knobs
|
||||||
let knobs = self.knobs.clone();
|
.iter()
|
||||||
move |_, _| {
|
.map(|knob| (knob.name().clone(), knob.first()))
|
||||||
Settings(
|
.collect(),
|
||||||
knobs
|
);
|
||||||
.iter()
|
// The knobs join the seed, so a changed option list or range starts over too.
|
||||||
.map(|knob| (knob.name().clone(), knob.first()))
|
let settings = use_seeded(
|
||||||
.collect(),
|
(id.clone(), "settings"),
|
||||||
)
|
(self.knobs.clone(), firsts),
|
||||||
}
|
window,
|
||||||
});
|
cx,
|
||||||
let now = settings.read(cx).clone();
|
);
|
||||||
|
let now = settings.read(cx).value.1.clone();
|
||||||
let preview = (self.preview)(&now, window, cx);
|
let preview = (self.preview)(&now, window, cx);
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let colors = &theme.colors;
|
let colors = &theme.colors;
|
||||||
let rows = self.knobs.iter().enumerate().map(|(ix, knob)| {
|
let rows = self.knobs.iter().map(|knob| {
|
||||||
let (name, set) = (knob.name().clone(), settings.clone());
|
let (name, set) = (knob.name().clone(), settings.clone());
|
||||||
|
let key = (id.clone(), format!("knob-{name}"));
|
||||||
let control = match knob {
|
let control = match knob {
|
||||||
Knob::Toggle(..) => Switch::new((id.clone(), format!("knob-{ix}")), now.on(&name))
|
Knob::Toggle(..) => Switch::new(key, now.on(&name))
|
||||||
.on_change(move |on, _, cx| {
|
.on_change(move |on, _, cx| {
|
||||||
set.update(cx, |settings, cx| {
|
set.update(cx, |settings, cx| {
|
||||||
settings.set(&name, Setting::On(on));
|
settings.value.1.set(&name, Setting::On(on));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
Knob::Choice(_, options, _) => Select::new(
|
Knob::Choice(_, options, _) => Select::new(
|
||||||
(id.clone(), format!("knob-{ix}")),
|
key,
|
||||||
options
|
options
|
||||||
.iter()
|
.iter()
|
||||||
.map(|option| Choice::new(option.clone(), option.clone())),
|
.map(|option| Choice::new(option.clone(), option.clone())),
|
||||||
@@ -157,7 +178,7 @@ impl RenderOnce for Playground {
|
|||||||
.selected(now.picked(&name).clone())
|
.selected(now.picked(&name).clone())
|
||||||
.on_change(move |option, _, cx| {
|
.on_change(move |option, _, cx| {
|
||||||
set.update(cx, |settings, cx| {
|
set.update(cx, |settings, cx| {
|
||||||
settings.set(&name, Setting::Picked(option.clone()));
|
settings.value.1.set(&name, Setting::Picked(option.clone()));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -168,27 +189,29 @@ impl RenderOnce for Playground {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
div().flex_1().min_w_0().child(
|
div().flex_1().min_w_0().child(
|
||||||
Slider::new((id.clone(), format!("knob-{ix}")), now.number(&name))
|
Slider::new(key, now.number(&name))
|
||||||
.range(*least, *most)
|
.range(*least, *most)
|
||||||
.step(*step)
|
.step(*step)
|
||||||
.on_change(move |value, _, cx| {
|
.on_change(move |value, _, cx| {
|
||||||
set.update(cx, |settings, cx| {
|
set.update(cx, |settings, cx| {
|
||||||
settings.set(&name, Setting::Number(value));
|
settings.value.1.set(&name, Setting::Number(value));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child({
|
||||||
|
let shown = format::number(
|
||||||
|
now.number(knob.name()),
|
||||||
|
format::decimals(*step),
|
||||||
|
format::Separators::EN,
|
||||||
|
);
|
||||||
tabular(div())
|
tabular(div())
|
||||||
|
.debug_selector(|| format!("knob-readout-{}-{shown}", knob.name()))
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.text_color(colors.fg_muted)
|
.text_color(colors.fg_muted)
|
||||||
.child(format::number(
|
.child(shown.clone())
|
||||||
now.number(knob.name()),
|
})
|
||||||
format::decimals(*step),
|
|
||||||
format::Separators::EN,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
};
|
};
|
||||||
div()
|
div()
|
||||||
@@ -236,6 +259,8 @@ impl RenderOnce for Playground {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use gpui::IntoElement;
|
||||||
|
|
||||||
use super::{Knob, Setting, Settings};
|
use super::{Knob, Setting, Settings};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -257,6 +282,34 @@ mod tests {
|
|||||||
assert_eq!(settings.number("share"), 0.7);
|
assert_eq!(settings.number("share"), 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "has no knobs")]
|
||||||
|
fn a_playground_without_knobs_fails() {
|
||||||
|
super::Playground::new("p", [], |_, _, _| gpui::div().into_any_element());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "has two knobs named size")]
|
||||||
|
fn twin_knob_names_fail() {
|
||||||
|
let size = || Knob::Toggle("size".into(), true);
|
||||||
|
super::Playground::new("p", [size(), size()], |_, _, _| {
|
||||||
|
gpui::div().into_any_element()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "share starts at 5, outside 0..=1")]
|
||||||
|
fn a_number_outside_its_range_fails() {
|
||||||
|
let share = Knob::Number("share".into(), 5.0, (0.0, 1.0), 0.1);
|
||||||
|
super::Playground::new("p", [share], |_, _, _| gpui::div().into_any_element());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "no knob named color")]
|
||||||
|
fn a_name_the_knobs_lack_fails() {
|
||||||
|
Settings(vec![("size".into(), Setting::Picked("Md".into()))]).on("color");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "playground knob size is Picked")]
|
#[should_panic(expected = "playground knob size is Picked")]
|
||||||
fn a_setting_asked_in_another_kind_fails() {
|
fn a_setting_asked_in_another_kind_fails() {
|
||||||
|
|||||||
+43
-9
@@ -3,6 +3,8 @@ use gpui::{
|
|||||||
ParentElement, Render, Styled, TestAppContext, VisualTestContext, Window, div, px,
|
ParentElement, Render, Styled, TestAppContext, VisualTestContext, Window, div, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
use super::{Knob, Playground};
|
use super::{Knob, Playground};
|
||||||
use crate::{
|
use crate::{
|
||||||
forms,
|
forms,
|
||||||
@@ -23,6 +25,10 @@ impl Render for Stage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static LARGE: Cell<bool> = const { Cell::new(true) };
|
||||||
|
}
|
||||||
|
|
||||||
fn playground() -> AnyElement {
|
fn playground() -> AnyElement {
|
||||||
Playground::new(
|
Playground::new(
|
||||||
"playground",
|
"playground",
|
||||||
@@ -30,14 +36,17 @@ fn playground() -> AnyElement {
|
|||||||
Knob::Toggle("disabled".into(), false),
|
Knob::Toggle("disabled".into(), false),
|
||||||
Knob::Choice(
|
Knob::Choice(
|
||||||
"size".into(),
|
"size".into(),
|
||||||
vec!["Sm".into(), "Md".into(), "Lg".into()],
|
["Sm", "Md", "Lg"][..2 + usize::from(LARGE.get())]
|
||||||
|
.iter()
|
||||||
|
.map(|size| (*size).into())
|
||||||
|
.collect(),
|
||||||
1,
|
1,
|
||||||
),
|
),
|
||||||
Knob::Number("share".into(), 0.4, (0.0, 1.0), 0.1),
|
Knob::Number("share".into(), 0.5, (0.0, 1.0), 0.25),
|
||||||
],
|
],
|
||||||
|settings, _, _| {
|
|settings, _, _| {
|
||||||
let shown = format!(
|
let shown = format!(
|
||||||
"preview-{}-{}-{:.1}",
|
"preview-{}-{}-{:.2}",
|
||||||
settings.on("disabled"),
|
settings.on("disabled"),
|
||||||
settings.picked("size"),
|
settings.picked("size"),
|
||||||
settings.number("share")
|
settings.number("share")
|
||||||
@@ -56,13 +65,14 @@ fn settle(cx: &mut VisualTestContext) {
|
|||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Presses a key with no refresh of its own, so only what notifies redraws.
|
||||||
fn press(key: &str, cx: &mut VisualTestContext) {
|
fn press(key: &str, cx: &mut VisualTestContext) {
|
||||||
cx.simulate_keystrokes(key);
|
cx.simulate_keystrokes(key);
|
||||||
settle(cx);
|
cx.run_until_parked();
|
||||||
cx.simulate_event(KeyUpEvent {
|
cx.simulate_event(KeyUpEvent {
|
||||||
keystroke: Keystroke::parse(key).expect("a key"),
|
keystroke: Keystroke::parse(key).expect("a key"),
|
||||||
});
|
});
|
||||||
settle(cx);
|
cx.run_until_parked();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Each knob's edit redraws the preview with the new settings.
|
/// Each knob's edit redraws the preview with the new settings.
|
||||||
@@ -84,16 +94,40 @@ fn knobs_redraw_the_preview(cx: &mut TestAppContext) {
|
|||||||
settle(cx);
|
settle(cx);
|
||||||
let shown =
|
let shown =
|
||||||
|selector: &'static str, cx: &mut VisualTestContext| cx.debug_bounds(selector).is_some();
|
|selector: &'static str, cx: &mut VisualTestContext| cx.debug_bounds(selector).is_some();
|
||||||
assert!(shown("preview-false-Md-0.4", cx));
|
assert!(shown("preview-false-Md-0.50", cx));
|
||||||
press("tab", cx);
|
press("tab", cx);
|
||||||
press("space", cx);
|
press("space", cx);
|
||||||
assert!(shown("preview-true-Md-0.4", cx), "the switch turned");
|
assert!(shown("preview-true-Md-0.50", cx), "the switch turned");
|
||||||
press("tab", cx);
|
press("tab", cx);
|
||||||
for key in ["down", "down", "enter"] {
|
for key in ["down", "down", "enter"] {
|
||||||
press(key, cx);
|
press(key, cx);
|
||||||
}
|
}
|
||||||
assert!(shown("preview-true-Lg-0.4", cx), "the choice moved");
|
assert!(shown("preview-true-Lg-0.50", cx), "the choice moved");
|
||||||
press("tab", cx);
|
press("tab", cx);
|
||||||
press("right", cx);
|
press("right", cx);
|
||||||
assert!(shown("preview-true-Lg-0.5", cx), "the number stepped");
|
press("right", cx);
|
||||||
|
assert!(
|
||||||
|
shown("preview-true-Lg-1.00", cx),
|
||||||
|
"the number stepped twice"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
shown("knob-readout-share-1.00", cx),
|
||||||
|
"its readout in the step's places"
|
||||||
|
);
|
||||||
|
press("shift-tab", cx);
|
||||||
|
press("shift-tab", cx);
|
||||||
|
press("space", cx);
|
||||||
|
assert!(
|
||||||
|
shown("preview-false-Lg-1.00", cx),
|
||||||
|
"the switch showed its own setting, so a second press turned it back"
|
||||||
|
);
|
||||||
|
LARGE.set(false);
|
||||||
|
settle(cx);
|
||||||
|
press("tab", cx);
|
||||||
|
press("tab", cx);
|
||||||
|
press("right", cx);
|
||||||
|
assert!(
|
||||||
|
shown("preview-false-Md-0.75", cx),
|
||||||
|
"the owner took away the picked size, so every setting started over"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user