From 97512be378a0246b40279881ec51d3fbdff76a1c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 1 May 2024 14:28:52 -0400 Subject: [PATCH] Add wiring for UI density (#11260) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note: You shouldn't use the `unstable.ui_density` setting – it is only being added for testing and to enable new UI components to be built with density in mind. Don't expect this to work well, or at all right now. Adds some of the basic wiring we'll need to start scaling UI elements throughout the app based on a desired density setting. Release Notes: - N/A --- crates/theme/src/settings.rs | 71 +++++++++++++++ crates/ui/src/components/button/button.rs | 8 +- .../ui/src/components/button/button_like.rs | 8 +- .../ui/src/components/button/icon_button.rs | 10 +-- crates/ui/src/components/checkbox/checkbox.rs | 40 +-------- .../checkbox/checkbox_with_label.rs | 4 +- crates/ui/src/components/tab.rs | 18 ++-- crates/ui/src/components/tab_bar.rs | 14 +-- crates/ui/src/prelude.rs | 1 + crates/ui/src/styles.rs | 2 + crates/ui/src/styles/spacing.rs | 87 +++++++++++++++++++ crates/workspace/src/status_bar.rs | 16 ++-- crates/workspace/src/toolbar.rs | 8 +- 13 files changed, 209 insertions(+), 78 deletions(-) create mode 100644 crates/ui/src/styles/spacing.rs diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index e5a6b6b2c7ae252697d7eac06d8f49b39c02172e..5d7d48618778243bb77a03f1f49283c9e432e783 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -21,6 +21,65 @@ use util::ResultExt as _; const MIN_FONT_SIZE: Pixels = px(6.0); const MIN_LINE_HEIGHT: f32 = 1.0; +#[derive( + Debug, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Clone, + Copy, + Serialize, + Deserialize, + JsonSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum UiDensity { + /// A denser UI with tighter spacing and smaller elements. + #[serde(alias = "compact")] + Compact, + #[default] + #[serde(alias = "default")] + /// The default UI density. + Default, + #[serde(alias = "comfortable")] + /// A looser UI with more spacing and larger elements. + Comfortable, +} + +impl UiDensity { + pub fn spacing_ratio(self) -> f32 { + match self { + UiDensity::Compact => 0.75, + UiDensity::Default => 1.0, + UiDensity::Comfortable => 1.25, + } + } +} + +impl From for UiDensity { + fn from(s: String) -> Self { + match s.as_str() { + "compact" => Self::Compact, + "default" => Self::Default, + "comfortable" => Self::Comfortable, + _ => Self::default(), + } + } +} + +impl Into for UiDensity { + fn into(self) -> String { + match self { + UiDensity::Compact => "compact".to_string(), + UiDensity::Default => "default".to_string(), + UiDensity::Comfortable => "comfortable".to_string(), + } + } +} + #[derive(Clone)] pub struct ThemeSettings { pub ui_font_size: Pixels, @@ -31,6 +90,7 @@ pub struct ThemeSettings { pub theme_selection: Option, pub active_theme: Arc, pub theme_overrides: Option, + pub ui_density: UiDensity, } impl ThemeSettings { @@ -183,6 +243,12 @@ pub struct ThemeSettingsContent { #[serde(default)] pub theme: Option, + /// UNSTABLE: Expect many elements to be broken. + /// + // Controls the density of the UI. + #[serde(rename = "unstable.ui_density", default)] + pub ui_density: Option, + /// EXPERIMENTAL: Overrides for the current theme. /// /// These values will override the ones on the current theme specified in `theme`. @@ -343,9 +409,14 @@ impl settings::Settings for ThemeSettings { .or(themes.get(&one_dark().name)) .unwrap(), theme_overrides: None, + ui_density: defaults.ui_density.unwrap_or(UiDensity::Default), }; for value in sources.user.into_iter().chain(sources.release_channel) { + if let Some(value) = value.ui_density { + this.ui_density = value; + } + if let Some(value) = value.buffer_font_family.clone() { this.buffer_font.family = value.into(); } diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index 3ca6d2867222200e44916c3b4adf86e8ecbb9022..cc1766dc0cdee5accbb49350f23abb2a48ed7a37 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -1,6 +1,6 @@ use gpui::{AnyView, DefiniteLength}; -use crate::{prelude::*, IconPosition, KeyBinding}; +use crate::{prelude::*, IconPosition, KeyBinding, Spacing}; use crate::{ ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label, LineHeightStyle, }; @@ -344,7 +344,7 @@ impl ButtonCommon for Button { impl RenderOnce for Button { #[allow(refining_impl_trait)] - fn render(self, _cx: &mut WindowContext) -> ButtonLike { + fn render(self, cx: &mut WindowContext) -> ButtonLike { let is_disabled = self.base.disabled; let is_selected = self.base.selected; @@ -363,7 +363,7 @@ impl RenderOnce for Button { self.base.child( h_flex() - .gap_1() + .gap(Spacing::Small.rems(cx)) .when(self.icon_position == Some(IconPosition::Start), |this| { this.children(self.icon.map(|icon| { ButtonIcon::new(icon) @@ -376,7 +376,7 @@ impl RenderOnce for Button { }) .child( h_flex() - .gap_2() + .gap(Spacing::Medium.rems(cx)) .justify_between() .child( Label::new(label) diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index eaff3a6e7355204de47410cff447d727cb206676..af6cd1541769fb2675c3d80d55b19bf82f80e192 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -2,7 +2,7 @@ use gpui::{relative, DefiniteLength, MouseButton}; use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems}; use smallvec::SmallVec; -use crate::prelude::*; +use crate::{prelude::*, Spacing}; /// A trait for buttons that can be Selected. Enables setting the [`ButtonStyle`] of a button when it is selected. pub trait SelectableButton: Selectable { @@ -431,10 +431,10 @@ impl RenderOnce for ButtonLike { ButtonLikeRounding::Left => this.rounded_l_md(), ButtonLikeRounding::Right => this.rounded_r_md(), }) - .gap_1() + .gap(Spacing::Small.rems(cx)) .map(|this| match self.size { - ButtonSize::Large => this.px_2(), - ButtonSize::Default | ButtonSize::Compact => this.px_1(), + ButtonSize::Large => this.px(Spacing::Medium.rems(cx)), + ButtonSize::Default | ButtonSize::Compact => this.px(Spacing::Small.rems(cx)), ButtonSize::None => this, }) .bg(style.enabled(cx).background) diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index 6de32c0eab29a22da541e2617d1adbe60f3656b2..e7872cfe03fa60e5e2875f482b34775bfcd7d190 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::*, SelectableButton}; +use crate::{prelude::*, SelectableButton, Spacing}; use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize}; use super::button_icon::ButtonIcon; @@ -139,10 +139,10 @@ impl RenderOnce for IconButton { IconButtonShape::Square => { let icon_size = self.icon_size.rems() * cx.rem_size(); let padding = match self.icon_size { - IconSize::Indicator => px(0.), - IconSize::XSmall => px(0.), - IconSize::Small => px(2.), - IconSize::Medium => px(2.), + IconSize::Indicator => Spacing::None.px(cx), + IconSize::XSmall => Spacing::None.px(cx), + IconSize::Small => Spacing::XSmall.px(cx), + IconSize::Medium => Spacing::XSmall.px(cx), }; this.width((icon_size + padding * 2.).into()) diff --git a/crates/ui/src/components/checkbox/checkbox.rs b/crates/ui/src/components/checkbox/checkbox.rs index a04a0a4a909b54f6c073cdcbb0781d2b2c167c05..3b53dfbe9c80821a1887e997ab65c1c1740a3772 100644 --- a/crates/ui/src/components/checkbox/checkbox.rs +++ b/crates/ui/src/components/checkbox/checkbox.rs @@ -61,28 +61,9 @@ impl RenderOnce for Checkbox { Selection::Unselected => None, }; - // A checkbox could be in an indeterminate state, - // for example the indeterminate state could represent: - // - a group of options of which only some are selected - // - an enabled option that is no longer available - // - a previously agreed to license that has been updated - // - // For the sake of styles we treat the indeterminate state as selected, - // but its icon will be different. let selected = self.checked == Selection::Selected || self.checked == Selection::Indeterminate; - // We could use something like this to make the checkbox background when selected: - // - // ```rs - // ... - // .when(selected, |this| { - // this.bg(cx.theme().colors().element_selected) - // }) - // ``` - // - // But we use a match instead here because the checkbox might be disabled, - // and it could be disabled _while_ it is selected, as well as while it is not selected. let (bg_color, border_color) = match (self.disabled, selected) { (true, _) => ( cx.theme().colors().ghost_element_disabled, @@ -102,36 +83,21 @@ impl RenderOnce for Checkbox { .id(self.id) .justify_center() .items_center() - // Rather than adding `px_1()` to add some space around the checkbox, - // we use a larger parent element to create a slightly larger - // click area for the checkbox. - .size_5() - // Because we've enlarged the click area, we need to create a - // `group` to pass down interactivity events to the checkbox. + .size(crate::styles::custom_spacing(cx, 20.)) .group(group_id.clone()) .child( div() .flex() - // This prevent the flex element from growing - // or shrinking in response to any size changes .flex_none() - // The combo of `justify_center()` and `items_center()` - // is used frequently to center elements in a flex container. - // - // We use this to center the icon in the checkbox. .justify_center() .items_center() - .m_1() - .size_4() + .m(Spacing::Small.px(cx)) + .size(crate::styles::custom_spacing(cx, 16.)) .rounded_sm() .bg(bg_color) .border() .border_color(border_color) - // We only want the interactivity states to fire when we - // are in a checkbox that isn't disabled. .when(!self.disabled, |this| { - // Here instead of `hover()` we use `group_hover()` - // to pass it the group id. this.group_hover(group_id.clone(), |el| { el.bg(cx.theme().colors().element_hover) }) diff --git a/crates/ui/src/components/checkbox/checkbox_with_label.rs b/crates/ui/src/components/checkbox/checkbox_with_label.rs index 91cb80d068e5f48185c8f2c4a5c6beb8d3b49545..2cf8fc28323b30e18c7a3c8b684979d8c94fbcc4 100644 --- a/crates/ui/src/components/checkbox/checkbox_with_label.rs +++ b/crates/ui/src/components/checkbox/checkbox_with_label.rs @@ -28,9 +28,9 @@ impl CheckboxWithLabel { } impl RenderOnce for CheckboxWithLabel { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { h_flex() - .gap_2() + .gap(Spacing::Large.rems(cx)) .child(Checkbox::new(self.id.clone(), self.checked).on_click({ let on_click = self.on_click.clone(); move |checked, cx| { diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index 2c9eeefa490f38877e4349701d1aa8d1c851e2c4..b2ca5e0c67688f71d1e8349c9b870f937e139de6 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -146,32 +146,30 @@ impl RenderOnce for Tab { .group("") .relative() .h(rems(Self::CONTENT_HEIGHT_IN_REMS)) - .px_5() - .gap_1() + .px(crate::custom_spacing(cx, 20.)) + .gap(Spacing::Small.rems(cx)) .text_color(text_color) // .hover(|style| style.bg(tab_hover_bg)) // .active(|style| style.bg(tab_active_bg)) .child( h_flex() - .w_3() - .h_3() + .size_3() .justify_center() .absolute() .map(|this| match self.close_side { - TabCloseSide::Start => this.right_1(), - TabCloseSide::End => this.left_1(), + TabCloseSide::Start => this.right(Spacing::Small.rems(cx)), + TabCloseSide::End => this.left(Spacing::Small.rems(cx)), }) .children(self.start_slot), ) .child( h_flex() - .w_3() - .h_3() + .size_3() .justify_center() .absolute() .map(|this| match self.close_side { - TabCloseSide::Start => this.left_1(), - TabCloseSide::End => this.right_1(), + TabCloseSide::Start => this.left(Spacing::Small.rems(cx)), + TabCloseSide::End => this.right(Spacing::Small.rems(cx)), }) .visible_on_hover("") .children(self.end_slot), diff --git a/crates/ui/src/components/tab_bar.rs b/crates/ui/src/components/tab_bar.rs index ce8eca033e144fff3145057e65aeb4ac8c9c2083..58c1277ad9587a22d3801462d608954da4492c0b 100644 --- a/crates/ui/src/components/tab_bar.rs +++ b/crates/ui/src/components/tab_bar.rs @@ -96,14 +96,18 @@ impl RenderOnce for TabBar { .flex() .flex_none() .w_full() - .h(rems_from_px(29.)) + .h( + // TODO: This should scale with [UiDensity], however tabs, + // and other tab bar tools need to scale dynamically first. + rems_from_px(29.), + ) .bg(cx.theme().colors().tab_bar_background) .when(!self.start_children.is_empty(), |this| { this.child( h_flex() .flex_none() - .gap_1() - .px_1() + .gap(Spacing::Small.rems(cx)) + .px(Spacing::Small.rems(cx)) .border_b() .border_r() .border_color(cx.theme().colors().border) @@ -140,8 +144,8 @@ impl RenderOnce for TabBar { this.child( h_flex() .flex_none() - .gap_1() - .px_1() + .gap(Spacing::Small.rems(cx)) + .px(Spacing::Medium.rems(cx)) .border_b() .border_l() .border_color(cx.theme().colors().border) diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 1e1c28cc59defba84eec41daf6691059954348ce..d51564e38341357cb63fd172bfcccdab67fa827e 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -13,6 +13,7 @@ pub use crate::fixed::*; pub use crate::selectable::*; pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle, StyledTypography}; pub use crate::visible_on_hover::*; +pub use crate::Spacing; pub use crate::{h_flex, v_flex}; pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton}; pub use crate::{ButtonCommon, Color, StyledExt}; diff --git a/crates/ui/src/styles.rs b/crates/ui/src/styles.rs index ad02ae9756f00fb335ff3566a0463a54b30b8141..02b9c9b1a0aa815a0143a5d0157f003e8942d716 100644 --- a/crates/ui/src/styles.rs +++ b/crates/ui/src/styles.rs @@ -1,11 +1,13 @@ mod color; mod elevation; mod platform; +mod spacing; mod typography; mod units; pub use color::*; pub use elevation::*; pub use platform::*; +pub use spacing::*; pub use typography::*; pub use units::*; diff --git a/crates/ui/src/styles/spacing.rs b/crates/ui/src/styles/spacing.rs new file mode 100644 index 0000000000000000000000000000000000000000..40dc548c821a3f513900804a07eddbb7db12f0e6 --- /dev/null +++ b/crates/ui/src/styles/spacing.rs @@ -0,0 +1,87 @@ +use gpui::*; +use settings::Settings; +use theme::{ThemeSettings, UiDensity}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Spacing { + /// No spacing + None, + /// Usually a one pixel spacing. Grows to 2px in comfortable density. + /// @16px/rem: `1px`|`1px`|`2px` + XXSmall, + /// Extra small spacing - @16px/rem: `1px`|`2px`|`4px` + /// + /// Relative to the user's `ui_font_size` and [UiDensity] setting. + XSmall, + /// Small spacing - @16px/rem: `2px`|`4px`|`6px` + /// + /// Relative to the user's `ui_font_size` and [UiDensity] setting. + Small, + /// Medium spacing - @16px/rem: `3px`|`6px`|`8px` + /// + /// Relative to the user's `ui_font_size` and [UiDensity] setting. + Medium, + /// Large spacing - @16px/rem: `4px`|`8px`|`10px` + /// + /// Relative to the user's `ui_font_size` and [UiDensity] setting. + Large, + XLarge, + XXLarge, +} + +impl Spacing { + pub fn spacing_ratio(self, cx: &WindowContext) -> f32 { + match ThemeSettings::get_global(cx).ui_density { + UiDensity::Compact => match self { + Spacing::None => 0.0, + Spacing::XXSmall => 1. / 16., + Spacing::XSmall => 1. / 16., + Spacing::Small => 2. / 16., + Spacing::Medium => 3. / 16., + Spacing::Large => 4. / 16., + Spacing::XLarge => 8. / 16., + Spacing::XXLarge => 12. / 16., + }, + UiDensity::Default => match self { + Spacing::None => 0.0, + Spacing::XXSmall => 1. / 16., + Spacing::XSmall => 2. / 16., + Spacing::Small => 4. / 16., + Spacing::Medium => 6. / 16., + Spacing::Large => 8. / 16., + Spacing::XLarge => 12. / 16., + #[allow(clippy::eq_op)] + Spacing::XXLarge => 16. / 16., + }, + UiDensity::Comfortable => match self { + Spacing::None => 0.0, + Spacing::XXSmall => 2. / 16., + Spacing::XSmall => 3. / 16., + Spacing::Small => 6. / 16., + Spacing::Medium => 8. / 16., + Spacing::Large => 10. / 16., + #[allow(clippy::eq_op)] + Spacing::XLarge => 16. / 16., + Spacing::XXLarge => 20. / 16., + }, + } + } + + pub fn rems(self, cx: &WindowContext) -> Rems { + rems(self.spacing_ratio(cx)) + } + + pub fn px(self, cx: &WindowContext) -> Pixels { + let ui_font_size_f32: f32 = ThemeSettings::get_global(cx).ui_font_size.into(); + + px(ui_font_size_f32 * self.spacing_ratio(cx)) + } +} + +pub fn user_spacing_style(cx: &WindowContext) -> UiDensity { + ThemeSettings::get_global(cx).ui_density +} + +pub fn custom_spacing(cx: &WindowContext, size: f32) -> Rems { + crate::rems_from_px(size * user_spacing_style(cx).spacing_ratio()) +} diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index f575feef7ed8046bd661c0f33d8d4e107a6dd058..0b80126163ee2e00157111229a2933b42cb03529 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -37,10 +37,10 @@ impl Render for StatusBar { h_flex() .w_full() .justify_between() - .gap_2() - .py_0p5() - .px_1() - .h_8() + .gap(Spacing::Large.rems(cx)) + .py(Spacing::Small.rems(cx)) + .px(Spacing::Large.rems(cx)) + // .h_8() .bg(cx.theme().colors().status_bar_background) .child(self.render_left_tools(cx)) .child(self.render_right_tools(cx)) @@ -48,16 +48,16 @@ impl Render for StatusBar { } impl StatusBar { - fn render_left_tools(&self, _: &mut ViewContext) -> impl IntoElement { + fn render_left_tools(&self, cx: &mut ViewContext) -> impl IntoElement { h_flex() - .gap_2() + .gap(Spacing::Large.rems(cx)) .overflow_x_hidden() .children(self.left_items.iter().map(|item| item.to_any())) } - fn render_right_tools(&self, _: &mut ViewContext) -> impl IntoElement { + fn render_right_tools(&self, cx: &mut ViewContext) -> impl IntoElement { h_flex() - .gap_2() + .gap(Spacing::Large.rems(cx)) .children(self.right_items.iter().rev().map(|item| item.to_any())) } } diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 1377c5519bcf07d28a0b60bc532fb1a5090cd5dd..0e2628aab49147387eb4429e2b3e4ae029deff73 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -104,15 +104,17 @@ impl Render for Toolbar { let has_right_items = self.right_items().count() > 0; v_flex() - .p_2() - .when(has_left_items || has_right_items, |this| this.gap_2()) + .p(Spacing::Large.rems(cx)) + .when(has_left_items || has_right_items, |this| { + this.gap(Spacing::Large.rems(cx)) + }) .border_b() .border_color(cx.theme().colors().border_variant) .bg(cx.theme().colors().toolbar_background) .child( h_flex() .justify_between() - .gap_2() + .gap(Spacing::Large.rems(cx)) .when(has_left_items, |this| { this.child( h_flex()