add sizable trait
This commit is contained in:
+83
-1
@@ -47,6 +47,9 @@ pub struct Theme {
|
||||
pub colors: ColorTokens,
|
||||
pub ramps: Ramps,
|
||||
pub radius: Radius,
|
||||
/// The platform's native root font size; rem-based sizing scales with it.
|
||||
pub root_font_size: Pixels,
|
||||
pub sizes: SizeTable,
|
||||
pub accessibility: AccessibilityPreferences,
|
||||
}
|
||||
|
||||
@@ -72,6 +75,78 @@ impl Default for Radius {
|
||||
}
|
||||
}
|
||||
|
||||
/// A named control size class.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Size {
|
||||
Small,
|
||||
/// The default.
|
||||
#[default]
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
/// The control metrics for one size class.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ControlMetrics {
|
||||
/// Push buttons, toggles, and field rows.
|
||||
pub control_height: Pixels,
|
||||
/// Text fields; slightly taller than buttons, as native fields are.
|
||||
pub input_height: Pixels,
|
||||
/// Horizontal padding inside controls.
|
||||
pub padding_x: Pixels,
|
||||
/// Label text size.
|
||||
pub text_size: Pixels,
|
||||
/// Icons and on/off indicators.
|
||||
pub icon_size: Pixels,
|
||||
/// Avatars.
|
||||
pub avatar_size: Pixels,
|
||||
}
|
||||
|
||||
/// The control metrics for every size class, derived from the theme's root
|
||||
/// font size so controls scale with the platform's native type.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct SizeTable {
|
||||
pub small: ControlMetrics,
|
||||
pub medium: ControlMetrics,
|
||||
pub large: ControlMetrics,
|
||||
}
|
||||
|
||||
impl SizeTable {
|
||||
pub fn with_root_font_size(rem_size: Pixels) -> Self {
|
||||
let class = |height: f32, padding: f32, text: f32, icon: f32, avatar: f32| ControlMetrics {
|
||||
control_height: rem_size * height,
|
||||
input_height: rem_size * (height + 0.125),
|
||||
padding_x: rem_size * padding,
|
||||
text_size: rem_size * text,
|
||||
icon_size: rem_size * icon,
|
||||
avatar_size: rem_size * avatar,
|
||||
};
|
||||
Self {
|
||||
small: class(1.5, 0.75, 0.85, 0.875, 1.5),
|
||||
medium: class(1.75, 1.0, 1.0, 1.0, 2.0),
|
||||
large: class(2.25, 1.25, 1.15, 1.25, 3.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// The metrics for `size`.
|
||||
pub fn resolve(&self, size: Size) -> ControlMetrics {
|
||||
match size {
|
||||
Size::Small => self.small,
|
||||
Size::Medium => self.medium,
|
||||
Size::Large => self.large,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the medium class's text-field height to the platform's stated
|
||||
/// value, scaling the other classes by the same ratio.
|
||||
pub fn set_input_height(&mut self, medium_input_height: Pixels) {
|
||||
let ratio = medium_input_height / self.medium.input_height;
|
||||
self.small.input_height *= ratio;
|
||||
self.medium.input_height = medium_input_height;
|
||||
self.large.input_height *= ratio;
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the resolved theme global from any app context.
|
||||
pub trait ActiveTheme {
|
||||
fn theme(&self) -> &Theme;
|
||||
@@ -95,6 +170,8 @@ impl Theme {
|
||||
colors: resolved.colors,
|
||||
ramps: resolved.ramps,
|
||||
radius: resolved.radius,
|
||||
root_font_size: resolved.root_font_size,
|
||||
sizes: resolved.sizes,
|
||||
accessibility: resolved.accessibility,
|
||||
}
|
||||
}
|
||||
@@ -114,7 +191,7 @@ impl Theme {
|
||||
/// Re-resolves when a window's appearance differs from the resolved variant.
|
||||
///
|
||||
/// Wire to `Window::observe_window_appearance`.
|
||||
pub fn sync_from_window(window: &gpui::Window, cx: &mut App) {
|
||||
pub fn sync_from_window(window: &mut gpui::Window, cx: &mut App) {
|
||||
let variant = ThemeVariant::from_appearance(window.appearance());
|
||||
if cx.global::<Theme>().variant != variant
|
||||
|| cx.global::<ThemeEngine>().mode_override.is_none()
|
||||
@@ -122,6 +199,11 @@ impl Theme {
|
||||
reapply(cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sizes the window's rem to the theme's root font size.
|
||||
pub fn sync_window_rem_size(window: &mut gpui::Window, cx: &App) {
|
||||
window.set_rem_size(cx.global::<Theme>().root_font_size);
|
||||
}
|
||||
}
|
||||
|
||||
/// The engine's knobs and its platform source.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::definition::ThemeDefinition;
|
||||
use crate::ramps::{AccentShades, Oklch, contrast_foreground, derive_shades, mix};
|
||||
use crate::system::SystemTheme;
|
||||
use crate::{ColorTokens, Radius, ThemeVariant};
|
||||
use crate::{ColorTokens, Radius, SizeTable, ThemeVariant};
|
||||
use gpui::px;
|
||||
|
||||
/// Derived state shades for the colors that get pressed and hovered.
|
||||
@@ -19,6 +19,9 @@ pub struct ResolvedTheme {
|
||||
pub colors: ColorTokens,
|
||||
pub ramps: Ramps,
|
||||
pub radius: Radius,
|
||||
/// The platform's native root font size; the fallback theme uses 16.
|
||||
pub root_font_size: gpui::Pixels,
|
||||
pub sizes: SizeTable,
|
||||
pub accessibility: crate::system::AccessibilityPreferences,
|
||||
}
|
||||
|
||||
@@ -132,6 +135,17 @@ pub(crate) fn resolve(
|
||||
.to_hsla();
|
||||
}
|
||||
|
||||
// The platform's root font size scales every control metric:
|
||||
// a medium control's label is exactly the root font size.
|
||||
//
|
||||
// Fallback: 16 (the web default gpui also uses for rems).
|
||||
let root_font_size = system.root_font_size.unwrap_or(px(16.));
|
||||
|
||||
let mut sizes = SizeTable::with_root_font_size(root_font_size);
|
||||
if let Some(input_height) = system.input_height {
|
||||
sizes.set_input_height(input_height);
|
||||
}
|
||||
|
||||
let mut radius = Radius::default();
|
||||
if let Some(control) = preset
|
||||
.and_then(|def| def.control_radius)
|
||||
@@ -158,6 +172,8 @@ pub(crate) fn resolve(
|
||||
},
|
||||
colors,
|
||||
radius,
|
||||
root_font_size,
|
||||
sizes,
|
||||
accessibility,
|
||||
}
|
||||
}
|
||||
@@ -182,6 +198,8 @@ mod tests {
|
||||
accent,
|
||||
palette,
|
||||
control_radius: None,
|
||||
input_height: None,
|
||||
root_font_size: None,
|
||||
accessibility: AccessibilityPreferences::default(),
|
||||
source: SourceKind::Macos,
|
||||
}
|
||||
@@ -218,9 +236,44 @@ mod tests {
|
||||
assert_eq!(resolved.ramps.secondary.base, base.secondary);
|
||||
assert_eq!(resolved.radius.sm, px(4.));
|
||||
assert_eq!(resolved.radius.control, px(6.));
|
||||
assert_eq!(resolved.root_font_size, px(16.));
|
||||
assert_eq!(resolved.sizes.medium.control_height, px(28.));
|
||||
assert_eq!(resolved.sizes.medium.input_height, px(30.));
|
||||
assert_eq!(resolved.sizes.medium.text_size, px(16.));
|
||||
assert_eq!(resolved.sizes.large.avatar_size, px(48.));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stated_root_font_size_scales_the_size_table() {
|
||||
// macOS states 13pt; the medium control shrinks to native proportions.
|
||||
let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None);
|
||||
stated.root_font_size = Some(px(13.));
|
||||
let resolved = resolve(&stated, None, None);
|
||||
assert_eq!(resolved.root_font_size, px(13.));
|
||||
assert_eq!(resolved.sizes.medium.control_height, px(22.75));
|
||||
assert_eq!(resolved.sizes.medium.text_size, px(13.));
|
||||
assert_eq!(resolved.sizes.small.text_size, px(11.05));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stated_input_height_overrides_the_derived_one_and_scales() {
|
||||
let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None);
|
||||
stated.input_height = Some(px(24.));
|
||||
let resolved = resolve(&stated, None, None);
|
||||
|
||||
// The medium class takes the stated height verbatim, and the other
|
||||
// classes keep their derived ratio to the medium class.
|
||||
assert_eq!(resolved.sizes.medium.input_height, px(24.));
|
||||
|
||||
// Derive what small would be without the statement, scale it by the
|
||||
// stated-to-derived ratio, and compare.
|
||||
let derived = SizeTable::with_root_font_size(px(16.));
|
||||
let scale = px(24.) / derived.medium.input_height;
|
||||
let expected_small = derived.small.input_height * scale;
|
||||
assert!((resolved.sizes.small.input_height - expected_small).abs() < px(0.01));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_colors_override_the_base_but_leave_the_rest_intact() {
|
||||
let palette = PartialPalette {
|
||||
|
||||
@@ -20,6 +20,10 @@ use crate::system::{AccessibilityPreferences, PartialPalette, SourceKind, System
|
||||
|
||||
/// Default control radius for macOS controls.
|
||||
const CONTROL_RADIUS_PTS: f32 = 6.0;
|
||||
/// The rounded-bezel text field's intrinsic height at regular size.
|
||||
const INPUT_HEIGHT_PTS: f32 = 24.0;
|
||||
/// The HIG's default body text on macOS is 13 pt (system font size 13 at 1x).
|
||||
const ROOT_FONT_SIZE_PTS: f32 = 13.0;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct MacosSource {
|
||||
@@ -102,6 +106,8 @@ impl ThemeSource for MacosSource {
|
||||
accent: Self::color_to_hsla(&NSColor::controlAccentColor()),
|
||||
palette,
|
||||
control_radius: Some(gpui::px(CONTROL_RADIUS_PTS)),
|
||||
input_height: Some(gpui::px(INPUT_HEIGHT_PTS)),
|
||||
root_font_size: Some(gpui::px(ROOT_FONT_SIZE_PTS)),
|
||||
accessibility,
|
||||
source: SourceKind::Macos,
|
||||
}
|
||||
|
||||
@@ -55,7 +55,12 @@ pub struct SystemTheme {
|
||||
pub variant: ThemeVariant,
|
||||
pub accent: Option<Hsla>,
|
||||
pub palette: PartialPalette,
|
||||
/// The platform's stated control corner radius, if it has one.
|
||||
pub control_radius: Option<Pixels>,
|
||||
/// The platform's stated text-field height, if it has one.
|
||||
pub input_height: Option<Pixels>,
|
||||
/// The platform's native root font size, if it has one.
|
||||
pub root_font_size: Option<Pixels>,
|
||||
pub accessibility: AccessibilityPreferences,
|
||||
pub source: SourceKind,
|
||||
}
|
||||
@@ -67,6 +72,8 @@ impl SystemTheme {
|
||||
accent: None,
|
||||
palette: PartialPalette::default(),
|
||||
control_radius: None,
|
||||
input_height: None,
|
||||
root_font_size: None,
|
||||
accessibility: AccessibilityPreferences::default(),
|
||||
source: SourceKind::Fallback,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user