From 740c444089ec19c15e23b38d9a69749502bf236c Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 24 Jul 2024 16:25:52 -0400 Subject: [PATCH] settings_ui: Add theme settings controls (#15115) This PR adds settings controls for the theme settings. Release Notes: - N/A --- crates/editor/src/editor_settings_controls.rs | 18 +- .../settings/src/editable_setting_control.rs | 10 +- .../src/appearance_settings_controls.rs | 257 ++++++++++++++++++ crates/settings_ui/src/settings_ui.rs | 6 +- .../src/theme_settings_controls.rs | 112 -------- crates/theme/src/settings.rs | 75 ++++- crates/theme_selector/src/theme_selector.rs | 22 +- crates/title_bar/src/application_menu.rs | 2 + crates/ui/src/components/dropdown_menu.rs | 8 +- crates/ui/src/components/numeric_stepper.rs | 12 +- crates/ui/src/components/popover_menu.rs | 26 +- 11 files changed, 392 insertions(+), 156 deletions(-) create mode 100644 crates/settings_ui/src/appearance_settings_controls.rs delete mode 100644 crates/settings_ui/src/theme_settings_controls.rs diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index 815e7b4aee..c8e396f904 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -44,7 +44,11 @@ impl EditableSettingControl for BufferFontSizeControl { settings.buffer_font_size } - fn apply(settings: &mut ::FileContent, value: Self::Value) { + fn apply( + settings: &mut ::FileContent, + value: Self::Value, + _cx: &AppContext, + ) { settings.buffer_font_size = Some(value.into()); } } @@ -84,7 +88,11 @@ impl EditableSettingControl for BufferFontWeightControl { settings.buffer_font.weight } - fn apply(settings: &mut ::FileContent, value: Self::Value) { + fn apply( + settings: &mut ::FileContent, + value: Self::Value, + _cx: &AppContext, + ) { settings.buffer_font_weight = Some(value.0); } } @@ -133,7 +141,11 @@ impl EditableSettingControl for InlineGitBlameControl { settings.git.inline_blame_enabled() } - fn apply(settings: &mut ::FileContent, value: Self::Value) { + fn apply( + settings: &mut ::FileContent, + value: Self::Value, + _cx: &AppContext, + ) { if let Some(inline_blame) = settings.git.inline_blame.as_mut() { inline_blame.enabled = value; } else { diff --git a/crates/settings/src/editable_setting_control.rs b/crates/settings/src/editable_setting_control.rs index a62097006f..5d9eefe604 100644 --- a/crates/settings/src/editable_setting_control.rs +++ b/crates/settings/src/editable_setting_control.rs @@ -20,14 +20,18 @@ pub trait EditableSettingControl: RenderOnce { /// Applies the given setting file to the settings file contents. /// /// This will be called when writing the setting value back to the settings file. - fn apply(settings: &mut ::FileContent, value: Self::Value); + fn apply( + settings: &mut ::FileContent, + value: Self::Value, + cx: &AppContext, + ); /// Writes the given setting value to the settings files. fn write(value: Self::Value, cx: &AppContext) { let fs = ::global(cx); - update_settings_file::(fs, cx, move |settings, _cx| { - Self::apply(settings, value); + update_settings_file::(fs, cx, move |settings, cx| { + Self::apply(settings, value, cx); }); } } diff --git a/crates/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs new file mode 100644 index 0000000000..599f735301 --- /dev/null +++ b/crates/settings_ui/src/appearance_settings_controls.rs @@ -0,0 +1,257 @@ +use gpui::{AppContext, FontWeight}; +use settings::{EditableSettingControl, Settings}; +use theme::{SystemAppearance, ThemeMode, ThemeRegistry, ThemeSettings}; +use ui::{ + prelude::*, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup, + ToggleButton, +}; + +#[derive(IntoElement)] +pub struct AppearanceSettingsControls {} + +impl AppearanceSettingsControls { + pub fn new() -> Self { + Self {} + } +} + +impl RenderOnce for AppearanceSettingsControls { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + SettingsContainer::new() + .child( + SettingsGroup::new("Theme").child( + h_flex() + .gap_2() + .justify_between() + .child(ThemeControl) + .child(ThemeModeControl), + ), + ) + .child( + SettingsGroup::new("Font") + .child(UiFontSizeControl) + .child(UiFontWeightControl), + ) + } +} + +#[derive(IntoElement)] +struct ThemeControl; + +impl EditableSettingControl for ThemeControl { + type Value = String; + type Settings = ThemeSettings; + + fn name(&self) -> SharedString { + "Theme".into() + } + + fn read(cx: &AppContext) -> Self::Value { + let settings = ThemeSettings::get_global(cx); + let appearance = SystemAppearance::global(cx); + settings + .theme_selection + .as_ref() + .map(|selection| selection.theme(appearance.0).to_string()) + .unwrap_or_else(|| ThemeSettings::default_theme(*appearance).to_string()) + } + + fn apply( + settings: &mut ::FileContent, + value: Self::Value, + cx: &AppContext, + ) { + let appearance = SystemAppearance::global(cx); + settings.set_theme(value, appearance.0); + } +} + +impl RenderOnce for ThemeControl { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let value = Self::read(cx); + + DropdownMenu::new( + "theme", + value.clone(), + ContextMenu::build(cx, |mut menu, cx| { + let theme_registry = ThemeRegistry::global(cx); + + for theme in theme_registry.list_names(false) { + menu = menu.custom_entry( + { + let theme = theme.clone(); + move |_cx| Label::new(theme.clone()).into_any_element() + }, + { + let theme = theme.clone(); + move |cx| { + Self::write(theme.to_string(), cx); + } + }, + ) + } + + menu + }), + ) + .full_width(true) + } +} + +#[derive(IntoElement)] +struct ThemeModeControl; + +impl EditableSettingControl for ThemeModeControl { + type Value = ThemeMode; + type Settings = ThemeSettings; + + fn name(&self) -> SharedString { + "Theme Mode".into() + } + + fn read(cx: &AppContext) -> Self::Value { + let settings = ThemeSettings::get_global(cx); + settings + .theme_selection + .as_ref() + .and_then(|selection| selection.mode()) + .unwrap_or_default() + } + + fn apply( + settings: &mut ::FileContent, + value: Self::Value, + _cx: &AppContext, + ) { + settings.set_mode(value); + } +} + +impl RenderOnce for ThemeModeControl { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let value = Self::read(cx); + + h_flex() + .child( + ToggleButton::new("light", "Light") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .selected(value == ThemeMode::Light) + .on_click(|_, cx| Self::write(ThemeMode::Light, cx)) + .first(), + ) + .child( + ToggleButton::new("system", "System") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .selected(value == ThemeMode::System) + .on_click(|_, cx| Self::write(ThemeMode::System, cx)) + .middle(), + ) + .child( + ToggleButton::new("dark", "Dark") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .selected(value == ThemeMode::Dark) + .on_click(|_, cx| Self::write(ThemeMode::Dark, cx)) + .last(), + ) + } +} + +#[derive(IntoElement)] +struct UiFontSizeControl; + +impl EditableSettingControl for UiFontSizeControl { + type Value = Pixels; + type Settings = ThemeSettings; + + fn name(&self) -> SharedString { + "UI Font Size".into() + } + + fn read(cx: &AppContext) -> Self::Value { + let settings = ThemeSettings::get_global(cx); + settings.ui_font_size + } + + fn apply( + settings: &mut ::FileContent, + value: Self::Value, + _cx: &AppContext, + ) { + settings.ui_font_size = Some(value.into()); + } +} + +impl RenderOnce for UiFontSizeControl { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let value = Self::read(cx); + + h_flex() + .gap_2() + .child(Icon::new(IconName::FontSize)) + .child(NumericStepper::new( + value.to_string(), + move |_, cx| { + Self::write(value - px(1.), cx); + }, + move |_, cx| { + Self::write(value + px(1.), cx); + }, + )) + } +} + +#[derive(IntoElement)] +struct UiFontWeightControl; + +impl EditableSettingControl for UiFontWeightControl { + type Value = FontWeight; + type Settings = ThemeSettings; + + fn name(&self) -> SharedString { + "UI Font Weight".into() + } + + fn read(cx: &AppContext) -> Self::Value { + let settings = ThemeSettings::get_global(cx); + settings.ui_font.weight + } + + fn apply( + settings: &mut ::FileContent, + value: Self::Value, + _cx: &AppContext, + ) { + settings.ui_font_weight = Some(value.0); + } +} + +impl RenderOnce for UiFontWeightControl { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let value = Self::read(cx); + + h_flex() + .gap_2() + .child(Icon::new(IconName::FontWeight)) + .child(DropdownMenu::new( + "ui-font-weight", + value.0.to_string(), + ContextMenu::build(cx, |mut menu, _cx| { + for weight in FontWeight::ALL { + menu = menu.custom_entry( + move |_cx| Label::new(weight.0.to_string()).into_any_element(), + { + move |cx| { + Self::write(weight, cx); + } + }, + ) + } + + menu + }), + )) + } +} diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index ad401b197f..b7c2994fa2 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1,4 +1,4 @@ -mod theme_settings_controls; +mod appearance_settings_controls; use std::any::TypeId; @@ -10,7 +10,7 @@ use ui::prelude::*; use workspace::item::{Item, ItemEvent}; use workspace::Workspace; -use crate::theme_settings_controls::ThemeSettingsControls; +use crate::appearance_settings_controls::AppearanceSettingsControls; pub struct SettingsUiFeatureFlag; @@ -110,7 +110,7 @@ impl Render for SettingsPage { v_flex() .gap_1() .child(Label::new("Appearance")) - .child(ThemeSettingsControls::new()), + .child(AppearanceSettingsControls::new()), ) .child( v_flex() diff --git a/crates/settings_ui/src/theme_settings_controls.rs b/crates/settings_ui/src/theme_settings_controls.rs deleted file mode 100644 index 4b7cdb8cf8..0000000000 --- a/crates/settings_ui/src/theme_settings_controls.rs +++ /dev/null @@ -1,112 +0,0 @@ -use gpui::{AppContext, FontWeight}; -use settings::{EditableSettingControl, Settings}; -use theme::ThemeSettings; -use ui::{prelude::*, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup}; - -#[derive(IntoElement)] -pub struct ThemeSettingsControls {} - -impl ThemeSettingsControls { - pub fn new() -> Self { - Self {} - } -} - -impl RenderOnce for ThemeSettingsControls { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - SettingsContainer::new().child( - SettingsGroup::new("Font") - .child(UiFontSizeControl) - .child(UiFontWeightControl), - ) - } -} - -#[derive(IntoElement)] -struct UiFontSizeControl; - -impl EditableSettingControl for UiFontSizeControl { - type Value = Pixels; - type Settings = ThemeSettings; - - fn name(&self) -> SharedString { - "UI Font Size".into() - } - - fn read(cx: &AppContext) -> Self::Value { - let settings = ThemeSettings::get_global(cx); - settings.ui_font_size - } - - fn apply(settings: &mut ::FileContent, value: Self::Value) { - settings.ui_font_size = Some(value.into()); - } -} - -impl RenderOnce for UiFontSizeControl { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - let value = Self::read(cx); - - h_flex() - .gap_2() - .child(Icon::new(IconName::FontSize)) - .child(NumericStepper::new( - value.to_string(), - move |_, cx| { - Self::write(value - px(1.), cx); - }, - move |_, cx| { - Self::write(value + px(1.), cx); - }, - )) - } -} - -#[derive(IntoElement)] -struct UiFontWeightControl; - -impl EditableSettingControl for UiFontWeightControl { - type Value = FontWeight; - type Settings = ThemeSettings; - - fn name(&self) -> SharedString { - "UI Font Weight".into() - } - - fn read(cx: &AppContext) -> Self::Value { - let settings = ThemeSettings::get_global(cx); - settings.ui_font.weight - } - - fn apply(settings: &mut ::FileContent, value: Self::Value) { - settings.ui_font_weight = Some(value.0); - } -} - -impl RenderOnce for UiFontWeightControl { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - let value = Self::read(cx); - - h_flex() - .gap_2() - .child(Icon::new(IconName::FontWeight)) - .child(DropdownMenu::new( - "ui-font-weight", - value.0.to_string(), - ContextMenu::build(cx, |mut menu, _cx| { - for weight in FontWeight::ALL { - menu = menu.custom_entry( - move |_cx| Label::new(weight.0.to_string()).into_any_element(), - { - move |cx| { - Self::write(weight, cx); - } - }, - ) - } - - menu - }), - )) - } -} diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index bd590ba533..7c2a9619c3 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -94,6 +94,17 @@ pub struct ThemeSettings { } impl ThemeSettings { + const DEFAULT_LIGHT_THEME: &'static str = "One Light"; + const DEFAULT_DARK_THEME: &'static str = "One Dark"; + + /// Returns the name of the default theme for the given [`Appearance`]. + pub fn default_theme(appearance: Appearance) -> &'static str { + match appearance { + Appearance::Light => Self::DEFAULT_LIGHT_THEME, + Appearance::Dark => Self::DEFAULT_DARK_THEME, + } + } + /// Reloads the current theme. /// /// Reads the [`ThemeSettings`] to know which theme should be loaded, @@ -109,10 +120,7 @@ impl ThemeSettings { // based on the system appearance. let theme_registry = ThemeRegistry::global(cx); if theme_registry.get(theme_name).ok().is_none() { - theme_name = match *system_appearance { - Appearance::Light => "One Light", - Appearance::Dark => "One Dark", - }; + theme_name = Self::default_theme(*system_appearance); }; if let Some(_theme) = theme_settings.switch_theme(theme_name, cx) { @@ -190,7 +198,7 @@ fn theme_name_ref(_: &mut SchemaGenerator) -> Schema { Schema::new_ref("#/definitions/ThemeName".into()) } -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ThemeMode { /// Use the specified `light` theme. @@ -218,6 +226,13 @@ impl ThemeSelection { }, } } + + pub fn mode(&self) -> Option { + match self { + ThemeSelection::Static(_) => None, + ThemeSelection::Dynamic { mode, .. } => Some(*mode), + } + } } /// Settings for rendering text in UI and text buffers. @@ -267,6 +282,56 @@ pub struct ThemeSettingsContent { pub theme_overrides: Option, } +impl ThemeSettingsContent { + /// Sets the theme for the given appearance to the theme with the specified name. + pub fn set_theme(&mut self, theme_name: String, appearance: Appearance) { + if let Some(selection) = self.theme.as_mut() { + let theme_to_update = match selection { + ThemeSelection::Static(theme) => theme, + ThemeSelection::Dynamic { mode, light, dark } => match mode { + ThemeMode::Light => light, + ThemeMode::Dark => dark, + ThemeMode::System => match appearance { + Appearance::Light => light, + Appearance::Dark => dark, + }, + }, + }; + + *theme_to_update = theme_name.to_string(); + } else { + self.theme = Some(ThemeSelection::Static(theme_name.to_string())); + } + } + + pub fn set_mode(&mut self, mode: ThemeMode) { + if let Some(selection) = self.theme.as_mut() { + match selection { + ThemeSelection::Static(theme) => { + // If the theme was previously set to a single static theme, + // we don't know whether it was a light or dark theme, so we + // just use it for both. + self.theme = Some(ThemeSelection::Dynamic { + mode, + light: theme.clone(), + dark: theme.clone(), + }); + } + ThemeSelection::Dynamic { + mode: mode_to_update, + .. + } => *mode_to_update = mode, + } + } else { + self.theme = Some(ThemeSelection::Dynamic { + mode, + light: ThemeSettings::DEFAULT_LIGHT_THEME.into(), + dark: ThemeSettings::DEFAULT_DARK_THEME.into(), + }); + } + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] #[serde(rename_all = "snake_case")] pub enum BufferLineHeight { diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 80a3769539..2ce2b9ba89 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -10,9 +10,7 @@ use picker::{Picker, PickerDelegate}; use serde::Deserialize; use settings::{update_settings_file, SettingsStore}; use std::sync::Arc; -use theme::{ - Appearance, Theme, ThemeMeta, ThemeMode, ThemeRegistry, ThemeSelection, ThemeSettings, -}; +use theme::{Appearance, Theme, ThemeMeta, ThemeRegistry, ThemeSettings}; use ui::{prelude::*, v_flex, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{ui::HighlightedLabel, ModalView, Workspace}; @@ -197,23 +195,7 @@ impl PickerDelegate for ThemeSelectorDelegate { let appearance = Appearance::from(cx.appearance()); update_settings_file::(self.fs.clone(), cx, move |settings, _| { - if let Some(selection) = settings.theme.as_mut() { - let theme_to_update = match selection { - ThemeSelection::Static(theme) => theme, - ThemeSelection::Dynamic { mode, light, dark } => match mode { - ThemeMode::Light => light, - ThemeMode::Dark => dark, - ThemeMode::System => match appearance { - Appearance::Light => light, - Appearance::Dark => dark, - }, - }, - }; - - *theme_to_update = theme_name.to_string(); - } else { - settings.theme = Some(ThemeSelection::Static(theme_name.to_string())); - } + settings.set_theme(theme_name.to_string(), appearance); }); self.view diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index 09ad4fe9d1..a1e92865d3 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -38,6 +38,7 @@ impl RenderOnce for ApplicationMenu { )) }, ) + .reserve_space_for_reset(true) .when( theme::has_adjusted_buffer_font_size(cx), |stepper| { @@ -72,6 +73,7 @@ impl RenderOnce for ApplicationMenu { )) }, ) + .reserve_space_for_reset(true) .when( theme::has_adjusted_ui_font_size(cx), |stepper| { diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs index 0fff168bbf..37a0bcff2c 100644 --- a/crates/ui/src/components/dropdown_menu.rs +++ b/crates/ui/src/components/dropdown_menu.rs @@ -42,8 +42,9 @@ impl Disableable for DropdownMenu { impl RenderOnce for DropdownMenu { fn render(self, _cx: &mut WindowContext) -> impl IntoElement { PopoverMenu::new(self.id) + .full_width(self.full_width) .menu(move |_cx| Some(self.menu.clone())) - .trigger(DropdownMenuTrigger::new(self.label)) + .trigger(DropdownMenuTrigger::new(self.label).full_width(self.full_width)) } } @@ -68,6 +69,11 @@ impl DropdownMenuTrigger { on_click: None, } } + + pub fn full_width(mut self, full_width: bool) -> Self { + self.full_width = full_width; + self + } } impl Disableable for DropdownMenuTrigger { diff --git a/crates/ui/src/components/numeric_stepper.rs b/crates/ui/src/components/numeric_stepper.rs index 027ff89008..16946588ba 100644 --- a/crates/ui/src/components/numeric_stepper.rs +++ b/crates/ui/src/components/numeric_stepper.rs @@ -7,6 +7,8 @@ pub struct NumericStepper { value: SharedString, on_decrement: Box, on_increment: Box, + /// Whether to reserve space for the reset button. + reserve_space_for_reset: bool, on_reset: Option>, } @@ -20,10 +22,16 @@ impl NumericStepper { value: value.into(), on_decrement: Box::new(on_decrement), on_increment: Box::new(on_increment), + reserve_space_for_reset: false, on_reset: None, } } + pub fn reserve_space_for_reset(mut self, reserve_space_for_reset: bool) -> Self { + self.reserve_space_for_reset = reserve_space_for_reset; + self + } + pub fn on_reset( mut self, on_reset: impl Fn(&ClickEvent, &mut WindowContext) + 'static, @@ -48,13 +56,15 @@ impl RenderOnce for NumericStepper { .icon_size(icon_size) .on_click(on_reset), ) - } else { + } else if self.reserve_space_for_reset { element.child( h_flex() .size(icon_size.square(cx)) .flex_none() .into_any_element(), ) + } else { + element } }) .child( diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 89eb6f0058..49d801b4f0 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -1,10 +1,10 @@ use std::{cell::RefCell, rc::Rc}; use gpui::{ - anchored, deferred, div, point, prelude::FluentBuilder, px, AnchorCorner, AnyElement, Bounds, - DismissEvent, DispatchPhase, Element, ElementId, GlobalElementId, HitboxId, InteractiveElement, - IntoElement, LayoutId, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, View, - VisualContext, WindowContext, + anchored, deferred, div, point, prelude::FluentBuilder, px, size, AnchorCorner, AnyElement, + Bounds, DismissEvent, DispatchPhase, Element, ElementId, GlobalElementId, HitboxId, + InteractiveElement, IntoElement, LayoutId, Length, ManagedView, MouseDownEvent, ParentElement, + Pixels, Point, Style, View, VisualContext, WindowContext, }; use crate::prelude::*; @@ -74,6 +74,7 @@ pub struct PopoverMenu { attach: Option, offset: Option>, trigger_handle: Option>, + full_width: bool, } impl PopoverMenu { @@ -87,9 +88,15 @@ impl PopoverMenu { attach: None, offset: None, trigger_handle: None, + full_width: false, } } + pub fn full_width(mut self, full_width: bool) -> Self { + self.full_width = full_width; + self + } + pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> Option> + 'static) -> Self { self.menu_builder = Some(Rc::new(f)); self @@ -258,10 +265,13 @@ impl Element for PopoverMenu { .as_mut() .map(|child_element| child_element.request_layout(cx)); - let layout_id = cx.request_layout( - gpui::Style::default(), - menu_layout_id.into_iter().chain(child_layout_id), - ); + let mut style = Style::default(); + if self.full_width { + style.size = size(relative(1.).into(), Length::Auto); + } + + let layout_id = + cx.request_layout(style, menu_layout_id.into_iter().chain(child_layout_id)); ( (