settings_ui: Add dynamic setting fields (#40443)

Closes #ISSUE

Includes the start of how we can get rid of most of the `.unimplemented`
"Edit in JSON" buttons in the settings UI. For now only Theme selection
is implemented, follow ups will add more settings

Release Notes:

- N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Ben Kunkle 2025-10-17 08:13:34 -05:00 committed by GitHub
parent b7112320bb
commit 9056d77604
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 2392 additions and 2367 deletions

58
Cargo.lock generated
View file

@ -678,7 +678,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum 0.27.1",
"strum 0.27.2",
"thiserror 2.0.12",
"workspace-hack",
]
@ -2198,7 +2198,7 @@ dependencies = [
"schemars 1.0.1",
"serde",
"serde_json",
"strum 0.27.1",
"strum 0.27.2",
"thiserror 2.0.12",
"workspace-hack",
]
@ -3388,7 +3388,7 @@ dependencies = [
"pretty_assertions",
"serde",
"serde_json",
"strum 0.27.1",
"strum 0.27.2",
"uuid",
"workspace-hack",
]
@ -3403,7 +3403,7 @@ dependencies = [
"ordered-float 2.10.1",
"rustc-hash 2.1.1",
"serde",
"strum 0.27.1",
"strum 0.27.2",
"workspace-hack",
]
@ -3598,7 +3598,7 @@ dependencies = [
"sha2",
"smol",
"sqlx",
"strum 0.27.1",
"strum 0.27.2",
"subtle",
"supermaven_api",
"task",
@ -3783,7 +3783,7 @@ dependencies = [
"gpui",
"inventory",
"parking_lot",
"strum 0.27.1",
"strum 0.27.2",
"theme",
"workspace-hack",
]
@ -5462,7 +5462,7 @@ dependencies = [
"serde_json",
"settings",
"slotmap",
"strum 0.27.1",
"strum 0.27.2",
"text",
"tree-sitter",
"tree-sitter-c",
@ -6074,7 +6074,7 @@ dependencies = [
"serde",
"settings",
"smallvec",
"strum 0.27.1",
"strum 0.27.2",
"telemetry",
"theme",
"ui",
@ -7195,7 +7195,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum 0.27.1",
"strum 0.27.2",
"telemetry",
"theme",
"time",
@ -7301,7 +7301,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum 0.27.1",
"strum 0.27.2",
"workspace-hack",
]
@ -7408,7 +7408,7 @@ dependencies = [
"smallvec",
"smol",
"stacksafe",
"strum 0.27.1",
"strum 0.27.2",
"sum_tree",
"taffy",
"thiserror 2.0.12",
@ -8059,7 +8059,7 @@ name = "icons"
version = "0.1.0"
dependencies = [
"serde",
"strum 0.27.1",
"strum 0.27.2",
"workspace-hack",
]
@ -9041,7 +9041,7 @@ dependencies = [
"serde_json",
"settings",
"smol",
"strum 0.27.1",
"strum 0.27.2",
"thiserror 2.0.12",
"tiktoken-rs",
"tokio",
@ -10192,7 +10192,7 @@ dependencies = [
"schemars 1.0.1",
"serde",
"serde_json",
"strum 0.27.1",
"strum 0.27.2",
"workspace-hack",
]
@ -11109,7 +11109,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum 0.27.1",
"strum 0.27.2",
"workspace-hack",
]
@ -11124,7 +11124,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum 0.27.1",
"strum 0.27.2",
"thiserror 2.0.12",
"workspace-hack",
]
@ -14418,7 +14418,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"strum 0.27.1",
"strum 0.27.2",
"tracing",
"util",
"workspace-hack",
@ -15427,7 +15427,7 @@ dependencies = [
"serde_with",
"settings_macros",
"smallvec",
"strum 0.27.1",
"strum 0.27.2",
"tree-sitter",
"tree-sitter-json",
"unindent",
@ -15494,7 +15494,7 @@ dependencies = [
"serde",
"session",
"settings",
"strum 0.27.1",
"strum 0.27.2",
"theme",
"title_bar",
"ui",
@ -16253,7 +16253,7 @@ dependencies = [
"settings",
"simplelog",
"story",
"strum 0.27.1",
"strum 0.27.2",
"theme",
"title_bar",
"ui",
@ -16355,9 +16355,9 @@ dependencies = [
[[package]]
name = "strum"
version = "0.27.1"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros 0.27.1",
]
@ -17232,7 +17232,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"settings",
"strum 0.27.1",
"strum 0.27.2",
"thiserror 2.0.12",
"util",
"uuid",
@ -17266,7 +17266,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"simplelog",
"strum 0.27.1",
"strum 0.27.2",
"theme",
"vscode_theme",
"workspace-hack",
@ -18330,7 +18330,7 @@ dependencies = [
"settings",
"smallvec",
"story",
"strum 0.27.1",
"strum 0.27.2",
"theme",
"ui_macros",
"util",
@ -18714,7 +18714,7 @@ dependencies = [
"anyhow",
"schemars 1.0.1",
"serde",
"strum 0.27.1",
"strum 0.27.2",
"workspace-hack",
]
@ -20655,7 +20655,7 @@ dependencies = [
"settings",
"smallvec",
"sqlez",
"strum 0.27.1",
"strum 0.27.2",
"task",
"telemetry",
"tempfile",
@ -20980,7 +20980,7 @@ dependencies = [
"anyhow",
"schemars 1.0.1",
"serde",
"strum 0.27.1",
"strum 0.27.2",
"workspace-hack",
]
@ -21751,7 +21751,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum 0.27.1",
"strum 0.27.2",
"telemetry",
"telemetry_events",
"theme",

View file

@ -651,7 +651,7 @@ sqlformat = "0.2"
stacksafe = "0.1"
streaming-iterator = "0.1"
strsim = "0.11"
strum = { version = "0.27.0", features = ["derive"] }
strum = { version = "0.27.2", features = ["derive"] }
subtle = "2.5.0"
syn = { version = "2.0.101", features = ["full", "extra-traits", "visit-mut"] }
sys-locale = "0.3.1"

View file

@ -125,7 +125,18 @@ fn default_buffer_font_weight() -> Option<FontWeight> {
}
/// Represents the selection of a theme, which can be either static or dynamic.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
#[derive(
Clone,
Debug,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
PartialEq,
Eq,
strum::EnumDiscriminants,
)]
#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
#[serde(untagged)]
pub enum ThemeSelection {
/// A static theme selection, represented by a single theme name.
@ -167,7 +178,18 @@ pub enum IconThemeSelection {
///
/// `System` will select the theme based on the system's appearance.
#[derive(
Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, MergeFrom,
Debug,
PartialEq,
Eq,
Clone,
Copy,
Default,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
strum::VariantArray,
strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
pub enum ThemeMode {

View file

@ -535,30 +535,30 @@ impl SettingsStore {
/// Returns the first file found that contains the value.
/// The value will only be None if no file contains the value.
/// I.e. if no file contains the value, returns `(File::Default, None)`
pub fn get_value_from_file<T>(
&self,
pub fn get_value_from_file<'a, T: 'a>(
&'a self,
target_file: SettingsFile,
pick: fn(&SettingsContent) -> &Option<T>,
) -> (SettingsFile, Option<&T>) {
pick: fn(&'a SettingsContent) -> Option<T>,
) -> (SettingsFile, Option<T>) {
self.get_value_from_file_inner(target_file, pick, true)
}
/// Same as `Self::get_value_from_file` except that it does not include the current file.
/// Therefore it returns the value that was potentially overloaded by the target file.
pub fn get_value_up_to_file<T>(
&self,
pub fn get_value_up_to_file<'a, T: 'a>(
&'a self,
target_file: SettingsFile,
pick: fn(&SettingsContent) -> &Option<T>,
) -> (SettingsFile, Option<&T>) {
pick: fn(&'a SettingsContent) -> Option<T>,
) -> (SettingsFile, Option<T>) {
self.get_value_from_file_inner(target_file, pick, false)
}
fn get_value_from_file_inner<T>(
&self,
fn get_value_from_file_inner<'a, T: 'a>(
&'a self,
target_file: SettingsFile,
pick: fn(&SettingsContent) -> &Option<T>,
pick: fn(&'a SettingsContent) -> Option<T>,
include_target_file: bool,
) -> (SettingsFile, Option<&T>) {
) -> (SettingsFile, Option<T>) {
// todo(settings_ui): Add a metadata field for overriding the "overrides" tag, for contextually different settings
// e.g. disable AI isn't overridden, or a vec that gets extended instead or some such
@ -588,7 +588,7 @@ impl SettingsStore {
let Some(content) = self.get_content_for_file(file.clone()) else {
continue;
};
if let Some(value) = pick(content).as_ref() {
if let Some(value) = pick(content) {
return (file, Some(value));
}
}
@ -1774,11 +1774,16 @@ mod tests {
)
.unwrap();
fn get(content: &SettingsContent) -> &Option<u32> {
&content.project.all_languages.defaults.preferred_line_length
fn get(content: &SettingsContent) -> Option<&u32> {
content
.project
.all_languages
.defaults
.preferred_line_length
.as_ref()
}
let default_value = get(&store.default_settings).unwrap();
let default_value = *get(&store.default_settings).unwrap();
assert_eq!(
store.get_value_from_file(SettingsFile::Project(local.clone()), get),
@ -1841,8 +1846,13 @@ mod tests {
.into_arc(),
);
fn get(content: &SettingsContent) -> &Option<u32> {
&content.project.all_languages.defaults.preferred_line_length
fn get(content: &SettingsContent) -> Option<&u32> {
content
.project
.all_languages
.defaults
.preferred_line_length
.as_ref()
}
store

File diff suppressed because it is too large Load diff

View file

@ -6,10 +6,10 @@ use editor::{Editor, EditorEvent};
use feature_flags::FeatureFlag;
use fuzzy::StringMatchCandidate;
use gpui::{
Action, App, Div, Entity, FocusHandle, Focusable, FontWeight, Global, ListState,
ReadGlobal as _, ScrollHandle, Stateful, Subscription, Task, TitlebarOptions,
UniformListScrollHandle, Window, WindowBounds, WindowHandle, WindowOptions, actions, div, list,
point, prelude::*, px, size, uniform_list,
Action, App, Div, Entity, FocusHandle, Focusable, Global, ListState, ReadGlobal as _,
ScrollHandle, Stateful, Subscription, Task, TitlebarOptions, UniformListScrollHandle, Window,
WindowBounds, WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, size,
uniform_list,
};
use heck::ToTitleCase as _;
use project::WorktreeId;
@ -84,8 +84,8 @@ actions!(
struct FocusFile(pub u32);
struct SettingField<T: 'static> {
pick: fn(&SettingsContent) -> &Option<T>,
pick_mut: fn(&mut SettingsContent) -> &mut Option<T>,
pick: fn(&SettingsContent) -> Option<&T>,
write: fn(&mut SettingsContent, Option<T>),
}
impl<T: 'static> Clone for SettingField<T> {
@ -98,7 +98,7 @@ impl<T: 'static> Clone for SettingField<T> {
impl<T: 'static> Copy for SettingField<T> {}
/// Helper for unimplemented settings, used in combination with `SettingField::unimplemented`
/// to keep the setting around in the UI with valid pick and pick_mut implementations, but don't actually try to render it.
/// to keep the setting around in the UI with valid pick and write implementations, but don't actually try to render it.
/// TODO(settings_ui): In non-dev builds (`#[cfg(not(debug_assertions))]`) make this render as edit-in-json
#[derive(Clone, Copy)]
struct UnimplementedSettingField;
@ -114,8 +114,8 @@ impl<T: 'static> SettingField<T> {
#[allow(unused)]
fn unimplemented(self) -> SettingField<UnimplementedSettingField> {
SettingField {
pick: |_| &Some(UnimplementedSettingField),
pick_mut: |_| unreachable!(),
pick: |_| Some(&UnimplementedSettingField),
write: |_, _| unreachable!(),
}
}
}
@ -163,12 +163,15 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
if file_set_in == &settings::SettingsFile::Default {
return None;
}
if file_set_in != &current_file.to_settings() {
return None;
}
let this = *self;
let store = SettingsStore::global(cx);
let default_value = (this.pick)(store.raw_default_settings());
let is_default = store
.get_content_for_file(file_set_in.clone())
.map_or(&None, this.pick)
.map_or(None, this.pick)
== default_value;
if is_default {
return None;
@ -183,12 +186,12 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
.0
!= settings::SettingsFile::Default;
let value_to_set = if is_set_somewhere_other_than_default {
default_value.clone()
default_value.cloned()
} else {
None
};
update_settings_file(current_file.clone(), cx, move |settings, _| {
*(this.pick_mut)(settings) = value_to_set;
(this.write)(settings, value_to_set);
})
// todo(settings_ui): Don't log err
.log_err();
@ -374,12 +377,6 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::OnLastWindowClosed>(render_dropdown)
.add_basic_renderer::<settings::CloseWindowWhenNoItems>(render_dropdown)
.add_basic_renderer::<settings::FontFamilyName>(render_font_picker)
// todo(settings_ui): This needs custom ui
// .add_renderer::<settings::BufferLineHeight>(|settings_field, file, _, window, cx| {
// // todo(settings_ui): Do we want to expose the custom variant of buffer line height?
// // right now there's a manual impl of strum::VariantArray
// render_dropdown(*settings_field, file, window, cx)
// })
.add_basic_renderer::<settings::BaseKeymapContent>(render_dropdown)
.add_basic_renderer::<settings::MultiCursorModifier>(render_dropdown)
.add_basic_renderer::<settings::HideMouseMode>(render_dropdown)
@ -420,7 +417,7 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<NonZero<usize>>(render_number_field)
.add_basic_renderer::<NonZeroU32>(render_number_field)
.add_basic_renderer::<settings::CodeFade>(render_number_field)
.add_basic_renderer::<FontWeight>(render_number_field)
.add_basic_renderer::<gpui::FontWeight>(render_number_field)
.add_basic_renderer::<settings::MinimumContrast>(render_number_field)
.add_basic_renderer::<settings::ShowScrollbar>(render_dropdown)
.add_basic_renderer::<settings::ScrollbarDiagnostics>(render_dropdown)
@ -430,17 +427,18 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::MinimapThumbBorder>(render_dropdown)
.add_basic_renderer::<settings::SteppingGranularity>(render_dropdown)
.add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)
.add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)
.add_basic_renderer::<settings::ImageFileSizeUnit>(render_dropdown)
.add_basic_renderer::<settings::StatusStyle>(render_dropdown)
.add_basic_renderer::<settings::PaneSplitDirectionHorizontal>(render_dropdown)
.add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
.add_basic_renderer::<settings::PaneSplitDirectionVertical>(render_dropdown)
.add_basic_renderer::<settings::DocumentColorsRenderMode>(render_dropdown)
// please semicolon stay on next line
;
// .add_renderer::<ThemeSelection>(|settings_field, file, _, window, cx| {
// render_dropdown(*settings_field, file, window, cx)
// });
.add_basic_renderer::<settings::ThemeSelectionDiscriminants>(render_dropdown)
.add_basic_renderer::<settings::ThemeMode>(render_dropdown)
.add_basic_renderer::<settings::ThemeName>(render_theme_picker)
// please semicolon stay on next line
;
}
pub fn open_settings_editor(
@ -500,7 +498,7 @@ pub fn open_settings_editor(
/// If this is empty the selected page is rendered,
/// otherwise the last sub page gets rendered.
///
/// Global so that `pick` and `pick_mut` callbacks can access it
/// Global so that `pick` and `write` callbacks can access it
/// and use it to dynamically render sub pages (e.g. for language settings)
static SUB_PAGE_STACK: LazyLock<RwLock<Vec<SubPage>>> = LazyLock::new(|| RwLock::new(Vec::new()));
@ -584,6 +582,7 @@ enum SettingsPageItem {
SectionHeader(&'static str),
SettingItem(SettingItem),
SubPageLink(SubPageLink),
DynamicItem(DynamicItem),
}
impl std::fmt::Debug for SettingsPageItem {
@ -596,6 +595,9 @@ impl std::fmt::Debug for SettingsPageItem {
SettingsPageItem::SubPageLink(sub_page_link) => {
write!(f, "SubPageLink({})", sub_page_link.title)
}
SettingsPageItem::DynamicItem(dynamic_item) => {
write!(f, "DynamicItem({})", dynamic_item.discriminant.title)
}
}
}
}
@ -610,19 +612,17 @@ impl SettingsPageItem {
cx: &mut Context<SettingsWindow>,
) -> AnyElement {
let file = settings_window.current_file.clone();
match self {
SettingsPageItem::SectionHeader(header) => v_flex()
.w_full()
.gap_1p5()
.child(
Label::new(SharedString::new_static(header))
.size(LabelSize::Small)
.color(Color::Muted)
.buffer_font(cx),
)
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.into_any_element(),
SettingsPageItem::SettingItem(setting_item) => {
let border_variant = cx.theme().colors().border_variant;
let apply_padding = |element: Stateful<Div>| -> Stateful<Div> {
let element = element.pt_4();
if is_last {
element.pb_10()
} else {
element.pb_4().border_b_1().border_color(border_variant)
}
};
let mut render_setting_item_inner =
|setting_item: &SettingItem, cx: &mut Context<SettingsWindow>| {
let renderer = cx.default_global::<SettingFieldRenderer>().clone();
let (_, found) = setting_item.field.file_set_in(file.clone(), cx);
@ -642,7 +642,7 @@ impl SettingsPageItem {
Ok(field_renderer) => field_renderer(
settings_window,
setting_item,
file,
file.clone(),
setting_item.metadata.as_deref(),
window,
cx,
@ -650,7 +650,7 @@ impl SettingsPageItem {
Err(warning) => render_settings_item(
settings_window,
setting_item,
file,
file.clone(),
Button::new("error-warning", warning)
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
@ -665,17 +665,23 @@ impl SettingsPageItem {
),
};
field
.pt_4()
.map(|this| {
if is_last {
this.pb_10()
} else {
this.pb_4()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
}
})
(field.map(apply_padding), field_renderer_or_warning.is_ok())
};
match self {
SettingsPageItem::SectionHeader(header) => v_flex()
.w_full()
.gap_1p5()
.child(
Label::new(SharedString::new_static(header))
.size(LabelSize::Small)
.color(Color::Muted)
.buffer_font(cx),
)
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.into_any_element(),
SettingsPageItem::SettingItem(setting_item) => {
render_setting_item_inner(setting_item, cx)
.0
.into_any_element()
}
SettingsPageItem::SubPageLink(sub_page_link) => h_flex()
@ -684,16 +690,7 @@ impl SettingsPageItem {
.min_w_0()
.gap_2()
.justify_between()
.pt_4()
.map(|this| {
if is_last {
this.pb_10()
} else {
this.pb_4()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
}
})
.map(apply_padding)
.child(
v_flex()
.w_full()
@ -736,6 +733,32 @@ impl SettingsPageItem {
}),
)
.into_any_element(),
SettingsPageItem::DynamicItem(DynamicItem {
discriminant: discriminant_setting_item,
pick_discriminant,
fields,
}) => {
let file = file.to_settings();
let discriminant = SettingsStore::global(cx)
.get_value_from_file(file, *pick_discriminant)
.1;
let (discriminant_element, rendered_ok) =
render_setting_item_inner(discriminant_setting_item, cx);
let mut content = v_flex()
.gap_2()
.id("dynamic-item")
.child(discriminant_element);
if rendered_ok {
let discriminant =
discriminant.expect("This should be Some if rendered_ok is true");
let sub_fields = &fields[discriminant];
for field in sub_fields {
content = content.child(render_setting_item_inner(field, cx).0.pl_6());
}
}
return content.into_any_element();
}
}
}
}
@ -767,8 +790,7 @@ fn render_settings_item(
.when_some(
setting_item
.field
.reset_to_default_fn(&file, &found_in_file, cx)
.filter(|_| file_set_in.as_ref() == Some(&file)),
.reset_to_default_fn(&file, &found_in_file, cx),
|this, reset_to_default| {
this.child(
IconButton::new("reset-to-default-btn", IconName::Undo)
@ -816,6 +838,18 @@ struct SettingItem {
files: FileMask,
}
struct DynamicItem {
discriminant: SettingItem,
pick_discriminant: fn(&SettingsContent) -> Option<usize>,
fields: Vec<Vec<SettingItem>>,
}
impl PartialEq for DynamicItem {
fn eq(&self, other: &Self) -> bool {
self.discriminant == other.discriminant && self.fields == other.fields
}
}
#[derive(PartialEq, Eq, Clone, Copy)]
struct FileMask(u8);
@ -1204,7 +1238,11 @@ impl SettingsWindow {
any_found_since_last_header = false;
}
SettingsPageItem::SettingItem(SettingItem { files, .. })
| SettingsPageItem::SubPageLink(SubPageLink { files, .. }) => {
| SettingsPageItem::SubPageLink(SubPageLink { files, .. })
| SettingsPageItem::DynamicItem(DynamicItem {
discriminant: SettingItem { files, .. },
..
}) => {
if !files.contains(current_file) {
page_filter[index] = false;
} else {
@ -1375,7 +1413,10 @@ impl SettingsWindow {
for (item_index, item) in page.items.iter().enumerate() {
let key_index = key_lut.len();
match item {
SettingsPageItem::SettingItem(item) => {
SettingsPageItem::DynamicItem(DynamicItem {
discriminant: item, ..
})
| SettingsPageItem::SettingItem(item) => {
documents.push(bm25::Document {
id: key_index,
contents: [page.title, header_str, item.title, item.description]
@ -2687,7 +2728,7 @@ fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
.on_confirm({
move |new_text, cx| {
update_settings_file(file.clone(), cx, move |settings, _cx| {
*(field.pick_mut)(settings) = new_text.map(Into::into);
(field.write)(settings, new_text.map(Into::into));
})
.log_err(); // todo(settings_ui) don't log err
}
@ -2716,7 +2757,7 @@ fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
move |state, _window, cx| {
let state = *state == ui::ToggleState::Selected;
update_settings_file(file.clone(), cx, move |settings, _cx| {
*(field.pick_mut)(settings) = Some(state.into());
(field.write)(settings, Some(state.into()));
})
.log_err(); // todo(settings_ui) don't log err
}
@ -2744,7 +2785,7 @@ fn render_font_picker(
current_value.clone().into(),
move |font_name, cx| {
update_settings_file(file.clone(), cx, move |settings, _cx| {
*(field.pick_mut)(settings) = Some(font_name.into());
(field.write)(settings, Some(font_name.into()));
})
.log_err(); // todo(settings_ui) don't log err
},
@ -2788,7 +2829,7 @@ fn render_number_field<T: NumberFieldType + Send + Sync>(
move |value, _window, cx| {
let value = *value;
update_settings_file(file.clone(), cx, move |settings, _cx| {
*(field.pick_mut)(settings) = Some(value);
(field.write)(settings, Some(value));
})
.log_err(); // todo(settings_ui) don't log err
}
@ -2843,7 +2884,58 @@ where
return;
}
update_settings_file(file.clone(), cx, move |settings, _cx| {
*(field.pick_mut)(settings) = Some(value);
(field.write)(settings, Some(value));
})
.log_err(); // todo(settings_ui) don't log err
},
);
}
menu
}),
)
.trigger_size(ButtonSize::Medium)
.style(DropdownStyle::Outlined)
.offset(gpui::Point {
x: px(0.0),
y: px(2.0),
})
.tab_index(0)
.into_any_element()
}
fn render_theme_picker(
field: SettingField<settings::ThemeName>,
file: SettingsUiFile,
_metadata: Option<&SettingsFieldMetadata>,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
let current_value = value
.cloned()
.map(|theme_name| theme_name.0.into())
.unwrap_or_else(|| cx.theme().name.clone());
DropdownMenu::new(
"font-picker",
current_value.clone(),
ContextMenu::build(window, cx, move |mut menu, _, cx| {
let all_theme_names = theme::ThemeRegistry::global(cx).list_names();
for theme_name in all_theme_names {
let file = file.clone();
let selected = theme_name.as_ref() == current_value.as_ref();
menu = menu.toggleable_entry(
theme_name.clone(),
selected,
IconPosition::End,
None,
move |_, cx| {
if selected {
return;
}
let theme_name = theme_name.clone();
update_settings_file(file.clone(), cx, move |settings, _cx| {
(field.write)(settings, Some(settings::ThemeName(theme_name.into())));
})
.log_err(); // todo(settings_ui) don't log err
},