diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index e5a6b6b2c7..5d7d486187 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 3ca6d28672..cc1766dc0c 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 eaff3a6e73..af6cd15417 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 6de32c0eab..e7872cfe03 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 a04a0a4a90..3b53dfbe9c 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 91cb80d068..2cf8fc2832 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 2c9eeefa49..b2ca5e0c67 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 ce8eca033e..58c1277ad9 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 1e1c28cc59..d51564e383 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 ad02ae9756..02b9c9b1a0 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 0000000000..40dc548c82 --- /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 f575feef7e..0b80126163 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 1377c5519b..0e2628aab4 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()