diff --git a/crates/theme/src/registry.rs b/crates/theme/src/registry.rs index 849223c104..7b101c2329 100644 --- a/crates/theme/src/registry.rs +++ b/crates/theme/src/registry.rs @@ -212,6 +212,19 @@ impl ThemeRegistry { self.get_icon_theme(DEFAULT_ICON_THEME_NAME) } + /// Returns the metadata of all icon themes in the registry. + pub fn list_icon_themes(&self) -> Vec { + self.state + .read() + .icon_themes + .values() + .map(|theme| ThemeMeta { + name: theme.name.clone(), + appearance: theme.appearance, + }) + .collect() + } + /// Returns the icon theme with the specified name. pub fn get_icon_theme(&self, name: &str) -> Result> { self.state diff --git a/crates/theme_selector/src/icon_theme_selector.rs b/crates/theme_selector/src/icon_theme_selector.rs new file mode 100644 index 0000000000..28af76ad71 --- /dev/null +++ b/crates/theme_selector/src/icon_theme_selector.rs @@ -0,0 +1,274 @@ +use fs::Fs; +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{ + App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, UpdateGlobal, WeakEntity, + Window, +}; +use picker::{Picker, PickerDelegate}; +use settings::{update_settings_file, Settings as _, SettingsStore}; +use std::sync::Arc; +use theme::{IconTheme, ThemeMeta, ThemeRegistry, ThemeSettings}; +use ui::{prelude::*, v_flex, ListItem, ListItemSpacing}; +use util::ResultExt; +use workspace::{ui::HighlightedLabel, ModalView}; + +pub(crate) struct IconThemeSelector { + picker: Entity>, +} + +impl EventEmitter for IconThemeSelector {} + +impl Focusable for IconThemeSelector { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl ModalView for IconThemeSelector {} + +impl IconThemeSelector { + pub fn new( + delegate: IconThemeSelectorDelegate, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); + Self { picker } + } +} + +impl Render for IconThemeSelector { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + v_flex().w(rems(34.)).child(self.picker.clone()) + } +} + +pub(crate) struct IconThemeSelectorDelegate { + fs: Arc, + themes: Vec, + matches: Vec, + original_theme: Arc, + selection_completed: bool, + selected_index: usize, + selector: WeakEntity, +} + +impl IconThemeSelectorDelegate { + pub fn new( + selector: WeakEntity, + fs: Arc, + themes_filter: Option<&Vec>, + cx: &mut Context, + ) -> Self { + let theme_settings = ThemeSettings::get_global(cx); + let original_theme = theme_settings.active_icon_theme.clone(); + + let registry = ThemeRegistry::global(cx); + let mut themes = registry + .list_icon_themes() + .into_iter() + .filter(|meta| { + if let Some(theme_filter) = themes_filter { + theme_filter.contains(&meta.name.to_string()) + } else { + true + } + }) + .collect::>(); + + themes.sort_unstable_by(|a, b| { + a.appearance + .is_light() + .cmp(&b.appearance.is_light()) + .then(a.name.cmp(&b.name)) + }); + let matches = themes + .iter() + .map(|meta| StringMatch { + candidate_id: 0, + score: 0.0, + positions: Default::default(), + string: meta.name.to_string(), + }) + .collect(); + let mut this = Self { + fs, + themes, + matches, + original_theme: original_theme.clone(), + selected_index: 0, + selection_completed: false, + selector, + }; + + this.select_if_matching(&original_theme.name); + this + } + + fn show_selected_theme(&mut self, cx: &mut Context>) { + if let Some(mat) = self.matches.get(self.selected_index) { + let registry = ThemeRegistry::global(cx); + match registry.get_icon_theme(&mat.string) { + Ok(theme) => { + Self::set_icon_theme(theme, cx); + } + Err(err) => { + log::error!("error loading icon theme {}: {err}", mat.string); + } + } + } + } + + fn select_if_matching(&mut self, theme_name: &str) { + self.selected_index = self + .matches + .iter() + .position(|mat| mat.string == theme_name) + .unwrap_or(self.selected_index); + } + + fn set_icon_theme(theme: Arc, cx: &mut App) { + SettingsStore::update_global(cx, |store, cx| { + let mut theme_settings = store.get::(None).clone(); + theme_settings.active_icon_theme = theme; + store.override_global(theme_settings); + cx.refresh_windows(); + }); + } +} + +impl PickerDelegate for IconThemeSelectorDelegate { + type ListItem = ui::ListItem; + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + "Select Icon Theme...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn confirm( + &mut self, + _: bool, + _window: &mut Window, + cx: &mut Context>, + ) { + self.selection_completed = true; + + let theme_settings = ThemeSettings::get_global(cx); + let theme_name = theme_settings.active_icon_theme.name.clone(); + + telemetry::event!( + "Settings Changed", + setting = "icon_theme", + value = theme_name + ); + + update_settings_file::(self.fs.clone(), cx, move |settings, _| { + settings.icon_theme = Some(theme_name.to_string()); + }); + + self.selector + .update(cx, |_, cx| { + cx.emit(DismissEvent); + }) + .ok(); + } + + fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { + if !self.selection_completed { + Self::set_icon_theme(self.original_theme.clone(), cx); + self.selection_completed = true; + } + + self.selector + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index( + &mut self, + ix: usize, + _: &mut Window, + cx: &mut Context>, + ) { + self.selected_index = ix; + self.show_selected_theme(cx); + } + + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> gpui::Task<()> { + let background = cx.background_executor().clone(); + let candidates = self + .themes + .iter() + .enumerate() + .map(|(id, meta)| StringMatchCandidate::new(id, &meta.name)) + .collect::>(); + + cx.spawn_in(window, |this, mut cx| async move { + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + ) + .await + }; + + this.update(&mut cx, |this, cx| { + this.delegate.matches = matches; + this.delegate.selected_index = this + .delegate + .selected_index + .min(this.delegate.matches.len().saturating_sub(1)); + this.delegate.show_selected_theme(cx); + }) + .log_err(); + }) + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _window: &mut Window, + _cx: &mut Context>, + ) -> Option { + let theme_match = &self.matches[ix]; + + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .toggle_state(selected) + .child(HighlightedLabel::new( + theme_match.string.clone(), + theme_match.positions.clone(), + )), + ) + } +} diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 67e282ac49..ee549091ff 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -1,3 +1,5 @@ +mod icon_theme_selector; + use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ @@ -11,22 +13,25 @@ use theme::{Appearance, Theme, ThemeMeta, ThemeRegistry, ThemeSettings}; use ui::{prelude::*, v_flex, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{ui::HighlightedLabel, ModalView, Workspace}; -use zed_actions::theme_selector::Toggle; + +use crate::icon_theme_selector::{IconThemeSelector, IconThemeSelectorDelegate}; actions!(theme_selector, [Reload]); pub fn init(cx: &mut App) { cx.observe_new( |workspace: &mut Workspace, _window, _cx: &mut Context| { - workspace.register_action(toggle); + workspace + .register_action(toggle_theme_selector) + .register_action(toggle_icon_theme_selector); }, ) .detach(); } -pub fn toggle( +fn toggle_theme_selector( workspace: &mut Workspace, - toggle: &Toggle, + toggle: &zed_actions::theme_selector::Toggle, window: &mut Window, cx: &mut Context, ) { @@ -42,9 +47,27 @@ pub fn toggle( }); } +fn toggle_icon_theme_selector( + workspace: &mut Workspace, + toggle: &zed_actions::icon_theme_selector::Toggle, + window: &mut Window, + cx: &mut Context, +) { + let fs = workspace.app_state().fs.clone(); + workspace.toggle_modal(window, cx, |window, cx| { + let delegate = IconThemeSelectorDelegate::new( + cx.entity().downgrade(), + fs, + toggle.themes_filter.as_ref(), + cx, + ); + IconThemeSelector::new(delegate, window, cx) + }); +} + impl ModalView for ThemeSelector {} -pub struct ThemeSelector { +struct ThemeSelector { picker: Entity>, } @@ -73,7 +96,7 @@ impl ThemeSelector { } } -pub struct ThemeSelectorDelegate { +struct ThemeSelectorDelegate { fs: Arc, themes: Vec, matches: Vec, diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 892f9d0962..b087e8cbe2 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -77,6 +77,20 @@ pub mod theme_selector { impl_actions!(theme_selector, [Toggle]); } +pub mod icon_theme_selector { + use gpui::impl_actions; + use schemars::JsonSchema; + use serde::Deserialize; + + #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)] + pub struct Toggle { + /// A list of icon theme names to filter the theme selector down to. + pub themes_filter: Option>, + } + + impl_actions!(icon_theme_selector, [Toggle]); +} + pub mod assistant { use gpui::{actions, impl_actions}; use schemars::JsonSchema;