Detailed changes
@@ -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<String> for UiDensity {
+ fn from(s: String) -> Self {
+ match s.as_str() {
+ "compact" => Self::Compact,
+ "default" => Self::Default,
+ "comfortable" => Self::Comfortable,
+ _ => Self::default(),
+ }
+ }
+}
+
+impl Into<String> 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<ThemeSelection>,
pub active_theme: Arc<Theme>,
pub theme_overrides: Option<ThemeStyleContent>,
+ pub ui_density: UiDensity,
}
impl ThemeSettings {
@@ -183,6 +243,12 @@ pub struct ThemeSettingsContent {
#[serde(default)]
pub theme: Option<ThemeSelection>,
+ /// UNSTABLE: Expect many elements to be broken.
+ ///
+ // Controls the density of the UI.
+ #[serde(rename = "unstable.ui_density", default)]
+ pub ui_density: Option<UiDensity>,
+
/// 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();
}
@@ -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)
@@ -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)
@@ -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())
@@ -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)
})
@@ -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| {
@@ -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),
@@ -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)
@@ -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};
@@ -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::*;
@@ -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())
+}
@@ -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<Self>) -> impl IntoElement {
+ fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> 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<Self>) -> impl IntoElement {
+ fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_flex()
- .gap_2()
+ .gap(Spacing::Large.rems(cx))
.children(self.right_items.iter().rev().map(|item| item.to_any()))
}
}
@@ -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()