Add wiring for UI density (#11260)

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
This commit is contained in:
Nate Butler 2024-05-01 14:28:52 -04:00 committed by GitHub
parent 0fce20d8da
commit 97512be378
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 209 additions and 78 deletions

View file

@ -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();
}

View file

@ -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)

View file

@ -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)

View file

@ -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())

View file

@ -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)
})

View file

@ -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| {

View file

@ -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),

View file

@ -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)

View file

@ -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};

View file

@ -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::*;

View file

@ -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())
}

View file

@ -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()))
}
}

View file

@ -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()