settings_ui: Add pickers for theme and icon themes (#40829)
In the process of adding pickers for the theme and icon themes fields in the settings UI, I felt like there was an improvement opportunity in regards to where some of these components are stored. The `ui_input` crate originally was meant only for the text field-like component, which couldn't be in the regular `ui` crate due to the dependency with `editor`. Given we had also added the number field there—which is similar in also having the same dependency—it made sense to think of this crate more like a home for form-like components rather than for only one component. However, we were also storing some settings UI-specific stuff in that crate, which didn't feel right. So I ended up creating a new directory within the `settings_ui` for components and moved all the pickers and the custom input field there. I think this makes it for a cleaner structure. Release Notes: - settings_ui: Added the ability to search for theme and icon themes in their respective fields.
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use theme::FontFamilyCache;
|
||||
use ui::{ListItem, ListItemSpacing, prelude::*};
|
||||
|
||||
type FontPicker = Picker<FontPickerDelegate>;
|
||||
|
||||
pub struct FontPickerDelegate {
|
||||
fonts: Vec<SharedString>,
|
||||
filtered_fonts: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
current_font: SharedString,
|
||||
on_font_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl FontPickerDelegate {
|
||||
fn new(
|
||||
current_font: SharedString,
|
||||
on_font_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
cx: &mut Context<FontPicker>,
|
||||
) -> Self {
|
||||
let font_family_cache = FontFamilyCache::global(cx);
|
||||
|
||||
let fonts = font_family_cache
|
||||
.try_list_font_families()
|
||||
.unwrap_or_else(|| vec![current_font.clone()]);
|
||||
let selected_index = fonts
|
||||
.iter()
|
||||
.position(|font| *font == current_font)
|
||||
.unwrap_or(0);
|
||||
|
||||
let filtered_fonts = fonts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, font)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: font.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
fonts,
|
||||
filtered_fonts,
|
||||
selected_index,
|
||||
current_font,
|
||||
on_font_changed: Arc::new(on_font_changed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for FontPickerDelegate {
|
||||
type ListItem = AnyElement;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.filtered_fonts.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<FontPicker>) {
|
||||
self.selected_index = ix.min(self.filtered_fonts.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Search fonts…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<FontPicker>,
|
||||
) -> Task<()> {
|
||||
let fonts = self.fonts.clone();
|
||||
let current_font = self.current_font.clone();
|
||||
|
||||
let matches: Vec<StringMatch> = if query.is_empty() {
|
||||
fonts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, font)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: font.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
let _candidates: Vec<StringMatchCandidate> = fonts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, font)| StringMatchCandidate::new(id, font.as_ref()))
|
||||
.collect();
|
||||
|
||||
fonts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, font)| font.to_lowercase().contains(&query.to_lowercase()))
|
||||
.map(|(index, font)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: font.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let selected_index = if query.is_empty() {
|
||||
fonts
|
||||
.iter()
|
||||
.position(|font| *font == current_font)
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
matches
|
||||
.iter()
|
||||
.position(|m| fonts[m.candidate_id] == current_font)
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
self.filtered_fonts = matches;
|
||||
self.selected_index = selected_index;
|
||||
cx.notify();
|
||||
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<FontPicker>) {
|
||||
if let Some(font_match) = self.filtered_fonts.get(self.selected_index) {
|
||||
let font = font_match.string.clone();
|
||||
(self.on_font_changed)(font.into(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<FontPicker>) {
|
||||
cx.defer_in(window, |picker, window, cx| {
|
||||
picker.set_query("", window, cx);
|
||||
});
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<FontPicker>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let font_match = self.filtered_fonts.get(ix)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(Label::new(font_match.string.clone()))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn font_picker(
|
||||
current_font: SharedString,
|
||||
on_font_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<FontPicker>,
|
||||
) -> FontPicker {
|
||||
let delegate = FontPickerDelegate::new(current_font, on_font_changed, cx);
|
||||
|
||||
Picker::uniform_list(delegate, window, cx)
|
||||
.show_scrollbar(true)
|
||||
.width(rems_from_px(210.))
|
||||
.max_height(Some(rems(18.).into()))
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use theme::ThemeRegistry;
|
||||
use ui::{ListItem, ListItemSpacing, prelude::*};
|
||||
|
||||
type IconThemePicker = Picker<IconThemePickerDelegate>;
|
||||
|
||||
pub struct IconThemePickerDelegate {
|
||||
icon_themes: Vec<SharedString>,
|
||||
filtered_themes: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl IconThemePickerDelegate {
|
||||
fn new(
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
cx: &mut Context<IconThemePicker>,
|
||||
) -> Self {
|
||||
let theme_registry = ThemeRegistry::global(cx);
|
||||
|
||||
let icon_themes: Vec<SharedString> = theme_registry
|
||||
.list_icon_themes()
|
||||
.into_iter()
|
||||
.map(|theme_meta| theme_meta.name)
|
||||
.collect();
|
||||
|
||||
let selected_index = icon_themes
|
||||
.iter()
|
||||
.position(|icon_themes| *icon_themes == current_theme)
|
||||
.unwrap_or(0);
|
||||
|
||||
let filtered_themes = icon_themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, icon_themes)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: icon_themes.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
icon_themes,
|
||||
filtered_themes,
|
||||
selected_index,
|
||||
current_theme,
|
||||
on_theme_changed: Arc::new(on_theme_changed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for IconThemePickerDelegate {
|
||||
type ListItem = AnyElement;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.filtered_themes.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<IconThemePicker>) {
|
||||
self.selected_index = ix.min(self.filtered_themes.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Search icon theme…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<IconThemePicker>,
|
||||
) -> Task<()> {
|
||||
let icon_themes = self.icon_themes.clone();
|
||||
let current_theme = self.current_theme.clone();
|
||||
|
||||
let matches: Vec<StringMatch> = if query.is_empty() {
|
||||
icon_themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, icon_theme)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: icon_theme.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
let _candidates: Vec<StringMatchCandidate> = icon_themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, icon_theme)| StringMatchCandidate::new(id, icon_theme.as_ref()))
|
||||
.collect();
|
||||
|
||||
icon_themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, icon_theme)| icon_theme.to_lowercase().contains(&query.to_lowercase()))
|
||||
.map(|(index, icon_theme)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: icon_theme.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let selected_index = if query.is_empty() {
|
||||
icon_themes
|
||||
.iter()
|
||||
.position(|icon_theme| *icon_theme == current_theme)
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
matches
|
||||
.iter()
|
||||
.position(|m| icon_themes[m.candidate_id] == current_theme)
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
self.filtered_themes = matches;
|
||||
self.selected_index = selected_index;
|
||||
cx.notify();
|
||||
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<IconThemePicker>,
|
||||
) {
|
||||
if let Some(theme_match) = self.filtered_themes.get(self.selected_index) {
|
||||
let theme = theme_match.string.clone();
|
||||
(self.on_theme_changed)(theme.into(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<IconThemePicker>) {
|
||||
cx.defer_in(window, |picker, window, cx| {
|
||||
picker.set_query("", window, cx);
|
||||
});
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<IconThemePicker>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let theme_match = self.filtered_themes.get(ix)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(Label::new(theme_match.string.clone()))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon_theme_picker(
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<IconThemePicker>,
|
||||
) -> IconThemePicker {
|
||||
let delegate = IconThemePickerDelegate::new(current_theme, on_theme_changed, cx);
|
||||
|
||||
Picker::uniform_list(delegate, window, cx)
|
||||
.show_scrollbar(true)
|
||||
.width(rems_from_px(210.))
|
||||
.max_height(Some(rems(18.).into()))
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
use editor::Editor;
|
||||
use gpui::{Focusable, div};
|
||||
use ui::{
|
||||
ActiveTheme as _, App, FluentBuilder as _, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, RenderOnce, Styled as _, Window,
|
||||
};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct SettingsInputField {
|
||||
initial_text: Option<String>,
|
||||
placeholder: Option<&'static str>,
|
||||
confirm: Option<Box<dyn Fn(Option<String>, &mut App)>>,
|
||||
tab_index: Option<isize>,
|
||||
}
|
||||
|
||||
impl SettingsInputField {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
initial_text: None,
|
||||
placeholder: None,
|
||||
confirm: None,
|
||||
tab_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_initial_text(mut self, initial_text: String) -> Self {
|
||||
self.initial_text = Some(initial_text);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_placeholder(mut self, placeholder: &'static str) -> Self {
|
||||
self.placeholder = Some(placeholder);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_confirm(mut self, confirm: impl Fn(Option<String>, &mut App) + 'static) -> Self {
|
||||
self.confirm = Some(Box::new(confirm));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn tab_index(mut self, arg: isize) -> Self {
|
||||
self.tab_index = Some(arg);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SettingsInputField {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
|
||||
let editor = window.use_state(cx, {
|
||||
move |window, cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
if let Some(text) = self.initial_text {
|
||||
editor.set_text(text, window, cx);
|
||||
}
|
||||
|
||||
if let Some(placeholder) = self.placeholder {
|
||||
editor.set_placeholder_text(placeholder, window, cx);
|
||||
}
|
||||
// todo(settings_ui): We should have an observe global use for settings store
|
||||
// so whenever a settings file is updated, the settings ui updates too
|
||||
editor
|
||||
}
|
||||
});
|
||||
|
||||
let weak_editor = editor.downgrade();
|
||||
|
||||
let theme_colors = cx.theme().colors();
|
||||
|
||||
div()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.min_w_64()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(theme_colors.border)
|
||||
.bg(theme_colors.editor_background)
|
||||
.when_some(self.tab_index, |this, tab_index| {
|
||||
let focus_handle = editor.focus_handle(cx).tab_index(tab_index).tab_stop(true);
|
||||
this.track_focus(&focus_handle)
|
||||
.focus(|s| s.border_color(theme_colors.border_focused))
|
||||
})
|
||||
.child(editor)
|
||||
.when_some(self.confirm, |this, confirm| {
|
||||
this.on_action::<menu::Confirm>({
|
||||
move |_, _, cx| {
|
||||
let Some(editor) = weak_editor.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let new_value = editor.read_with(cx, |editor, cx| editor.text(cx));
|
||||
let new_value = (!new_value.is_empty()).then_some(new_value);
|
||||
confirm(new_value, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use theme::ThemeRegistry;
|
||||
use ui::{ListItem, ListItemSpacing, prelude::*};
|
||||
|
||||
type ThemePicker = Picker<ThemePickerDelegate>;
|
||||
|
||||
pub struct ThemePickerDelegate {
|
||||
themes: Vec<SharedString>,
|
||||
filtered_themes: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl ThemePickerDelegate {
|
||||
fn new(
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
cx: &mut Context<ThemePicker>,
|
||||
) -> Self {
|
||||
let theme_registry = ThemeRegistry::global(cx);
|
||||
|
||||
let themes = theme_registry.list_names();
|
||||
let selected_index = themes
|
||||
.iter()
|
||||
.position(|theme| *theme == current_theme)
|
||||
.unwrap_or(0);
|
||||
|
||||
let filtered_themes = themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, theme)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: theme.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
themes,
|
||||
filtered_themes,
|
||||
selected_index,
|
||||
current_theme,
|
||||
on_theme_changed: Arc::new(on_theme_changed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ThemePickerDelegate {
|
||||
type ListItem = AnyElement;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.filtered_themes.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<ThemePicker>) {
|
||||
self.selected_index = ix.min(self.filtered_themes.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Search theme…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<ThemePicker>,
|
||||
) -> Task<()> {
|
||||
let themes = self.themes.clone();
|
||||
let current_theme = self.current_theme.clone();
|
||||
|
||||
let matches: Vec<StringMatch> = if query.is_empty() {
|
||||
themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, theme)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: theme.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
let _candidates: Vec<StringMatchCandidate> = themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, theme)| StringMatchCandidate::new(id, theme.as_ref()))
|
||||
.collect();
|
||||
|
||||
themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, theme)| theme.to_lowercase().contains(&query.to_lowercase()))
|
||||
.map(|(index, theme)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: theme.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let selected_index = if query.is_empty() {
|
||||
themes
|
||||
.iter()
|
||||
.position(|theme| *theme == current_theme)
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
matches
|
||||
.iter()
|
||||
.position(|m| themes[m.candidate_id] == current_theme)
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
self.filtered_themes = matches;
|
||||
self.selected_index = selected_index;
|
||||
cx.notify();
|
||||
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<ThemePicker>) {
|
||||
if let Some(theme_match) = self.filtered_themes.get(self.selected_index) {
|
||||
let theme = theme_match.string.clone();
|
||||
(self.on_theme_changed)(theme.into(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<ThemePicker>) {
|
||||
cx.defer_in(window, |picker, window, cx| {
|
||||
picker.set_query("", window, cx);
|
||||
});
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<ThemePicker>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let theme_match = self.filtered_themes.get(ix)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(Label::new(theme_match.string.clone()))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn theme_picker(
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ThemePicker>,
|
||||
) -> ThemePicker {
|
||||
let delegate = ThemePickerDelegate::new(current_theme, on_theme_changed, cx);
|
||||
|
||||
Picker::uniform_list(delegate, window, cx)
|
||||
.show_scrollbar(true)
|
||||
.width(rems_from_px(210.))
|
||||
.max_height(Some(rems(18.).into()))
|
||||
}
|
||||
Reference in New Issue
Block a user