settings_ui: Add dropdown component + other fixes (#38909)
Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Generated
+2
@@ -14462,6 +14462,7 @@ dependencies = [
|
||||
"serde_with",
|
||||
"settings_macros",
|
||||
"smallvec",
|
||||
"strum 0.27.1",
|
||||
"tree-sitter",
|
||||
"tree-sitter-json",
|
||||
"unindent",
|
||||
@@ -14522,6 +14523,7 @@ dependencies = [
|
||||
"serde",
|
||||
"session",
|
||||
"settings",
|
||||
"strum 0.27.1",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
|
||||
@@ -28,14 +28,15 @@ paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
rust-embed.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings_macros = { path = "../settings_macros" }
|
||||
serde_json_lenient.workspace = true
|
||||
serde_repr.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_path_to_error.workspace = true
|
||||
serde_repr.workspace = true
|
||||
serde_with.workspace = true
|
||||
serde.workspace = true
|
||||
settings_macros = { path = "../settings_macros" }
|
||||
smallvec.workspace = true
|
||||
strum.workspace = true
|
||||
tree-sitter-json.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -870,6 +870,12 @@ impl From<bool> for SaturatingBool {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SaturatingBool> for bool {
|
||||
fn from(value: SaturatingBool) -> bool {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl merge_from::MergeFrom for SaturatingBool {
|
||||
fn merge_from(&mut self, other: &Self) {
|
||||
self.0 |= other.0
|
||||
|
||||
@@ -485,7 +485,18 @@ pub enum ScrollBeyondLastLine {
|
||||
|
||||
/// The shape of a selection cursor.
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
PartialEq,
|
||||
Eq,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CursorShape {
|
||||
|
||||
@@ -217,6 +217,7 @@ pub enum ShowScrollbar {
|
||||
Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
// todo() -> combine with CursorShape
|
||||
pub enum CursorShapeContent {
|
||||
/// Cursor is a block like `█`.
|
||||
#[default]
|
||||
|
||||
@@ -7,7 +7,7 @@ use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
future::LocalBoxFuture,
|
||||
};
|
||||
use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGlobal};
|
||||
use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal};
|
||||
|
||||
use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
@@ -34,7 +34,7 @@ use crate::{
|
||||
ActiveSettingsProfileName, FontFamilyName, IconThemeName, LanguageSettingsContent,
|
||||
LanguageToSettingsMap, SettingsJsonSchemaParams, ThemeName, VsCodeSettings, WorktreeId,
|
||||
merge_from::MergeFrom,
|
||||
parse_json_with_comments, replace_value_in_json_text,
|
||||
parse_json_with_comments,
|
||||
settings_content::{
|
||||
ExtensionsSettingsContent, ProjectSettingsContent, SettingsContent, UserSettingsContent,
|
||||
},
|
||||
@@ -439,34 +439,6 @@ impl SettingsStore {
|
||||
return rx;
|
||||
}
|
||||
|
||||
pub fn update_settings_file_at_path(
|
||||
&self,
|
||||
fs: Arc<dyn Fs>,
|
||||
path: &[impl AsRef<str>],
|
||||
new_value: serde_json::Value,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
let key_path = path
|
||||
.into_iter()
|
||||
.map(AsRef::as_ref)
|
||||
.map(SharedString::new)
|
||||
.collect::<Vec<_>>();
|
||||
let update = move |mut old_text: String, cx: AsyncApp| {
|
||||
cx.read_global(|store: &SettingsStore, _cx| {
|
||||
// todo(settings_ui) use `update_value_in_json_text` for merging new and old objects with comment preservation, needs old value though...
|
||||
let (range, replacement) = replace_value_in_json_text(
|
||||
&old_text,
|
||||
key_path.as_slice(),
|
||||
store.json_tab_size(),
|
||||
Some(&new_value),
|
||||
None,
|
||||
);
|
||||
old_text.replace_range(range, &replacement);
|
||||
old_text
|
||||
})
|
||||
};
|
||||
self.update_settings_file_inner(fs, update)
|
||||
}
|
||||
|
||||
pub fn update_settings_file(
|
||||
&self,
|
||||
fs: Arc<dyn Fs>,
|
||||
|
||||
@@ -26,6 +26,7 @@ gpui.workspace = true
|
||||
menu.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
strum.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! # settings_ui
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::Editor;
|
||||
use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
|
||||
@@ -11,9 +11,9 @@ use project::WorktreeId;
|
||||
use settings::{SettingsContent, SettingsStore};
|
||||
use ui::{
|
||||
ActiveTheme as _, AnyElement, BorrowAppContext as _, Button, Clickable as _, Color,
|
||||
FluentBuilder as _, Icon, IconName, InteractiveElement as _, Label, LabelCommon as _,
|
||||
LabelSize, ParentElement, SharedString, StatefulInteractiveElement as _, Styled, Switch,
|
||||
v_flex,
|
||||
DropdownMenu, FluentBuilder as _, Icon, IconName, InteractiveElement as _, Label,
|
||||
LabelCommon as _, LabelSize, ParentElement, SharedString, StatefulInteractiveElement as _,
|
||||
Styled, Switch, v_flex,
|
||||
};
|
||||
use util::{paths::PathStyle, rel_path::RelPath};
|
||||
|
||||
@@ -22,30 +22,24 @@ fn user_settings_data() -> Vec<SettingsPage> {
|
||||
SettingsPage {
|
||||
title: "General Page",
|
||||
items: vec![
|
||||
SettingsPageItem::SectionHeader("General Section"),
|
||||
SettingsPageItem::SectionHeader("General"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Confirm Quit",
|
||||
description: "Whether to confirm before quitting Zed",
|
||||
render: Rc::new(|_, cx| {
|
||||
render_toggle_button(
|
||||
"confirm_quit",
|
||||
SettingsFile::User,
|
||||
cx,
|
||||
|settings_content| &mut settings_content.workspace.confirm_quit,
|
||||
)
|
||||
}),
|
||||
render: |file, _, cx| {
|
||||
render_toggle_button("confirm_quit", file, cx, |settings_content| {
|
||||
&mut settings_content.workspace.confirm_quit
|
||||
})
|
||||
},
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Auto Update",
|
||||
description: "Automatically update Zed (may be ignored on Linux if installed through a package manager)",
|
||||
render: Rc::new(|_, cx| {
|
||||
render_toggle_button(
|
||||
"Auto Update",
|
||||
SettingsFile::User,
|
||||
cx,
|
||||
|settings_content| &mut settings_content.auto_update,
|
||||
)
|
||||
}),
|
||||
render: |file, _, cx| {
|
||||
render_toggle_button("Auto Update", file, cx, |settings_content| {
|
||||
&mut settings_content.auto_update
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -56,15 +50,45 @@ fn user_settings_data() -> Vec<SettingsPage> {
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Project Name",
|
||||
description: "The displayed name of this project. If not set, the root directory name",
|
||||
render: Rc::new(|window, cx| {
|
||||
render_text_field(
|
||||
"project_name",
|
||||
SettingsFile::User,
|
||||
render: |file, window, cx| {
|
||||
render_text_field("project_name", file, window, cx, |settings_content| {
|
||||
&mut settings_content.project.worktree.project_name
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
SettingsPage {
|
||||
title: "AI",
|
||||
items: vec![
|
||||
SettingsPageItem::SectionHeader("General"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Disable AI",
|
||||
description: "Whether to disable all AI features in Zed",
|
||||
render: |file, _, cx| {
|
||||
render_toggle_button("disable_AI", file, cx, |settings_content| {
|
||||
&mut settings_content.disable_ai
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
SettingsPage {
|
||||
title: "Appearance & Behavior",
|
||||
items: vec![
|
||||
SettingsPageItem::SectionHeader("Cursor"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Cursor Shape",
|
||||
description: "Cursor shape for the editor",
|
||||
render: |file, window, cx| {
|
||||
render_dropdown::<settings::CursorShape>(
|
||||
"cursor_shape",
|
||||
file,
|
||||
window,
|
||||
cx,
|
||||
|settings_content| &mut settings_content.project.worktree.project_name,
|
||||
|settings_content| &mut settings_content.editor.cursor_shape,
|
||||
)
|
||||
}),
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -79,18 +103,11 @@ fn project_settings_data() -> Vec<SettingsPage> {
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Project Name",
|
||||
description: " The displayed name of this project. If not set, the root directory name",
|
||||
render: Rc::new(|window, cx| {
|
||||
render_text_field(
|
||||
"project_name",
|
||||
SettingsFile::Local((
|
||||
WorktreeId::from_usize(0),
|
||||
Arc::from(RelPath::new("TODO: actually pass through file").unwrap()),
|
||||
)),
|
||||
window,
|
||||
cx,
|
||||
|settings_content| &mut settings_content.project.worktree.project_name,
|
||||
)
|
||||
}),
|
||||
render: |file, window, cx| {
|
||||
render_text_field("project_name", file, window, cx, |settings_content| {
|
||||
&mut settings_content.project.worktree.project_name
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
}]
|
||||
@@ -169,7 +186,7 @@ enum SettingsPageItem {
|
||||
}
|
||||
|
||||
impl SettingsPageItem {
|
||||
fn render(&self, window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
fn render(&self, file: SettingsFile, window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
match self {
|
||||
SettingsPageItem::SectionHeader(header) => Label::new(SharedString::new_static(header))
|
||||
.size(LabelSize::Large)
|
||||
@@ -177,7 +194,7 @@ impl SettingsPageItem {
|
||||
SettingsPageItem::SettingItem(setting_item) => div()
|
||||
.child(setting_item.title)
|
||||
.child(setting_item.description)
|
||||
.child((setting_item.render)(window, cx))
|
||||
.child((setting_item.render)(file, window, cx))
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
@@ -196,7 +213,7 @@ impl SettingsPageItem {
|
||||
struct SettingItem {
|
||||
title: &'static str,
|
||||
description: &'static str,
|
||||
render: std::rc::Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
||||
render: fn(file: SettingsFile, &mut Window, &mut App) -> AnyElement,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
@@ -345,7 +362,11 @@ impl SettingsWindow {
|
||||
div()
|
||||
.child(self.render_files(window, cx))
|
||||
.child(Label::new(page.title))
|
||||
.children(page.items.iter().map(|item| item.render(window, cx)))
|
||||
.children(
|
||||
page.items
|
||||
.iter()
|
||||
.map(|item| item.render(self.current_file.clone(), window, cx)),
|
||||
)
|
||||
}
|
||||
|
||||
fn current_page(&self) -> &SettingsPage {
|
||||
@@ -440,11 +461,11 @@ fn render_text_field(
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_toggle_button(
|
||||
fn render_toggle_button<B: Into<bool> + From<bool> + Copy + Send + 'static>(
|
||||
id: &'static str,
|
||||
_: SettingsFile,
|
||||
cx: &mut App,
|
||||
get_value: fn(&mut SettingsContent) -> &mut Option<bool>,
|
||||
get_value: fn(&mut SettingsContent) -> &mut Option<B>,
|
||||
) -> AnyElement {
|
||||
// TODO: in settings window state
|
||||
let store = SettingsStore::global(cx);
|
||||
@@ -457,18 +478,78 @@ fn render_toggle_button(
|
||||
.unwrap_or_default()
|
||||
.content;
|
||||
|
||||
let toggle_state =
|
||||
if get_value(&mut user_settings).unwrap_or_else(|| get_value(&mut defaults).unwrap()) {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
};
|
||||
let toggle_state = if get_value(&mut user_settings)
|
||||
.unwrap_or_else(|| get_value(&mut defaults).unwrap())
|
||||
.into()
|
||||
{
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
};
|
||||
|
||||
Switch::new(id, toggle_state)
|
||||
.on_click({
|
||||
move |state, _window, cx| {
|
||||
write_setting_value(get_value, Some(*state == ui::ToggleState::Selected), cx);
|
||||
write_setting_value(
|
||||
get_value,
|
||||
Some((*state == ui::ToggleState::Selected).into()),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_dropdown<T>(
|
||||
id: &'static str,
|
||||
_: SettingsFile,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
get_value: fn(&mut SettingsContent) -> &mut Option<T>,
|
||||
) -> AnyElement
|
||||
where
|
||||
T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + 'static,
|
||||
{
|
||||
let variants = || -> &'static [T] { <T as strum::VariantArray>::VARIANTS };
|
||||
let labels = || -> &'static [&'static str] { <T as strum::VariantNames>::VARIANTS };
|
||||
|
||||
let store = SettingsStore::global(cx);
|
||||
let mut defaults = store.raw_default_settings().clone();
|
||||
let mut user_settings = store
|
||||
.raw_user_settings()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.content;
|
||||
|
||||
let current_value =
|
||||
get_value(&mut user_settings).unwrap_or_else(|| get_value(&mut defaults).unwrap());
|
||||
let current_value_label =
|
||||
labels()[variants().iter().position(|v| *v == current_value).unwrap()];
|
||||
|
||||
DropdownMenu::new(
|
||||
id,
|
||||
current_value_label,
|
||||
ui::ContextMenu::build(window, cx, move |mut menu, _, _| {
|
||||
for (value, label) in variants()
|
||||
.into_iter()
|
||||
.copied()
|
||||
.zip(labels().into_iter().copied())
|
||||
{
|
||||
menu = menu.toggleable_entry(
|
||||
label,
|
||||
value == current_value,
|
||||
ui::IconPosition::Start,
|
||||
None,
|
||||
move |_, cx| {
|
||||
if value == current_value {
|
||||
return;
|
||||
}
|
||||
write_setting_value(get_value, Some(value), cx);
|
||||
},
|
||||
);
|
||||
}
|
||||
menu
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user