From 9e36a66fecae2cd6894a7f2ca21dbb82b95892b2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 8 Jul 2024 18:45:49 -0400 Subject: [PATCH] ui: Add `NumericStepper` component (#13954) This PR adds a `NumericStepper` component that can be used to display a numeric value along with controls to increment, decrement, and reset the value. The `ApplicationMenu` has been updated to use the `NumericStepper` for adjusting the buffer and UI font size. Here it is in action: https://github.com/zed-industries/zed/assets/1486634/03cffe67-1256-4283-aa3d-560fffa06dad Note: Due to the way we do font adjustments, once modified the reset button will be displayed until it is clicked (or the font size adjustment is otherwise reset). Simply returning to the original value will currently not hide the reset button. Release Notes: - N/A --- crates/theme/src/settings.rs | 8 + crates/title_bar/Cargo.toml | 1 - crates/title_bar/src/application_menu.rs | 157 ++++++------------ crates/ui/src/components.rs | 2 + .../ui/src/components/button/icon_button.rs | 14 +- crates/ui/src/components/icon.rs | 13 ++ crates/ui/src/components/numeric_stepper.rs | 81 +++++++++ crates/ui/src/components/title_bar.rs | 5 - 8 files changed, 159 insertions(+), 122 deletions(-) create mode 100644 crates/ui/src/components/numeric_stepper.rs delete mode 100644 crates/ui/src/components/title_bar.rs diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index d1b329dfb49faf1a1acae030e93a0b9eebea4a6f..bd590ba5332867d14007197c648ab3606dbf9002 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -381,6 +381,10 @@ pub fn adjust_buffer_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) { cx.refresh(); } +pub fn has_adjusted_buffer_font_size(cx: &mut AppContext) -> bool { + cx.has_global::() +} + pub fn reset_buffer_font_size(cx: &mut AppContext) { if cx.has_global::() { cx.remove_global::(); @@ -417,6 +421,10 @@ pub fn adjust_ui_font_size(cx: &mut WindowContext, f: fn(&mut Pixels)) { cx.refresh(); } +pub fn has_adjusted_ui_font_size(cx: &mut AppContext) -> bool { + cx.has_global::() +} + pub fn reset_ui_font_size(cx: &mut WindowContext) { if cx.has_global::() { cx.remove_global::(); diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 5e5bd754e5fdeeffc7016e151d99a107f149aa4b..919d738bd2b9f662ada846bc43c9c1a2f81f172f 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -42,7 +42,6 @@ project.workspace = true recent_projects.workspace = true rpc.workspace = true serde.workspace = true -settings.workspace = true smallvec.workspace = true story = { workspace = true, optional = true } theme.workspace = true diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index a55b0d600b2aa6e54ef57508f756753d6aa53481..44d8f8b53cacd73ce291eb1b6a2b9e16d73c6509 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -1,6 +1,4 @@ -use settings::Settings; -use theme::ThemeSettings; -use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip}; +use ui::{prelude::*, ContextMenu, NumericStepper, PopoverMenu, Tooltip}; #[derive(IntoElement)] pub struct ApplicationMenu; @@ -12,128 +10,77 @@ impl ApplicationMenu { } impl RenderOnce for ApplicationMenu { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; - let font = cx.text_style().font(); - let font_id = cx.text_system().resolve_font(&font); - let width = cx - .text_system() - .typographic_bounds(font_id, ui_font_size, 'm') - .unwrap() - .size - .width - * 3.0; - + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { PopoverMenu::new("application-menu") .menu(move |cx| { - let width = width; ContextMenu::build(cx, move |menu, _cx| { - let width = width; menu.header("Workspace") .action("Open Command Palette", Box::new(command_palette::Toggle)) .custom_row(move |cx| { - div() + h_flex() + .gap_2() .w_full() - .flex() - .flex_row() .justify_between() .cursor(gpui::CursorStyle::Arrow) .child(Label::new("Buffer Font Size")) .child( - div() - .flex() - .flex_row() - .child(div().w(px(16.0))) - .child( - IconButton::new( - "reset-buffer-zoom", - IconName::RotateCcw, - ) - .on_click( - |_, cx| { - cx.dispatch_action(Box::new( - zed_actions::ResetBufferFontSize, - )) - }, - ), - ) - .child( - IconButton::new("--buffer-zoom", IconName::Dash) - .on_click(|_, cx| { - cx.dispatch_action(Box::new( - zed_actions::DecreaseBufferFontSize, - )) - }), - ) - .child( - div() - .w(width) - .flex() - .flex_row() - .justify_around() - .child(Label::new( - theme::get_buffer_font_size(cx).to_string(), - )), - ) - .child( - IconButton::new("+-buffer-zoom", IconName::Plus) - .on_click(|_, cx| { - cx.dispatch_action(Box::new( - zed_actions::IncreaseBufferFontSize, - )) - }), - ), + NumericStepper::new( + theme::get_buffer_font_size(cx).to_string(), + |_, cx| { + cx.dispatch_action(Box::new( + zed_actions::DecreaseBufferFontSize, + )) + }, + |_, cx| { + cx.dispatch_action(Box::new( + zed_actions::IncreaseBufferFontSize, + )) + }, + ) + .when( + theme::has_adjusted_buffer_font_size(cx), + |stepper| { + stepper.on_reset(|_, cx| { + cx.dispatch_action(Box::new( + zed_actions::ResetBufferFontSize, + )) + }) + }, + ), ) .into_any_element() }) .custom_row(move |cx| { - div() + h_flex() + .gap_2() .w_full() - .flex() - .flex_row() .justify_between() .cursor(gpui::CursorStyle::Arrow) .child(Label::new("UI Font Size")) .child( - div() - .flex() - .flex_row() - .child( - IconButton::new("reset-ui-zoom", IconName::RotateCcw) - .on_click(|_, cx| { - cx.dispatch_action(Box::new( - zed_actions::ResetUiFontSize, - )) - }), - ) - .child( - IconButton::new("--ui-zoom", IconName::Dash).on_click( - |_, cx| { - cx.dispatch_action(Box::new( - zed_actions::DecreaseUiFontSize, - )) - }, - ), - ) - .child( - div() - .w(width) - .flex() - .flex_row() - .justify_around() - .child(Label::new( - theme::get_ui_font_size(cx).to_string(), - )), - ) - .child( - IconButton::new("+-ui-zoom", IconName::Plus).on_click( - |_, cx| { - cx.dispatch_action(Box::new( - zed_actions::IncreaseUiFontSize, - )) - }, - ), - ), + NumericStepper::new( + theme::get_ui_font_size(cx).to_string(), + |_, cx| { + cx.dispatch_action(Box::new( + zed_actions::DecreaseUiFontSize, + )) + }, + |_, cx| { + cx.dispatch_action(Box::new( + zed_actions::IncreaseUiFontSize, + )) + }, + ) + .when( + theme::has_adjusted_ui_font_size(cx), + |stepper| { + stepper.on_reset(|_, cx| { + cx.dispatch_action(Box::new( + zed_actions::ResetUiFontSize, + )) + }) + }, + ), ) .into_any_element() }) diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 1ceaf4348728e9d2ebba1c072d6e970ac934ab83..d987a99d1c03481941d61029b29b1f0e59651a1e 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -12,6 +12,7 @@ mod keybinding; mod label; mod list; mod modal; +mod numeric_stepper; mod popover; mod popover_menu; mod radio; @@ -40,6 +41,7 @@ pub use keybinding::*; pub use label::*; pub use list::*; pub use modal::*; +pub use numeric_stepper::*; pub use popover::*; pub use popover_menu::*; pub use radio::*; diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index cb88793240d5c1197d90d42ae8e65ab4c2afc948..03a5c3a22da641d16287414ada62834df79a9294 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -1,6 +1,6 @@ use gpui::{AnyView, DefiniteLength}; -use crate::{prelude::*, ElevationIndex, SelectableButton, Spacing}; +use crate::{prelude::*, ElevationIndex, SelectableButton}; use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize}; use super::button_icon::ButtonIcon; @@ -147,16 +147,8 @@ impl RenderOnce for IconButton { self.base .map(|this| match self.shape { IconButtonShape::Square => { - let icon_size = self.icon_size.rems() * cx.rem_size(); - let padding = match self.icon_size { - IconSize::Indicator => Spacing::None.px(cx), - IconSize::XSmall => Spacing::XSmall.px(cx), - IconSize::Small => Spacing::XSmall.px(cx), - IconSize::Medium => Spacing::XSmall.px(cx), - }; - - this.width((icon_size + padding * 2.).into()) - .height((icon_size + padding * 2.).into()) + let size = self.icon_size.square(cx); + this.width(size.into()).height(size.into()) } IconButtonShape::Wide => this, }) diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index c9135c288329040f3dd3cb3f2dbef39a5a6525b7..332e30a14d9366588f6bcdf12e577272c92cd580 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -75,6 +75,19 @@ impl IconSize { IconSize::Medium => rems_from_px(16.), } } + + /// Returns the length of a side of the square that contains this [`IconSize`], with padding. + pub(crate) fn square(&self, cx: &mut WindowContext) -> Pixels { + let icon_size = self.rems() * cx.rem_size(); + let padding = match self { + IconSize::Indicator => Spacing::None.px(cx), + IconSize::XSmall => Spacing::XSmall.px(cx), + IconSize::Small => Spacing::XSmall.px(cx), + IconSize::Medium => Spacing::XSmall.px(cx), + }; + + icon_size + padding * 2. + } } #[derive(Debug, PartialEq, Copy, Clone, EnumIter, Serialize, Deserialize)] diff --git a/crates/ui/src/components/numeric_stepper.rs b/crates/ui/src/components/numeric_stepper.rs new file mode 100644 index 0000000000000000000000000000000000000000..027ff89008909519762ea5917e0f93e62f3c7a44 --- /dev/null +++ b/crates/ui/src/components/numeric_stepper.rs @@ -0,0 +1,81 @@ +use gpui::ClickEvent; + +use crate::{prelude::*, IconButtonShape}; + +#[derive(IntoElement)] +pub struct NumericStepper { + value: SharedString, + on_decrement: Box, + on_increment: Box, + on_reset: Option>, +} + +impl NumericStepper { + pub fn new( + value: impl Into, + on_decrement: impl Fn(&ClickEvent, &mut WindowContext) + 'static, + on_increment: impl Fn(&ClickEvent, &mut WindowContext) + 'static, + ) -> Self { + Self { + value: value.into(), + on_decrement: Box::new(on_decrement), + on_increment: Box::new(on_increment), + on_reset: None, + } + } + + pub fn on_reset( + mut self, + on_reset: impl Fn(&ClickEvent, &mut WindowContext) + 'static, + ) -> Self { + self.on_reset = Some(Box::new(on_reset)); + self + } +} + +impl RenderOnce for NumericStepper { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let shape = IconButtonShape::Square; + let icon_size = IconSize::Small; + + h_flex() + .gap_1() + .map(|element| { + if let Some(on_reset) = self.on_reset { + element.child( + IconButton::new("reset", IconName::RotateCcw) + .shape(shape) + .icon_size(icon_size) + .on_click(on_reset), + ) + } else { + element.child( + h_flex() + .size(icon_size.square(cx)) + .flex_none() + .into_any_element(), + ) + } + }) + .child( + h_flex() + .gap_1() + .px_1() + .rounded_sm() + .bg(cx.theme().colors().editor_background) + .child( + IconButton::new("decrement", IconName::Dash) + .shape(shape) + .icon_size(icon_size) + .on_click(self.on_decrement), + ) + .child(Label::new(self.value)) + .child( + IconButton::new("increment", IconName::Plus) + .shape(shape) + .icon_size(icon_size) + .on_click(self.on_increment), + ), + ) + } +} diff --git a/crates/ui/src/components/title_bar.rs b/crates/ui/src/components/title_bar.rs deleted file mode 100644 index 28aac32c73f99bd9b6ba943273299f85734b0e71..0000000000000000000000000000000000000000 --- a/crates/ui/src/components/title_bar.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod linux_window_controls; -mod title_bar; -mod windows_window_controls; - -pub use title_bar::*;