use gpui::Rems;
const BORDER_REMS: f32 = 1.0 / 16.0;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ControlSize {
Small,
#[default]
Medium,
Large,
}
impl ControlSize {
pub const ALL: [ControlSize; 3] = [ControlSize::Small, ControlSize::Medium, ControlSize::Large];
pub fn name(&self) -> &'static str {
match self {
ControlSize::Small => "Small",
ControlSize::Medium => "Medium",
ControlSize::Large => "Large",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ControlMetrics {
pub height: Rems,
pub padding_x: Rems,
pub gap: Rems,
pub radius: Rems,
pub text_size: Rems,
pub line_height: Rems,
pub ink: Rems,
}
impl ControlMetrics {
pub fn padding_y(&self) -> Rems {
Rems(((self.height.0 - self.line_height.0) / 2.0 - BORDER_REMS).max(0.0))
}
pub fn inner_radius(&self) -> Rems {
Rems((self.radius.0 - BORDER_REMS).max(0.0))
}
pub fn multiline_line_height(&self) -> Rems {
self.text_size * 1.5
}
pub fn track(&self) -> TrackMetrics {
let margin = Rems(BORDER_REMS);
let inset = (margin + Rems(BORDER_REMS)) * 2.0;
TrackMetrics {
width: self.ink * 2.0 - Rems(0.25),
height: self.ink,
thumb: self.ink - inset,
thumb_margin: margin,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TrackMetrics {
pub width: Rems,
pub height: Rems,
pub thumb: Rems,
pub thumb_margin: Rems,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ControlScale {
pub small: ControlMetrics,
pub medium: ControlMetrics,
pub large: ControlMetrics,
}
impl ControlScale {
pub fn metrics(&self, size: ControlSize) -> ControlMetrics {
match size {
ControlSize::Small => self.small,
ControlSize::Medium => self.medium,
ControlSize::Large => self.large,
}
}
}
impl Default for ControlScale {
fn default() -> Self {
Self {
small: ControlMetrics {
height: Rems(1.0), padding_x: Rems(0.375), gap: Rems(0.1875), radius: Rems(0.1875), text_size: Rems(0.6875), line_height: Rems(0.875), ink: Rems(0.75), },
medium: ControlMetrics {
height: Rems(1.25), padding_x: Rems(0.5), gap: Rems(0.25), radius: Rems(0.25), text_size: Rems(0.75), line_height: Rems(1.0), ink: Rems(0.875), },
large: ControlMetrics {
height: Rems(1.5), padding_x: Rems(0.625), gap: Rems(0.375), radius: Rems(0.3125), text_size: Rems(0.8125), line_height: Rems(1.125), ink: Rems(1.0), },
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn px(value: Rems) -> f32 {
value.0 * 16.0
}
#[test]
fn the_rungs_are_the_documented_pixel_sizes() {
let scale = ControlScale::default();
assert_eq!(px(scale.small.height), 16.0);
assert_eq!(px(scale.medium.height), 20.0);
assert_eq!(px(scale.large.height), 24.0);
}
#[test]
fn every_dimension_grows_with_the_rung() {
let scale = ControlScale::default();
for (smaller, larger) in [(scale.small, scale.medium), (scale.medium, scale.large)] {
assert!(smaller.height.0 < larger.height.0, "height");
assert!(smaller.padding_x.0 < larger.padding_x.0, "padding_x");
assert!(smaller.gap.0 < larger.gap.0, "gap");
assert!(smaller.radius.0 < larger.radius.0, "radius");
assert!(smaller.text_size.0 < larger.text_size.0, "text_size");
assert!(smaller.line_height.0 < larger.line_height.0, "line_height");
assert!(smaller.ink.0 < larger.ink.0, "ink");
}
}
#[test]
fn the_line_box_fits_inside_the_rung() {
let scale = ControlScale::default();
for size in ControlSize::ALL {
let metrics = scale.metrics(size);
assert!(
px(metrics.line_height) + 2.0 <= px(metrics.height),
"{}: a {}px line box plus 2px of border does not fit in {}px",
size.name(),
px(metrics.line_height),
px(metrics.height),
);
}
}
#[test]
fn ink_is_smaller_than_the_box_it_fills() {
let scale = ControlScale::default();
for size in ControlSize::ALL {
let metrics = scale.metrics(size);
assert!(
metrics.ink.0 < metrics.height.0,
"{}: ink {}px is not smaller than the {}px box",
size.name(),
px(metrics.ink),
px(metrics.height),
);
}
}
#[test]
fn the_thumb_fits_inside_its_track() {
let scale = ControlScale::default();
for size in ControlSize::ALL {
let track = scale.metrics(size).track();
let occupied = px(track.thumb) + 2.0 * (px(track.thumb_margin) + 1.0);
assert!(
occupied <= px(track.height),
"{}: a {}px thumb inset {}px inside a 1px border does not fit a \
{}px track",
size.name(),
px(track.thumb),
px(track.thumb_margin),
px(track.height),
);
assert!(
track.width.0 > track.height.0,
"{}: a track that is not wider than it is tall has nowhere to \
slide the thumb",
size.name(),
);
}
}
#[test]
fn multiline_text_gets_a_looser_line_box() {
let scale = ControlScale::default();
for size in ControlSize::ALL {
let metrics = scale.metrics(size);
assert!(
metrics.multiline_line_height().0 > metrics.text_size.0,
"{}: wrapped lines would collide",
size.name(),
);
}
}
#[test]
fn medium_is_the_default_rung() {
assert_eq!(ControlSize::default(), ControlSize::Medium);
assert_eq!(ControlSize::ALL.len(), 3);
}
}