Skip to content

Commit c374532

Browse files
settings_ui: Fix dropdowns after #41036 (#41920) (cherry-pick) (#41930)
Cherry-pick of #41920 ---- Closes #41533 Both of the issues in the release notes that are fixed in this PR, were caused by incorrect usage of the `window.use_state` API. The first issue was caused by calling `window.use_state` in a render helper, resulting in the element ID used to share state being the same across different pages, resulting in the state being re-used when it should have been re-created. The fix for this was to move the `window.state` (and rendering logic) into a `impl RenderOnce` component, so that the IDs are resolved during the render, avoiding the state conflicts. The second issue is caused by using a `move` closure in the `window.use_state` call, resulting in stale closure values when the window state is re-used. Release Notes: - settings_ui: Fixed an issue where some dropdown menus would show options from a different dropdown when clicked - settings_ui: Fixed an issue where attempting to change a setting in a dropdown back to it's original value after changing it would do nothing Co-authored-by: Ben Kunkle <[email protected]>
1 parent a677ecc commit c374532

File tree

3 files changed

+125
-51
lines changed

3 files changed

+125
-51
lines changed

crates/settings_ui/src/components.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
mod dropdown;
12
mod font_picker;
23
mod icon_theme_picker;
34
mod input_field;
45
mod theme_picker;
56

7+
pub use dropdown::*;
68
pub use font_picker::font_picker;
79
pub use icon_theme_picker::icon_theme_picker;
810
pub use input_field::*;
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use std::rc::Rc;
2+
3+
use gpui::{App, ElementId, IntoElement, RenderOnce};
4+
use heck::ToTitleCase as _;
5+
use ui::{
6+
ButtonSize, ContextMenu, DropdownMenu, DropdownStyle, FluentBuilder as _, IconPosition, px,
7+
};
8+
9+
#[derive(IntoElement)]
10+
pub struct EnumVariantDropdown<T>
11+
where
12+
T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static,
13+
{
14+
id: ElementId,
15+
current_value: T,
16+
variants: &'static [T],
17+
labels: &'static [&'static str],
18+
should_do_title_case: bool,
19+
tab_index: Option<isize>,
20+
on_change: Rc<dyn Fn(T, &mut App) + 'static>,
21+
}
22+
23+
impl<T> EnumVariantDropdown<T>
24+
where
25+
T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static,
26+
{
27+
pub fn new(
28+
id: impl Into<ElementId>,
29+
current_value: T,
30+
variants: &'static [T],
31+
labels: &'static [&'static str],
32+
on_change: impl Fn(T, &mut App) + 'static,
33+
) -> Self {
34+
Self {
35+
id: id.into(),
36+
current_value,
37+
variants,
38+
labels,
39+
should_do_title_case: true,
40+
tab_index: None,
41+
on_change: Rc::new(on_change),
42+
}
43+
}
44+
45+
pub fn title_case(mut self, title_case: bool) -> Self {
46+
self.should_do_title_case = title_case;
47+
self
48+
}
49+
50+
pub fn tab_index(mut self, tab_index: isize) -> Self {
51+
self.tab_index = Some(tab_index);
52+
self
53+
}
54+
}
55+
56+
impl<T> RenderOnce for EnumVariantDropdown<T>
57+
where
58+
T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static,
59+
{
60+
fn render(self, window: &mut ui::Window, cx: &mut ui::App) -> impl gpui::IntoElement {
61+
let current_value_label = self.labels[self
62+
.variants
63+
.iter()
64+
.position(|v| *v == self.current_value)
65+
.unwrap()];
66+
67+
let context_menu = window.use_keyed_state(current_value_label, cx, |window, cx| {
68+
ContextMenu::new(window, cx, move |mut menu, _, _| {
69+
for (&value, &label) in std::iter::zip(self.variants, self.labels) {
70+
let on_change = self.on_change.clone();
71+
let current_value = self.current_value;
72+
menu = menu.toggleable_entry(
73+
if self.should_do_title_case {
74+
label.to_title_case()
75+
} else {
76+
label.to_string()
77+
},
78+
value == current_value,
79+
IconPosition::End,
80+
None,
81+
move |_, cx| {
82+
on_change(value, cx);
83+
},
84+
);
85+
}
86+
menu
87+
})
88+
});
89+
90+
DropdownMenu::new(
91+
self.id,
92+
if self.should_do_title_case {
93+
current_value_label.to_title_case()
94+
} else {
95+
current_value_label.to_string()
96+
},
97+
context_menu,
98+
)
99+
.when_some(self.tab_index, |elem, tab_index| elem.tab_index(tab_index))
100+
.trigger_size(ButtonSize::Medium)
101+
.style(DropdownStyle::Outlined)
102+
.offset(gpui::Point {
103+
x: px(0.0),
104+
y: px(2.0),
105+
})
106+
.into_any_element()
107+
}
108+
}

crates/settings_ui/src/settings_ui.rs

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use gpui::{
1111
TitlebarOptions, UniformListScrollHandle, Window, WindowBounds, WindowHandle, WindowOptions,
1212
actions, div, list, point, prelude::*, px, uniform_list,
1313
};
14-
use heck::ToTitleCase as _;
1514
use project::{Project, WorktreeId};
1615
use release_channel::ReleaseChannel;
1716
use schemars::JsonSchema;
@@ -38,7 +37,9 @@ use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
3837
use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decorations};
3938
use zed_actions::{OpenSettings, OpenSettingsAt};
4039

41-
use crate::components::{SettingsInputField, font_picker, icon_theme_picker, theme_picker};
40+
use crate::components::{
41+
EnumVariantDropdown, SettingsInputField, font_picker, icon_theme_picker, theme_picker,
42+
};
4243

4344
const NAVBAR_CONTAINER_TAB_INDEX: isize = 0;
4445
const NAVBAR_GROUP_TAB_INDEX: isize = 1;
@@ -3325,7 +3326,7 @@ fn render_dropdown<T>(
33253326
field: SettingField<T>,
33263327
file: SettingsUiFile,
33273328
metadata: Option<&SettingsFieldMetadata>,
3328-
window: &mut Window,
3329+
_window: &mut Window,
33293330
cx: &mut App,
33303331
) -> AnyElement
33313332
where
@@ -3341,56 +3342,19 @@ where
33413342
SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
33423343
let current_value = current_value.copied().unwrap_or(variants()[0]);
33433344

3344-
let current_value_label =
3345-
labels()[variants().iter().position(|v| *v == current_value).unwrap()];
3346-
3347-
DropdownMenu::new(
3348-
"dropdown",
3349-
if should_do_titlecase {
3350-
current_value_label.to_title_case()
3351-
} else {
3352-
current_value_label.to_string()
3353-
},
3354-
window.use_state(cx, |window, cx| {
3355-
ContextMenu::new(window, cx, move |mut menu, _, _| {
3356-
for (&value, &label) in std::iter::zip(variants(), labels()) {
3357-
let file = file.clone();
3358-
menu = menu.toggleable_entry(
3359-
if should_do_titlecase {
3360-
label.to_title_case()
3361-
} else {
3362-
label.to_string()
3363-
},
3364-
value == current_value,
3365-
IconPosition::End,
3366-
None,
3367-
move |_, cx| {
3368-
if value == current_value {
3369-
return;
3370-
}
3371-
update_settings_file(
3372-
file.clone(),
3373-
field.json_path,
3374-
cx,
3375-
move |settings, _cx| {
3376-
(field.write)(settings, Some(value));
3377-
},
3378-
)
3379-
.log_err(); // todo(settings_ui) don't log err
3380-
},
3381-
);
3382-
}
3383-
menu
3345+
EnumVariantDropdown::new("dropdown", current_value, variants(), labels(), {
3346+
move |value, cx| {
3347+
if value == current_value {
3348+
return;
3349+
}
3350+
update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
3351+
(field.write)(settings, Some(value));
33843352
})
3385-
}),
3386-
)
3387-
.tab_index(0)
3388-
.trigger_size(ButtonSize::Medium)
3389-
.style(DropdownStyle::Outlined)
3390-
.offset(gpui::Point {
3391-
x: px(0.0),
3392-
y: px(2.0),
3353+
.log_err(); // todo(settings_ui) don't log err
3354+
}
33933355
})
3356+
.tab_index(0)
3357+
.title_case(should_do_titlecase)
33943358
.into_any_element()
33953359
}
33963360

0 commit comments

Comments
 (0)