Settings: a settings window, its sections, a search, and what fills them
SettingsLayout lists sections down the side and shows the open one, the list going above the page in a narrow box; SettingsSection and SettingsRow title and hold settings, a row's control wrapping under its name. SettingsSearch finds a setting by its name or words and opens its section. ThemeSelector, AccentColorPicker, FontSizeControl and DensitySelector set the look; KeyboardShortcutsList finds shortcuts by every word; ProxySettings, PrivacySettings, NotificationSettings, StartupSettings, StorageSettings with its clear, ImportExportSettings, ResetToDefault, FeatureFlags and DeveloperModeToggle fill the rest. Every handler a control needs is required and fails loud when missing. ColorPalette wraps its swatches in a row capped at its columns, so it keeps their size in a box sized by its content, and its swatches carry the palette's id. ThemeSwitcher points here: src/theme sits under the chapters that draw with buttons.
This commit is contained in:
1 parent
27c7cd965b
commit
380cdb0676
18 files changed
+2873
-27
No files matched your search
@@ -0,0 +1,432 @@
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, IntoElement, KeyBinding, ParentElement, Render, Styled,
|
||||
TestAppContext, VisualTestContext, Window, div, hsla, px,
|
||||
};
|
||||
|
||||
use super::{
|
||||
AccentColorPicker, Appearance, Changed, DeveloperModeToggle, FeatureFlags, Flag,
|
||||
FontSizeControl, ImportExportSettings, Notices, NotificationSettings, Privacy, PrivacySettings,
|
||||
Proxy, ProxySettings, ResetToDefault, SettingEntry, SettingsLayout, SettingsSearch, Stage,
|
||||
Startup, StartupSettings, StorageSettings, Store, ThemeSelector,
|
||||
};
|
||||
use crate::{
|
||||
forms::{self, Choice, TextInput},
|
||||
primitives::{FocusNext, IconName},
|
||||
theme::Theme,
|
||||
};
|
||||
|
||||
type Part = fn(&mut Window, &mut App, Entity<Desk>) -> AnyElement;
|
||||
|
||||
/// A view that shows one setting's part and keeps the words it heard.
|
||||
struct Desk {
|
||||
part: Part,
|
||||
said: Vec<String>,
|
||||
}
|
||||
|
||||
impl Render for Desk {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let owner = cx.entity();
|
||||
let part = (self.part)(window, cx, owner);
|
||||
div().w(px(640.0)).child(part)
|
||||
}
|
||||
}
|
||||
|
||||
fn settle(cx: &mut VisualTestContext) {
|
||||
for _ in 0..3 {
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, _| window.refresh());
|
||||
}
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn desk(part: Part, cx: &mut TestAppContext) -> (Entity<Desk>, &mut VisualTestContext) {
|
||||
cx.update(|cx| {
|
||||
Theme::init(cx);
|
||||
Theme::update(cx, |theme| theme.reduced_motion = true);
|
||||
forms::bind_keys(cx);
|
||||
cx.bind_keys([KeyBinding::new("tab", FocusNext, None)]);
|
||||
});
|
||||
let (host, cx) = cx.add_window_view(|_, _| Desk {
|
||||
part,
|
||||
said: Vec::new(),
|
||||
});
|
||||
settle(cx);
|
||||
(host, cx)
|
||||
}
|
||||
|
||||
fn say(owner: &Entity<Desk>, words: String, cx: &mut App) {
|
||||
owner.update(cx, |desk, cx| {
|
||||
desk.said.push(words);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
fn said(host: &Entity<Desk>, cx: &mut VisualTestContext) -> Vec<String> {
|
||||
host.read_with(cx, |desk, _| desk.said.clone())
|
||||
}
|
||||
|
||||
fn tab(stops: usize, cx: &mut VisualTestContext) {
|
||||
cx.update(|window, _| {
|
||||
window.blur();
|
||||
(0..stops).for_each(|_| window.focus_next());
|
||||
});
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
fn tap(key: &str, cx: &mut VisualTestContext) {
|
||||
cx.simulate_keystrokes(key);
|
||||
cx.simulate_event(gpui::KeyUpEvent {
|
||||
keystroke: gpui::Keystroke::parse(key).expect("a key"),
|
||||
});
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
fn search(window: &mut Window, cx: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
let field = window.use_keyed_state("search", cx, TextInput::new);
|
||||
let entry = |key: &str, name: &str, words: &[&str]| SettingEntry {
|
||||
key: key.to_string().into(),
|
||||
section: "Appearance".into(),
|
||||
name: name.to_string().into(),
|
||||
words: words.iter().map(|word| word.to_string().into()).collect(),
|
||||
};
|
||||
SettingsSearch::new(
|
||||
"search",
|
||||
[
|
||||
entry("theme", "Theme", &["dark"]),
|
||||
entry("size", "Font size", &["zoom"]),
|
||||
],
|
||||
&field,
|
||||
)
|
||||
.on_pick(move |key, _, cx| say(&owner, format!("go {key}"), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Stops: the field, then the list of what answers.
|
||||
#[gpui::test]
|
||||
fn a_search_narrows_the_settings_and_enter_goes_to_one(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(search, cx);
|
||||
tab(1, cx);
|
||||
cx.simulate_input("zoom");
|
||||
settle(cx);
|
||||
tab(2, cx);
|
||||
tap("enter", cx);
|
||||
assert_eq!(said(&host, cx), ["go size"]);
|
||||
}
|
||||
|
||||
fn reset(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
let changed = |key: &str| Changed {
|
||||
key: key.to_string().into(),
|
||||
name: key.to_string().into(),
|
||||
now: "on".into(),
|
||||
default: "off".into(),
|
||||
};
|
||||
let (one, all) = (owner.clone(), owner);
|
||||
ResetToDefault::new("reset", [changed("a"), changed("b")])
|
||||
.on_reset(move |key, _, cx| say(&one, format!("reset {key}"), cx))
|
||||
.on_reset_all(move |_, cx| say(&all, "reset all".into(), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Stops: each row's way back, then Reset all, which asks twice.
|
||||
#[gpui::test]
|
||||
fn a_row_goes_back_alone_and_reset_all_asks_twice(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(reset, cx);
|
||||
tab(2, cx);
|
||||
tap("space", cx);
|
||||
tab(3, cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(said(&host, cx), ["reset b"], "the first press only arms");
|
||||
tap("space", cx);
|
||||
assert_eq!(said(&host, cx), ["reset b", "reset all"]);
|
||||
}
|
||||
|
||||
fn flags(window: &mut Window, cx: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
let field = window.use_keyed_state("flags-search", cx, TextInput::new);
|
||||
let flag = |key: &str, on| Flag {
|
||||
key: key.to_string().into(),
|
||||
name: key.to_string().into(),
|
||||
description: "does a thing".into(),
|
||||
stage: Stage::Beta,
|
||||
on,
|
||||
};
|
||||
FeatureFlags::new("flags", [flag("tabs", false), flag("sync", true)], &field)
|
||||
.on_toggle(move |key, on, _, cx| say(&owner, format!("{key} {on}"), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Stops: the search, then each flag's switch.
|
||||
#[gpui::test]
|
||||
fn a_flags_switch_turns_it(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(flags, cx);
|
||||
tab(3, cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(said(&host, cx), ["sync false"]);
|
||||
}
|
||||
|
||||
fn notices(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
let notices = Notices {
|
||||
channels: vec!["App".into(), "Email".into()],
|
||||
kinds: vec![("mention".into(), "Mentions".into())],
|
||||
on: vec![("mention".into(), "App".into())],
|
||||
quiet: false,
|
||||
};
|
||||
NotificationSettings::new("notices", notices)
|
||||
.on_change(move |notices, _, cx| {
|
||||
say(&owner, format!("{:?} {}", notices.on, notices.quiet), cx)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Stops: Do not disturb, then a box per channel.
|
||||
#[gpui::test]
|
||||
fn a_box_turns_its_kind_on_its_channel(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(notices, cx);
|
||||
tab(3, cx);
|
||||
tap("space", cx);
|
||||
tab(1, cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(
|
||||
said(&host, cx),
|
||||
[
|
||||
"[(\"mention\", \"App\"), (\"mention\", \"Email\")] false",
|
||||
"[(\"mention\", \"App\")] true"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
fn storage(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
StorageSettings::new(
|
||||
"storage",
|
||||
[
|
||||
Store {
|
||||
key: "cache".into(),
|
||||
name: "Cache".into(),
|
||||
bytes: 3 << 20,
|
||||
clearable: true,
|
||||
},
|
||||
Store {
|
||||
key: "data".into(),
|
||||
name: "Data".into(),
|
||||
bytes: 9 << 20,
|
||||
clearable: false,
|
||||
},
|
||||
],
|
||||
)
|
||||
.on_clear(move |key, _, cx| say(&owner, format!("clear {key}"), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_store_clears_on_the_second_press(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(storage, cx);
|
||||
tab(1, cx);
|
||||
tap("space", cx);
|
||||
assert!(said(&host, cx).is_empty(), "the first press only arms");
|
||||
tap("space", cx);
|
||||
assert_eq!(said(&host, cx), ["clear cache"]);
|
||||
}
|
||||
|
||||
fn size(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
FontSizeControl::new("size", 18.0, 14.0, (12.0, 24.0))
|
||||
.on_change(move |size, _, cx| say(&owner, format!("size {size}"), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Stops: the slider, then the way back.
|
||||
#[gpui::test]
|
||||
fn the_way_back_gives_the_default_size(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(size, cx);
|
||||
tab(2, cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(said(&host, cx), ["size 14"]);
|
||||
}
|
||||
|
||||
fn theme(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
ThemeSelector::new("theme", Appearance::Light)
|
||||
.on_change(move |appearance, _, cx| say(&owner, format!("{appearance:?}"), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Stops: a segment each.
|
||||
#[gpui::test]
|
||||
fn a_segment_picks_the_appearance(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(theme, cx);
|
||||
tab(3, cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(said(&host, cx), ["System"]);
|
||||
}
|
||||
|
||||
fn proxy(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
let proxy = Proxy::Manual {
|
||||
host: "proxy.corp".into(),
|
||||
port: 8080,
|
||||
bypass: vec!["localhost".into()],
|
||||
};
|
||||
ProxySettings::new("proxy", proxy)
|
||||
.on_apply(move |proxy, _, cx| say(&owner, format!("{proxy:?}"), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Apply is the last stop, so a step back from nothing reaches it.
|
||||
#[gpui::test]
|
||||
fn apply_hands_on_the_manual_proxy(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(proxy, cx);
|
||||
cx.update(|window, _| {
|
||||
window.blur();
|
||||
window.focus_prev();
|
||||
});
|
||||
settle(cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(
|
||||
said(&host, cx),
|
||||
["Manual { host: \"proxy.corp\", port: 8080, bypass: [\"localhost\"] }"]
|
||||
);
|
||||
}
|
||||
|
||||
fn layout(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
SettingsLayout::new(
|
||||
"layout",
|
||||
[
|
||||
("a", "Appearance", IconName::Palette),
|
||||
("b", "Network", IconName::Globe),
|
||||
],
|
||||
"a",
|
||||
)
|
||||
.on_select(move |key, _, cx| say(&owner, format!("open {key}"), cx))
|
||||
.page(div())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Stops: the section list.
|
||||
#[gpui::test]
|
||||
fn the_arrows_open_the_next_section(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(layout, cx);
|
||||
tab(1, cx);
|
||||
tap("down", cx);
|
||||
assert_eq!(said(&host, cx), ["open b"]);
|
||||
}
|
||||
|
||||
fn accent(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
let (red, blue) = (hsla(0.0, 0.6, 0.5, 1.0), hsla(0.6, 0.6, 0.5, 1.0));
|
||||
AccentColorPicker::new("accent", red, [("Red", red), ("Blue", blue)])
|
||||
.on_change(move |color, _, cx| say(&owner, format!("blue {}", color == blue), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Stops: the presets, Red then Blue, then the custom well.
|
||||
#[gpui::test]
|
||||
fn a_preset_sets_the_accent(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(accent, cx);
|
||||
tab(2, cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(said(&host, cx), ["blue true"]);
|
||||
}
|
||||
|
||||
fn privacy(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
let privacy = Privacy {
|
||||
telemetry: true,
|
||||
crash_reports: true,
|
||||
personalization: false,
|
||||
history: true,
|
||||
};
|
||||
PrivacySettings::new("privacy", privacy)
|
||||
.on_change(move |next, _, cx| say(&owner, format!("{next:?}"), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Stops: a switch per field, telemetry first.
|
||||
#[gpui::test]
|
||||
fn a_privacy_switch_turns_its_own_field(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(privacy, cx);
|
||||
tab(1, cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(
|
||||
said(&host, cx),
|
||||
[
|
||||
"Privacy { telemetry: false, crash_reports: true, personalization: false, history: true }"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
fn quiet(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
let notices = Notices {
|
||||
channels: vec!["App".into()],
|
||||
kinds: vec![("replies".into(), "Replies".into())],
|
||||
on: vec![("replies".into(), "App".into())],
|
||||
quiet: false,
|
||||
};
|
||||
NotificationSettings::new("notices", notices)
|
||||
.on_change(move |next, _, cx| {
|
||||
say(
|
||||
&owner,
|
||||
format!("quiet {} on {}", next.quiet, next.on.len()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Stops: Do not disturb, then a box per kind and channel.
|
||||
#[gpui::test]
|
||||
fn do_not_disturb_holds_notices_and_keeps_the_boxes(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(quiet, cx);
|
||||
tab(1, cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(said(&host, cx), ["quiet true on 1"]);
|
||||
}
|
||||
|
||||
fn startup(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
let startup = Startup {
|
||||
at_login: false,
|
||||
restore: true,
|
||||
opens: "last".into(),
|
||||
updates: true,
|
||||
};
|
||||
StartupSettings::new(
|
||||
"startup",
|
||||
startup,
|
||||
[Choice::new("last", "Where I left off")],
|
||||
)
|
||||
.on_change(move |next, _, cx| say(&owner, format!("login {}", next.at_login), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Stops: Open at login first.
|
||||
#[gpui::test]
|
||||
fn open_at_login_turns_on(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(startup, cx);
|
||||
tab(1, cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(said(&host, cx), ["login true"]);
|
||||
}
|
||||
|
||||
fn developer(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
DeveloperModeToggle::new("developer", false)
|
||||
.on_change(move |on, _, cx| say(&owner, format!("developer {on}"), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Stops: the switch.
|
||||
#[gpui::test]
|
||||
fn developer_mode_turns_on(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(developer, cx);
|
||||
tab(1, cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(said(&host, cx), ["developer true"]);
|
||||
}
|
||||
|
||||
fn files(_: &mut Window, _: &mut App, owner: Entity<Desk>) -> AnyElement {
|
||||
ImportExportSettings::new("files")
|
||||
.on_export(move |_, cx| say(&owner, "export".into(), cx))
|
||||
.on_import(|_, _, _| {})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Stops: Export first.
|
||||
#[gpui::test]
|
||||
fn export_asks_the_owner(cx: &mut TestAppContext) {
|
||||
let (host, cx) = desk(files, cx);
|
||||
tab(1, cx);
|
||||
tap("space", cx);
|
||||
assert_eq!(said(&host, cx), ["export"]);
|
||||
}
|
||||
Reference in new issue
Block a user