settings ui: Add another batch of UX fixes and improvements (#39742)

Release Notes:

- N/A
This commit is contained in:
Danilo Leal
2025-10-08 06:11:34 +00:00
committed by GitHub
parent 5c7907ad2f
commit 294ca25f44
5 changed files with 200 additions and 186 deletions
+2 -2
View File
@@ -9,7 +9,7 @@ use workspace::{
item::{Item, ItemEvent},
with_active_or_new_workspace,
};
use zed_actions::{Extensions, OpenSettings, agent, command_palette};
use zed_actions::{Extensions, OpenSettingsEditor, agent, command_palette};
use crate::{Onboarding, OpenOnboarding};
@@ -53,7 +53,7 @@ const CONTENT: (Section<4>, Section<3>) = (
SectionEntry {
icon: IconName::Settings,
title: "Open Settings",
action: &OpenSettings,
action: &OpenSettingsEditor,
},
SectionEntry {
icon: IconName::ZedAssistant,
+92 -69
View File
@@ -131,7 +131,7 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
SettingsPageItem::SettingItem(SettingItem {
files: USER,
title: "Settings Profiles",
description: "Any number of settings profiles that are temporarily applied on top of your existing user settings.",
description: "Any number of settings profiles that are temporarily applied on top of your existing user settings",
field: Box::new(
SettingField {
pick: |settings_content| &settings_content.workspace.use_system_prompts,
@@ -295,6 +295,28 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Agent Panel UI Font Size",
description: "Font size for agent response text in the agent panel. Falls back to the regular UI font size.",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.agent_ui_font_size,
pick_mut: |settings_content| &mut settings_content.theme.agent_ui_font_size,
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Agent Panel Buffer Font Size",
description: "Font size for user messages text in the agent panel",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.agent_buffer_font_size,
pick_mut: |settings_content| {
&mut settings_content.theme.agent_buffer_font_size
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Keymap"),
SettingsPageItem::SettingItem(SettingItem {
title: "Base Keymap",
@@ -1020,7 +1042,7 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Min Line Number Digits",
description: "Minimum number of characters to reserve space for in the gutter.",
description: "Minimum number of characters to reserve space for in the gutter",
field: Box::new(SettingField {
pick: |settings_content| {
if let Some(gutter) = &settings_content.editor.gutter {
@@ -1638,6 +1660,27 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
title: "Workbench & Window",
items: vec![
SettingsPageItem::SectionHeader("Status Bar"),
SettingsPageItem::SettingItem(SettingItem {
title: "Project Panel Button",
description: "Whether to show the project panel button in the status bar",
field: Box::new(SettingField {
pick: |settings_content| {
if let Some(project_panel) = &settings_content.project_panel {
&project_panel.button
} else {
&None
}
},
pick_mut: |settings_content| {
&mut settings_content
.project_panel
.get_or_insert_default()
.button
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Active Language Button",
description: "Whether to show the active language button in the status bar",
@@ -1738,6 +1781,24 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Debugger Button",
description: "Whether to show the debugger button in the status bar",
field: Box::new(SettingField {
pick: |settings_content| {
if let Some(debugger) = &settings_content.debugger {
&debugger.button
} else {
&None
}
},
pick_mut: |settings_content| {
&mut settings_content.debugger.get_or_insert_default().button
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Tab Bar"),
SettingsPageItem::SettingItem(SettingItem {
title: "Editor Tabs",
@@ -1844,7 +1905,7 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Show Onboarding Banner",
description: "Whether to show onboarding banners in the titlebar",
description: "Whether to show banners announcing new features in the titlebar",
field: Box::new(SettingField {
pick: |settings_content| {
if let Some(title_bar) = &settings_content.title_bar {
@@ -2255,27 +2316,6 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
title: "Panels",
items: vec![
SettingsPageItem::SectionHeader("Project Panel"),
SettingsPageItem::SettingItem(SettingItem {
title: "Project Panel Button",
description: "Whether to show the project panel button in the status bar",
field: Box::new(SettingField {
pick: |settings_content| {
if let Some(project_panel) = &settings_content.project_panel {
&project_panel.button
} else {
&None
}
},
pick_mut: |settings_content| {
&mut settings_content
.project_panel
.get_or_insert_default()
.button
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Project Panel Dock",
description: "Where to dock the project panel",
@@ -2884,8 +2924,8 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
}),
SettingsPageItem::SectionHeader("Git Panel"),
SettingsPageItem::SettingItem(SettingItem {
title: "Button",
description: "Whether to show the git panel button in the status bar",
title: "Git Panel Button",
description: "Whether to show the Git panel button in the status bar",
field: Box::new(SettingField {
pick: |settings_content| {
if let Some(git_panel) = &settings_content.git_panel {
@@ -2902,8 +2942,8 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Dock",
description: "Where to dock the git panel",
title: "Git Panel Dock",
description: "Where to dock the Git panel",
field: Box::new(SettingField {
pick: |settings_content| {
if let Some(git_panel) = &settings_content.git_panel {
@@ -2920,8 +2960,8 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Default Width",
description: "Default width of the git panel in pixels",
title: "Git Panel Default Width",
description: "Default width of the Git panel in pixels",
field: Box::new(SettingField {
pick: |settings_content| {
if let Some(git_panel) = &settings_content.git_panel {
@@ -2940,6 +2980,25 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Debugger Panel"),
SettingsPageItem::SettingItem(SettingItem {
title: "Debugger Panel Dock",
description: "The dock position of the debug panel",
field: Box::new(SettingField {
pick: |settings_content| {
if let Some(debugger) = &settings_content.debugger {
&debugger.dock
} else {
&None
}
},
pick_mut: |settings_content| {
&mut settings_content.debugger.get_or_insert_default().dock
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Notification Panel"),
SettingsPageItem::SettingItem(SettingItem {
title: "Notification Panel Button",
@@ -3636,24 +3695,6 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Dock",
description: "The dock position of the debug panel",
field: Box::new(SettingField {
pick: |settings_content| {
if let Some(debugger) = &settings_content.debugger {
&debugger.dock
} else {
&None
}
},
pick_mut: |settings_content| {
&mut settings_content.debugger.get_or_insert_default().dock
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Log DAP Communications",
description: "Whether to log messages between active debug adapters and Zed",
@@ -3696,24 +3737,6 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Button",
description: "Whether to show the debug button in the status bar",
field: Box::new(SettingField {
pick: |settings_content| {
if let Some(debugger) = &settings_content.debugger {
&debugger.button
} else {
&None
}
},
pick_mut: |settings_content| {
&mut settings_content.debugger.get_or_insert_default().button
},
}),
metadata: None,
files: USER,
}),
],
},
SettingsPage {
@@ -4006,7 +4029,7 @@ fn language_settings_data() -> Vec<SettingsPageItem> {
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Show Wrap Guides",
description: "Whether to show wrap guides in the editor. Setting this to true will show a guide at the 'preferred_line_length' value if softwrap is set to 'preferred_line_length', and will show any additional guides as specified by the 'wrap_guides' setting",
description: "Whether to show wrap guides in the editor",
field: Box::new(SettingField {
pick: |settings_content| {
language_settings_field(settings_content, |language| &language.show_wrap_guides)
@@ -4438,7 +4461,7 @@ fn language_settings_data() -> Vec<SettingsPageItem> {
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Always Treat Brackets As Autoclosed",
description: "Controls how the editor handles the autoclosed characters. When set to `false`(default), skipping over and auto-removing of the closing characters happen only for auto-inserted characters. Otherwise(when `true`), the closing characters are always skipped over and auto-removed no matter how they were inserted",
description: "Controls whether the closing characters are always skipped over and auto-removed no matter how they were inserted",
field: Box::new(SettingField {
pick: |settings_content| {
language_settings_field(settings_content, |language| {
@@ -4518,7 +4541,7 @@ fn language_settings_data() -> Vec<SettingsPageItem> {
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Linked Edits",
description: "Whether to perform linked edits of associated ranges, if the language server supports it. For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well",
description: "Whether to perform linked edits of associated ranges, if the LS supports it. For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well",
field: Box::new(SettingField {
pick: |settings_content| {
language_settings_field(settings_content, |language| &language.linked_edits)
+49 -50
View File
@@ -29,8 +29,9 @@ use std::{
sync::{Arc, LazyLock, RwLock, atomic::AtomicBool},
};
use ui::{
ContextMenu, Divider, DropdownMenu, DropdownStyle, IconButtonShape, KeyBinding, KeybindingHint,
PopoverMenu, Switch, SwitchColor, TreeViewItem, WithScrollbar, prelude::*,
ContextMenu, Divider, DividerColor, DropdownMenu, DropdownStyle, IconButtonShape, KeyBinding,
KeybindingHint, PopoverMenu, Switch, SwitchColor, Tooltip, TreeViewItem, WithScrollbar,
prelude::*,
};
use ui_input::{NumberField, NumberFieldType};
use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
@@ -242,6 +243,9 @@ fn init_renderers(cx: &mut App) {
.icon_color(Color::Error)
.icon_size(IconSize::Small)
.style(ButtonStyle::Outlined)
.tooltip(Tooltip::text(
"This warning is only displayed in dev builds.",
))
.into_any_element()
})
.add_renderer::<bool>(|settings_field, file, _, _, cx| {
@@ -554,14 +558,14 @@ impl SettingsPageItem {
match self {
SettingsPageItem::SectionHeader(header) => v_flex()
.w_full()
.gap_1()
.gap_1p5()
.child(
Label::new(SharedString::new_static(header))
.size(LabelSize::XSmall)
.size(LabelSize::Small)
.color(Color::Muted)
.buffer_font(cx),
)
.child(Divider::horizontal().color(ui::DividerColor::BorderVariant))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.into_any_element(),
SettingsPageItem::SettingItem(setting_item) => {
let renderer = cx.default_global::<SettingFieldRenderer>().clone();
@@ -571,8 +575,8 @@ impl SettingsPageItem {
h_flex()
.id(setting_item.title)
.w_full()
.min_w_0()
.gap_2()
.flex_wrap()
.justify_between()
.map(|this| {
if is_last {
@@ -585,8 +589,8 @@ impl SettingsPageItem {
})
.child(
v_flex()
.w_full()
.max_w_1_2()
.flex_shrink()
.child(
h_flex()
.w_full()
@@ -620,6 +624,9 @@ impl SettingsPageItem {
.icon_color(Color::Error)
.icon_size(IconSize::Small)
.style(ButtonStyle::Outlined)
.tooltip(Tooltip::text(
"This warning is only displayed in dev builds.",
))
.into_any_element()
} else {
renderer.render(
@@ -651,7 +658,6 @@ impl SettingsPageItem {
)
.child(
Button::new(("sub-page".into(), sub_page_link.title), "Configure")
.size(ButtonSize::Medium)
.icon(IconName::ChevronRight)
.icon_position(IconPosition::End)
.icon_color(Color::Muted)
@@ -1218,6 +1224,7 @@ impl SettingsWindow {
h_flex()
.py_1()
.px_1p5()
.mb_3()
.gap_1p5()
.rounded_sm()
.bg(cx.theme().colors().editor_background)
@@ -1233,7 +1240,7 @@ impl SettingsWindow {
cx: &mut Context<SettingsWindow>,
) -> impl IntoElement {
let visible_count = self.visible_navbar_entries().count();
let nav_background = cx.theme().colors().panel_background;
let focus_keybind_label = if self.navbar_focus_handle.contains_focused(window, cx) {
"Focus Content"
} else {
@@ -1244,15 +1251,14 @@ impl SettingsWindow {
.w_64()
.p_2p5()
.pt_10()
.gap_3()
.flex_none()
.border_r_1()
.border_color(cx.theme().colors().border)
.bg(nav_background)
.bg(cx.theme().colors().panel_background)
.child(self.render_search(window, cx))
.child(
v_flex()
.flex_grow()
.size_full()
.track_focus(&self.navbar_focus_handle)
.tab_group()
.tab_index(NAVBAR_GROUP_TAB_INDEX)
@@ -1280,53 +1286,46 @@ impl SettingsWindow {
},
))
})
.on_click(cx.listener(
move |this, evt: &gpui::ClickEvent, window, cx| {
this.navbar_entry = ix;
.on_click(cx.listener(move |this, _, _, cx| {
this.navbar_entry = ix;
if !this.navbar_entries[ix].is_root {
let mut selected_page_ix = ix;
if !this.navbar_entries[ix].is_root {
let mut selected_page_ix = ix;
while !this.navbar_entries[selected_page_ix]
.is_root
{
selected_page_ix -= 1;
}
let section_header = ix - selected_page_ix;
if let Some(section_index) = this
.page_items()
.enumerate()
.filter(|item| {
matches!(
item.1,
SettingsPageItem::SectionHeader(_)
)
})
.take(section_header)
.last()
.map(|pair| pair.0)
{
this.scroll_handle
.scroll_to_top_of_item(section_index);
}
while !this.navbar_entries[selected_page_ix].is_root
{
selected_page_ix -= 1;
}
if evt.is_keyboard() {
// todo(settings_ui): Focus the actual item and scroll to it
this.focus_first_content_item(window, cx);
let section_header = ix - selected_page_ix;
if let Some(section_index) = this
.page_items()
.enumerate()
.filter(|item| {
matches!(
item.1,
SettingsPageItem::SectionHeader(_)
)
})
.take(section_header)
.last()
.map(|pair| pair.0)
{
this.scroll_handle
.scroll_to_top_of_item(section_index);
}
cx.notify();
},
))
}
cx.notify();
}))
.into_any_element()
})
.collect()
}),
)
.track_scroll(self.list_handle.clone())
.flex_grow(),
.size_full()
.track_scroll(self.list_handle.clone()),
)
.vertical_scrollbar_for(self.list_handle.clone(), window, cx),
)
@@ -1335,6 +1334,7 @@ impl SettingsWindow {
.w_full()
.p_2()
.pb_0p5()
.flex_none()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.children(
@@ -1804,7 +1804,6 @@ fn render_number_field<T: NumberFieldType + Send + Sync>(
.log_err(); // todo(settings_ui) don't log err
}
})
.tab_index(0)
.into_any_element()
}
+5 -5
View File
@@ -126,11 +126,12 @@ impl Toggleable for TreeViewItem {
impl RenderOnce for TreeViewItem {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let selected_bg = cx.theme().colors().element_active.opacity(0.5);
let selected_border = cx.theme().colors().border.opacity(0.6);
let focused_border = cx.theme().colors().border_focused;
let transparent_border = cx.theme().colors().border_transparent;
let item_size = rems_from_px(28.);
let item_size = rems_from_px(28.);
let indentation_line = h_flex().size(item_size).flex_none().justify_center().child(
div()
.w_px()
@@ -149,14 +150,12 @@ impl RenderOnce for TreeViewItem {
.cursor_pointer()
.size_full()
.relative()
.when_some(self.tab_index, |this, index| this.tab_index(index))
.map(|this| {
let label = self.label;
if self.root_item {
this.h(item_size)
.px_1()
.mb_1()
.gap_2p5()
.rounded_sm()
.border_1()
@@ -166,6 +165,7 @@ impl RenderOnce for TreeViewItem {
})
.focus(|s| s.border_color(focused_border))
.hover(|s| s.bg(cx.theme().colors().element_hover))
.when_some(self.tab_index, |this, index| this.tab_index(index))
.child(
Disclosure::new("toggle", self.expanded)
.when_some(
@@ -190,13 +190,13 @@ impl RenderOnce for TreeViewItem {
.px_1()
.rounded_sm()
.border_1()
.focusable()
.border_color(transparent_border)
.when(self.selected, |this| {
this.border_color(selected_border).bg(selected_bg)
})
.in_focus(|s| s.border_color(focused_border))
.focus(|s| s.border_color(focused_border))
.hover(|s| s.bg(cx.theme().colors().element_hover))
.when_some(self.tab_index, |this, index| this.tab_index(index))
.child(
Label::new(label)
.when(!self.selected, |this| this.color(Color::Muted)),
+52 -60
View File
@@ -6,7 +6,7 @@ use std::{
};
use editor::{Editor, EditorStyle};
use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers};
use gpui::{ClickEvent, CursorStyle, Entity, FocusHandle, Focusable, FontWeight, Modifiers};
use settings::{CodeFade, MinimumContrast};
use ui::prelude::*;
@@ -344,6 +344,27 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
}
};
let bg_color = cx.theme().colors().surface_background;
let hover_bg_color = cx.theme().colors().element_hover;
let border_color = cx.theme().colors().border_variant;
let focus_border_color = cx.theme().colors().border_focused;
let base_button = |icon: IconName| {
h_flex()
.cursor(CursorStyle::PointingHand)
.p_1p5()
.size_full()
.justify_center()
.overflow_hidden()
.border_1()
.border_color(border_color)
.bg(bg_color)
.hover(|s| s.bg(hover_bg_color))
.focus(|s| s.border_color(focus_border_color).bg(hover_bg_color))
.child(Icon::new(icon).size(IconSize::Small))
};
h_flex()
.id(self.id.clone())
.track_focus(&self.focus_handle)
@@ -376,28 +397,19 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
};
decrement.child(
h_flex()
base_button(IconName::Dash)
.id("decrement_button")
.cursor(gpui::CursorStyle::PointingHand)
.p_1p5()
.size_full()
.justify_center()
.overflow_hidden()
.rounded_tl_sm()
.rounded_bl_sm()
.border_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().surface_background)
.hover(|s| s.bg(cx.theme().colors().element_hover))
.child(Icon::new(IconName::Dash).size(IconSize::Small))
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1).focus(|style| {
style
.border_color(cx.theme().colors().border_focused)
.bg(cx.theme().colors().element_hover)
})
})
.tab_index(
tab_index
.as_mut()
.map(|tab_index| {
*tab_index += 1;
*tab_index - 1
})
.unwrap_or(0),
)
.on_click(decrement_handler),
)
})
@@ -406,34 +418,23 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
.min_w_16()
.size_full()
.border_y_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().surface_background)
.in_focus(|this| this.border_color(cx.theme().colors().border_focused))
.border_color(border_color)
.bg(bg_color)
.in_focus(|this| this.border_color(focus_border_color))
.child(match *self.mode.read(cx) {
NumberFieldMode::Read => h_flex()
.id("numeric_stepper_label")
.px_1()
.flex_1()
.justify_center()
.child(Label::new((self.format)(&self.value)))
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1).focus(|style| {
style
.border_color(cx.theme().colors().border_focused)
.bg(cx.theme().colors().element_hover)
})
})
.on_click({
let _mode = self.mode.clone();
move |click, _, _cx| {
if click.click_count() == 2 || click.is_keyboard() {
// Edit mode is disabled until we implement center text alignment for editor
// mode.write(cx, NumberFieldMode::Edit);
}
}
})
.into_any_element(),
// Edit mode is disabled until we implement center text alignment for editor
// mode.write(cx, NumberFieldMode::Edit);
//
// When we get to making Edit mode work, we shouldn't even focus the decrement/increment buttons.
// Focus should go instead straight to the editor, avoiding any double-step focus.
// In this world, the buttons become a mouse-only interaction, given users should be able
// to do everything they'd do with the buttons straight in the editor anyway.
NumberFieldMode::Edit => h_flex()
.flex_1()
.child(window.use_state(cx, {
@@ -501,28 +502,19 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
};
increment.child(
h_flex()
base_button(IconName::Plus)
.id("increment_button")
.cursor(gpui::CursorStyle::PointingHand)
.p_1p5()
.size_full()
.justify_center()
.overflow_hidden()
.rounded_tr_sm()
.rounded_br_sm()
.border_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().surface_background)
.hover(|s| s.bg(cx.theme().colors().element_hover))
.child(Icon::new(IconName::Plus).size(IconSize::Small))
.when_some(tab_index.as_mut(), |this, tab_index| {
*tab_index += 1;
this.tab_index(*tab_index - 1).focus(|style| {
style
.border_color(cx.theme().colors().border_focused)
.bg(cx.theme().colors().element_hover)
})
})
.tab_index(
tab_index
.as_mut()
.map(|tab_index| {
*tab_index += 1;
*tab_index - 1
})
.unwrap_or(0),
)
.on_click(increment_handler),
)
}),