feat: state platform metrics and chrome in theme
This commit is contained in:
@@ -36,10 +36,11 @@ impl Render for Harness {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A medium input is exactly `input_height`, and sits taller than the
|
/// The medium input renders at the theme's stated field height and the
|
||||||
/// medium button: native text fields are a touch taller than push buttons.
|
/// medium button at the stated control height; the two match wherever the
|
||||||
|
/// platform states equal values (macOS, Windows, GNOME).
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn medium_input_is_taller_than_the_button(cx: &mut TestAppContext) {
|
fn controls_render_at_the_stated_heights(cx: &mut TestAppContext) {
|
||||||
cx.update(Theme::init);
|
cx.update(Theme::init);
|
||||||
let state = cx.update(|cx| InputState::new_with_text("", cx));
|
let state = cx.update(|cx| InputState::new_with_text("", cx));
|
||||||
let state_for_window = state.clone();
|
let state_for_window = state.clone();
|
||||||
@@ -58,9 +59,9 @@ fn medium_input_is_taller_than_the_button(cx: &mut TestAppContext) {
|
|||||||
.debug_bounds("input-wrapper")
|
.debug_bounds("input-wrapper")
|
||||||
.expect("input rendered");
|
.expect("input rendered");
|
||||||
|
|
||||||
// The input's height is the theme table's (platform-stated) field height;
|
// Both bounds are snapped to device pixels, so allow half a pixel.
|
||||||
// both bounds are snapped to device pixels, so allow half a pixel.
|
|
||||||
let input_height = cx.update(|cx| cx.theme().sizes.medium.input_height);
|
let input_height = cx.update(|cx| cx.theme().sizes.medium.input_height);
|
||||||
assert!(input.size.height > button.size.height);
|
let button_height = cx.update(|cx| cx.theme().sizes.medium.control_height);
|
||||||
assert!((input.size.height - input_height).abs() <= gpui::px(0.5));
|
assert!((input.size.height - input_height).abs() <= gpui::px(0.5));
|
||||||
|
assert!((button.size.height - button_height).abs() <= gpui::px(0.5));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ use crate::ThemeVariant;
|
|||||||
pub struct ThemeDefinition {
|
pub struct ThemeDefinition {
|
||||||
pub mode: Option<ThemeVariant>,
|
pub mode: Option<ThemeVariant>,
|
||||||
pub control_radius: Option<f32>,
|
pub control_radius: Option<f32>,
|
||||||
|
pub overlay_radius: Option<f32>,
|
||||||
|
pub sm_radius: Option<f32>,
|
||||||
pub accent: Option<String>,
|
pub accent: Option<String>,
|
||||||
pub accent_foreground: Option<String>,
|
pub accent_foreground: Option<String>,
|
||||||
pub background: Option<String>,
|
pub background: Option<String>,
|
||||||
@@ -221,13 +223,15 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_a_full_definition() {
|
fn parses_a_full_definition() {
|
||||||
let def = ThemeDefinition::parse_toml(
|
let def = ThemeDefinition::parse_toml(
|
||||||
"mode = \"dark\"\naccent = \"#7aa2f7\"\nbackground = \"#1a1b26\"\ninfo = \"#38bdf8\"\n",
|
"mode = \"dark\"\naccent = \"#7aa2f7\"\nbackground = \"#1a1b26\"\ninfo = \"#38bdf8\"\noverlay_radius = 15.0\nsm_radius = 6.0\n",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(def.mode, Some(crate::ThemeVariant::Dark));
|
assert_eq!(def.mode, Some(crate::ThemeVariant::Dark));
|
||||||
assert!(def.accent().is_some());
|
assert!(def.accent().is_some());
|
||||||
assert!(def.info().is_some());
|
assert!(def.info().is_some());
|
||||||
assert!(def.foreground().is_none());
|
assert!(def.foreground().is_none());
|
||||||
|
assert_eq!(def.overlay_radius, Some(15.0));
|
||||||
|
assert_eq!(def.sm_radius, Some(6.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+114
-3
@@ -48,6 +48,9 @@ pub struct Theme {
|
|||||||
pub colors: ColorTokens,
|
pub colors: ColorTokens,
|
||||||
pub ramps: Ramps,
|
pub ramps: Ramps,
|
||||||
pub radius: Radius,
|
pub radius: Radius,
|
||||||
|
/// Per-platform border and focus chrome.
|
||||||
|
pub borders: Borders,
|
||||||
|
pub spacing: Spacing,
|
||||||
pub elevations: Elevations,
|
pub elevations: Elevations,
|
||||||
/// Overlay transition timing; gpui's animation primitives do the rest.
|
/// Overlay transition timing; gpui's animation primitives do the rest.
|
||||||
pub motion: Motion,
|
pub motion: Motion,
|
||||||
@@ -88,6 +91,59 @@ impl Default for Radius {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which surfaces keep a 1px border, per the platform's chrome: Windows and
|
||||||
|
/// KDE frame their controls, macOS and GNOME paint fills.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Borders {
|
||||||
|
/// Filled actions keep a border (Windows, KDE).
|
||||||
|
pub buttons: bool,
|
||||||
|
/// Text fields and selects keep a border (Windows, KDE; the macOS
|
||||||
|
/// hairline bezel).
|
||||||
|
pub fields: bool,
|
||||||
|
/// Floating layers keep a border (Windows, KDE).
|
||||||
|
pub overlays: bool,
|
||||||
|
/// Keyboard focus paints an accent ring around the control (macOS,
|
||||||
|
/// GNOME); otherwise focus re-colors the border (Windows, KDE).
|
||||||
|
pub focus_ring: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Borders {
|
||||||
|
pub(crate) fn for_platform(source: SourceKind) -> Self {
|
||||||
|
match source {
|
||||||
|
SourceKind::Windows | SourceKind::Kde => Self {
|
||||||
|
buttons: true,
|
||||||
|
fields: true,
|
||||||
|
overlays: true,
|
||||||
|
focus_ring: false,
|
||||||
|
},
|
||||||
|
SourceKind::Macos => Self {
|
||||||
|
buttons: false,
|
||||||
|
fields: true,
|
||||||
|
overlays: false,
|
||||||
|
focus_ring: true,
|
||||||
|
},
|
||||||
|
SourceKind::Omarchy | SourceKind::Gnome => Self {
|
||||||
|
buttons: false,
|
||||||
|
fields: false,
|
||||||
|
overlays: false,
|
||||||
|
focus_ring: true,
|
||||||
|
},
|
||||||
|
SourceKind::Fallback => Self {
|
||||||
|
buttons: true,
|
||||||
|
fields: true,
|
||||||
|
overlays: true,
|
||||||
|
focus_ring: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The spacing rhythm of the platform: paddings and gaps step by this unit.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Spacing {
|
||||||
|
pub unit: Pixels,
|
||||||
|
}
|
||||||
|
|
||||||
/// One layer of a floating panel's drop shadow.
|
/// One layer of a floating panel's drop shadow.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct Shadow {
|
pub struct Shadow {
|
||||||
@@ -289,6 +345,12 @@ pub struct ControlMetrics {
|
|||||||
pub plate_inset: Pixels,
|
pub plate_inset: Pixels,
|
||||||
/// The band height of multi-segment strips.
|
/// The band height of multi-segment strips.
|
||||||
pub strip_height: Pixels,
|
pub strip_height: Pixels,
|
||||||
|
/// The height of one menu row.
|
||||||
|
pub menu_item_height: Pixels,
|
||||||
|
/// Switch track height, width, and knob size.
|
||||||
|
pub switch_height: Pixels,
|
||||||
|
pub switch_width: Pixels,
|
||||||
|
pub switch_knob: Pixels,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The control metrics for every size class.
|
/// The control metrics for every size class.
|
||||||
@@ -309,6 +371,7 @@ impl SizeTable {
|
|||||||
avatar: f32,
|
avatar: f32,
|
||||||
strip: f32,
|
strip: f32,
|
||||||
inset: f32,
|
inset: f32,
|
||||||
|
floor_icon: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
let small = ClassFactors {
|
let small = ClassFactors {
|
||||||
@@ -319,6 +382,7 @@ impl SizeTable {
|
|||||||
avatar: 1.5,
|
avatar: 1.5,
|
||||||
strip: 1.5,
|
strip: 1.5,
|
||||||
inset: 0.125,
|
inset: 0.125,
|
||||||
|
floor_icon: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let medium = ClassFactors {
|
let medium = ClassFactors {
|
||||||
@@ -329,6 +393,7 @@ impl SizeTable {
|
|||||||
avatar: 2.0,
|
avatar: 2.0,
|
||||||
strip: 2.0,
|
strip: 2.0,
|
||||||
inset: 0.25,
|
inset: 0.25,
|
||||||
|
floor_icon: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let large = ClassFactors {
|
let large = ClassFactors {
|
||||||
@@ -339,21 +404,36 @@ impl SizeTable {
|
|||||||
avatar: 3.0,
|
avatar: 3.0,
|
||||||
strip: 3.0,
|
strip: 3.0,
|
||||||
inset: 0.375,
|
inset: 0.375,
|
||||||
|
floor_icon: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let metrics = |f: &ClassFactors| ControlMetrics {
|
let metrics = |f: &ClassFactors| {
|
||||||
|
// Standard controls carry 16px glyphs on every platform, fixed
|
||||||
|
// against the root font the way the platforms fix them.
|
||||||
|
let icon_size = if f.floor_icon {
|
||||||
|
(rem_size * f.icon).max(px(16.))
|
||||||
|
} else {
|
||||||
|
rem_size * f.icon
|
||||||
|
};
|
||||||
|
let switch_height = rem_size * f.height * 0.75;
|
||||||
|
ControlMetrics {
|
||||||
control_height: rem_size * f.height,
|
control_height: rem_size * f.height,
|
||||||
input_height: rem_size * (f.height + 0.125),
|
input_height: rem_size * (f.height + 0.125),
|
||||||
padding_x: rem_size * f.padding,
|
padding_x: rem_size * f.padding,
|
||||||
text_size: rem_size * f.text,
|
text_size: rem_size * f.text,
|
||||||
icon_size: rem_size * f.icon,
|
icon_size,
|
||||||
dense_icon_size: rem_size * f.icon * 0.75,
|
dense_icon_size: icon_size * 0.75,
|
||||||
indicator_size: rem_size * f.height * 0.7,
|
indicator_size: rem_size * f.height * 0.7,
|
||||||
avatar_size: rem_size * f.avatar,
|
avatar_size: rem_size * f.avatar,
|
||||||
caption_size: rem_size * f.text,
|
caption_size: rem_size * f.text,
|
||||||
dense_padding_x: rem_size * f.padding * 0.75,
|
dense_padding_x: rem_size * f.padding * 0.75,
|
||||||
plate_inset: rem_size * f.inset,
|
plate_inset: rem_size * f.inset,
|
||||||
strip_height: rem_size * f.strip,
|
strip_height: rem_size * f.strip,
|
||||||
|
menu_item_height: rem_size * f.height * 0.85,
|
||||||
|
switch_height,
|
||||||
|
switch_width: switch_height * 2.25,
|
||||||
|
switch_knob: switch_height * 0.85,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut table = Self {
|
let mut table = Self {
|
||||||
@@ -383,6 +463,35 @@ impl SizeTable {
|
|||||||
self.medium.input_height = medium_input_height;
|
self.medium.input_height = medium_input_height;
|
||||||
self.large.input_height *= ratio;
|
self.large.input_height *= ratio;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the medium class's control height to the platform's stated value.
|
||||||
|
pub fn set_control_height(&mut self, medium_control_height: Pixels) {
|
||||||
|
let ratio = medium_control_height / self.medium.control_height;
|
||||||
|
self.small.control_height *= ratio;
|
||||||
|
self.medium.control_height = medium_control_height;
|
||||||
|
self.large.control_height *= ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the medium class's menu row height to the platform's stated value.
|
||||||
|
pub fn set_menu_item_height(&mut self, medium_menu_item_height: Pixels) {
|
||||||
|
let ratio = medium_menu_item_height / self.medium.menu_item_height;
|
||||||
|
self.small.menu_item_height *= ratio;
|
||||||
|
self.medium.menu_item_height = medium_menu_item_height;
|
||||||
|
self.large.menu_item_height *= ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the medium class's switch dimensions to the platform's stated values.
|
||||||
|
pub fn set_switch_metrics(&mut self, track_height: Pixels, track_width: Pixels, knob: Pixels) {
|
||||||
|
let ratio = track_height / self.medium.switch_height;
|
||||||
|
for metrics in [&mut self.small, &mut self.large] {
|
||||||
|
metrics.switch_height *= ratio;
|
||||||
|
metrics.switch_width *= ratio;
|
||||||
|
metrics.switch_knob *= ratio;
|
||||||
|
}
|
||||||
|
self.medium.switch_height = track_height;
|
||||||
|
self.medium.switch_width = track_width;
|
||||||
|
self.medium.switch_knob = knob;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the resolved theme global from any app context.
|
/// Reads the resolved theme global from any app context.
|
||||||
@@ -408,6 +517,8 @@ impl Theme {
|
|||||||
colors: resolved.colors,
|
colors: resolved.colors,
|
||||||
ramps: resolved.ramps,
|
ramps: resolved.ramps,
|
||||||
radius: resolved.radius,
|
radius: resolved.radius,
|
||||||
|
borders: resolved.borders,
|
||||||
|
spacing: resolved.spacing,
|
||||||
elevations: resolved.elevations,
|
elevations: resolved.elevations,
|
||||||
motion: resolved.motion,
|
motion: resolved.motion,
|
||||||
scrollbar: resolved.scrollbar,
|
scrollbar: resolved.scrollbar,
|
||||||
|
|||||||
+152
-3
@@ -1,7 +1,9 @@
|
|||||||
use crate::definition::ThemeDefinition;
|
use crate::definition::ThemeDefinition;
|
||||||
use crate::ramps::{AccentShades, Oklch, contrast_foreground, derive_shades, mix};
|
use crate::ramps::{AccentShades, Oklch, contrast_foreground, derive_shades, mix};
|
||||||
use crate::system::SystemTheme;
|
use crate::system::SystemTheme;
|
||||||
use crate::{ColorTokens, Elevations, Motion, Radius, Scrollbar, SizeTable, ThemeVariant};
|
use crate::{
|
||||||
|
Borders, ColorTokens, Elevations, Motion, Radius, Scrollbar, SizeTable, Spacing, ThemeVariant,
|
||||||
|
};
|
||||||
use gpui::px;
|
use gpui::px;
|
||||||
|
|
||||||
/// The floor of a custom titlebar's height, in px at 100% scaling: the height
|
/// The floor of a custom titlebar's height, in px at 100% scaling: the height
|
||||||
@@ -26,6 +28,8 @@ pub struct ResolvedTheme {
|
|||||||
pub colors: ColorTokens,
|
pub colors: ColorTokens,
|
||||||
pub ramps: Ramps,
|
pub ramps: Ramps,
|
||||||
pub radius: Radius,
|
pub radius: Radius,
|
||||||
|
pub borders: Borders,
|
||||||
|
pub spacing: Spacing,
|
||||||
pub elevations: Elevations,
|
pub elevations: Elevations,
|
||||||
pub motion: Motion,
|
pub motion: Motion,
|
||||||
pub scrollbar: Scrollbar,
|
pub scrollbar: Scrollbar,
|
||||||
@@ -198,18 +202,35 @@ pub(crate) fn resolve(
|
|||||||
//
|
//
|
||||||
// Fallback: 16 (the web default gpui also uses for rems).
|
// Fallback: 16 (the web default gpui also uses for rems).
|
||||||
let root_font_size = system.root_font_size.unwrap_or(px(16.));
|
let root_font_size = system.root_font_size.unwrap_or(px(16.));
|
||||||
|
|
||||||
let mut sizes = SizeTable::with_root_font_size(root_font_size);
|
let mut sizes = SizeTable::with_root_font_size(root_font_size);
|
||||||
|
|
||||||
|
if let Some(control_height) = system.control_height {
|
||||||
|
sizes.set_control_height(control_height);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(input_height) = system.input_height {
|
if let Some(input_height) = system.input_height {
|
||||||
sizes.set_input_height(input_height);
|
sizes.set_input_height(input_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(switch_height) = system.switch_height {
|
||||||
|
sizes.set_switch_metrics(
|
||||||
|
switch_height,
|
||||||
|
system.switch_width.unwrap_or(switch_height * 2.25),
|
||||||
|
system.switch_knob.unwrap_or(switch_height * 0.85),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(menu_item_height) = system.menu_item_height {
|
||||||
|
sizes.set_menu_item_height(menu_item_height);
|
||||||
|
}
|
||||||
|
|
||||||
// A platform that states its titlebar height wins.
|
// A platform that states its titlebar height wins.
|
||||||
let titlebar_height = system
|
let titlebar_height = system
|
||||||
.titlebar_height
|
.titlebar_height
|
||||||
.unwrap_or_else(|| sizes.medium.control_height.max(px(TITLEBAR_HEIGHT_MIN_PX)));
|
.unwrap_or_else(|| sizes.medium.control_height.max(px(TITLEBAR_HEIGHT_MIN_PX)));
|
||||||
|
|
||||||
let mut radius = Radius::default();
|
let mut radius = Radius::default();
|
||||||
|
|
||||||
if let Some(control) = preset
|
if let Some(control) = preset
|
||||||
.and_then(|def| def.control_radius)
|
.and_then(|def| def.control_radius)
|
||||||
.map(px)
|
.map(px)
|
||||||
@@ -218,6 +239,22 @@ pub(crate) fn resolve(
|
|||||||
radius.control = control;
|
radius.control = control;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(overlay) = preset
|
||||||
|
.and_then(|def| def.overlay_radius)
|
||||||
|
.map(px)
|
||||||
|
.or(system.overlay_radius)
|
||||||
|
{
|
||||||
|
radius.overlay = overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(sm) = preset
|
||||||
|
.and_then(|def| def.sm_radius)
|
||||||
|
.map(px)
|
||||||
|
.or(system.sm_radius)
|
||||||
|
{
|
||||||
|
radius.sm = sm;
|
||||||
|
}
|
||||||
|
|
||||||
ResolvedTheme {
|
ResolvedTheme {
|
||||||
variant,
|
variant,
|
||||||
ramps: Ramps {
|
ramps: Ramps {
|
||||||
@@ -238,6 +275,10 @@ pub(crate) fn resolve(
|
|||||||
},
|
},
|
||||||
colors,
|
colors,
|
||||||
radius,
|
radius,
|
||||||
|
borders: Borders::for_platform(system.source),
|
||||||
|
spacing: Spacing {
|
||||||
|
unit: system.source.spacing_unit(),
|
||||||
|
},
|
||||||
elevations: Elevations::for_platform(system.source, variant),
|
elevations: Elevations::for_platform(system.source, variant),
|
||||||
motion: Motion::default(),
|
motion: Motion::default(),
|
||||||
scrollbar: Scrollbar {
|
scrollbar: Scrollbar {
|
||||||
@@ -283,7 +324,14 @@ mod tests {
|
|||||||
accent,
|
accent,
|
||||||
palette,
|
palette,
|
||||||
control_radius: None,
|
control_radius: None,
|
||||||
|
overlay_radius: None,
|
||||||
|
sm_radius: None,
|
||||||
|
control_height: None,
|
||||||
input_height: None,
|
input_height: None,
|
||||||
|
menu_item_height: None,
|
||||||
|
switch_height: None,
|
||||||
|
switch_width: None,
|
||||||
|
switch_knob: None,
|
||||||
titlebar_height: None,
|
titlebar_height: None,
|
||||||
root_font_size: None,
|
root_font_size: None,
|
||||||
accessibility: AccessibilityPreferences::default(),
|
accessibility: AccessibilityPreferences::default(),
|
||||||
@@ -308,7 +356,10 @@ mod tests {
|
|||||||
fn empty_system_resolves_to_the_neutral_fallback() {
|
fn empty_system_resolves_to_the_neutral_fallback() {
|
||||||
for variant in [ThemeVariant::Light, ThemeVariant::Dark] {
|
for variant in [ThemeVariant::Light, ThemeVariant::Dark] {
|
||||||
let resolved = resolve(
|
let resolved = resolve(
|
||||||
&system(variant, PartialPalette::default(), None),
|
&SystemTheme {
|
||||||
|
source: SourceKind::Fallback,
|
||||||
|
..system(variant, PartialPalette::default(), None)
|
||||||
|
},
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
@@ -333,6 +384,21 @@ mod tests {
|
|||||||
assert_eq!(resolved.sizes.medium.dense_padding_x, px(16. * 0.75));
|
assert_eq!(resolved.sizes.medium.dense_padding_x, px(16. * 0.75));
|
||||||
assert_eq!(resolved.sizes.medium.plate_inset, px(4.));
|
assert_eq!(resolved.sizes.medium.plate_inset, px(4.));
|
||||||
assert_eq!(resolved.sizes.medium.strip_height, px(32.));
|
assert_eq!(resolved.sizes.medium.strip_height, px(32.));
|
||||||
|
assert_eq!(resolved.sizes.medium.icon_size, px(16.));
|
||||||
|
assert_eq!(resolved.sizes.medium.menu_item_height, px(28. * 0.85));
|
||||||
|
assert_eq!(resolved.sizes.medium.switch_height, px(21.));
|
||||||
|
assert_eq!(resolved.sizes.medium.switch_width, px(21. * 2.25));
|
||||||
|
assert_eq!(resolved.sizes.medium.switch_knob, px(21. * 0.85));
|
||||||
|
assert_eq!(
|
||||||
|
resolved.borders,
|
||||||
|
Borders {
|
||||||
|
buttons: true,
|
||||||
|
fields: true,
|
||||||
|
overlays: true,
|
||||||
|
focus_ring: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(resolved.spacing.unit, px(4.));
|
||||||
assert_eq!(resolved.titlebar_height, px(34.));
|
assert_eq!(resolved.titlebar_height, px(34.));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,6 +462,89 @@ mod tests {
|
|||||||
assert!((resolved.sizes.small.input_height - expected_small).abs() < px(0.01));
|
assert!((resolved.sizes.small.input_height - expected_small).abs() < px(0.01));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_stated_control_height_overrides_the_medium_class_and_scales() {
|
||||||
|
let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None);
|
||||||
|
stated.control_height = Some(px(32.));
|
||||||
|
let resolved = resolve(&stated, None, None);
|
||||||
|
assert_eq!(resolved.sizes.medium.control_height, px(32.));
|
||||||
|
|
||||||
|
let derived = SizeTable::with_root_font_size(px(16.));
|
||||||
|
let scale = px(32.) / derived.medium.control_height;
|
||||||
|
assert!(
|
||||||
|
(resolved.sizes.small.control_height - derived.small.control_height * scale).abs()
|
||||||
|
< px(0.01)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(resolved.sizes.large.control_height - derived.large.control_height * scale).abs()
|
||||||
|
< px(0.01)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stated_switch_and_menu_metrics_override_the_derived_ones() {
|
||||||
|
let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None);
|
||||||
|
stated.switch_height = Some(px(20.));
|
||||||
|
stated.switch_width = Some(px(40.));
|
||||||
|
stated.switch_knob = Some(px(12.));
|
||||||
|
stated.menu_item_height = Some(px(33.));
|
||||||
|
let resolved = resolve(&stated, None, None);
|
||||||
|
|
||||||
|
assert_eq!(resolved.sizes.medium.switch_height, px(20.));
|
||||||
|
assert_eq!(resolved.sizes.medium.switch_width, px(40.));
|
||||||
|
assert_eq!(resolved.sizes.medium.switch_knob, px(12.));
|
||||||
|
assert_eq!(resolved.sizes.medium.menu_item_height, px(33.));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_radius_ladder_follows_stated_values_with_preset_priority() {
|
||||||
|
let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None);
|
||||||
|
stated.control_radius = Some(px(9.));
|
||||||
|
stated.overlay_radius = Some(px(15.));
|
||||||
|
stated.sm_radius = Some(px(6.));
|
||||||
|
let resolved = resolve(&stated, None, None);
|
||||||
|
assert_eq!(resolved.radius.control, px(9.));
|
||||||
|
assert_eq!(resolved.radius.overlay, px(15.));
|
||||||
|
assert_eq!(resolved.radius.sm, px(6.));
|
||||||
|
|
||||||
|
let def = ThemeDefinition::parse_toml("overlay_radius = 12.0\n").unwrap();
|
||||||
|
let overridden = resolve(&stated, Some(&def), None);
|
||||||
|
assert_eq!(overridden.radius.overlay, px(12.));
|
||||||
|
assert_eq!(overridden.radius.sm, px(6.));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn borders_and_spacing_follow_the_platform_source() {
|
||||||
|
let chrome = |source| {
|
||||||
|
let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None);
|
||||||
|
stated.source = source;
|
||||||
|
let resolved = resolve(&stated, None, None);
|
||||||
|
(resolved.borders, resolved.spacing.unit)
|
||||||
|
};
|
||||||
|
|
||||||
|
let (borders, unit) = chrome(SourceKind::Windows);
|
||||||
|
assert!(borders.buttons && borders.fields && borders.overlays && !borders.focus_ring);
|
||||||
|
assert_eq!(unit, px(4.));
|
||||||
|
|
||||||
|
let (borders, unit) = chrome(SourceKind::Kde);
|
||||||
|
assert!(borders.buttons && borders.fields && borders.overlays && !borders.focus_ring);
|
||||||
|
assert_eq!(unit, px(4.));
|
||||||
|
|
||||||
|
let (borders, unit) = chrome(SourceKind::Macos);
|
||||||
|
assert!(!borders.buttons && borders.fields && !borders.overlays && borders.focus_ring);
|
||||||
|
assert_eq!(unit, px(4.));
|
||||||
|
|
||||||
|
for source in [SourceKind::Gnome, SourceKind::Omarchy] {
|
||||||
|
let (borders, unit) = chrome(source);
|
||||||
|
assert!(!borders.buttons && !borders.fields && !borders.overlays && borders.focus_ring);
|
||||||
|
assert_eq!(unit, px(6.));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (borders, unit) = chrome(SourceKind::Fallback);
|
||||||
|
assert!(borders.buttons && borders.fields && borders.overlays && !borders.focus_ring);
|
||||||
|
assert_eq!(unit, px(4.));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn overlay_is_translucent_and_overridable() {
|
fn overlay_is_translucent_and_overridable() {
|
||||||
let light = resolve(
|
let light = resolve(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use gpui::Hsla;
|
use gpui::{Hsla, Pixels, px};
|
||||||
|
|
||||||
use crate::system::{PartialPalette, SourceKind, SystemTheme};
|
use crate::system::{PartialPalette, SourceKind, SystemTheme};
|
||||||
|
|
||||||
@@ -14,6 +14,9 @@ pub(crate) struct KdeScheme {
|
|||||||
pub(crate) accent: Option<Hsla>,
|
pub(crate) accent: Option<Hsla>,
|
||||||
pub(crate) background: Option<Hsla>,
|
pub(crate) background: Option<Hsla>,
|
||||||
pub(crate) palette: PartialPalette,
|
pub(crate) palette: PartialPalette,
|
||||||
|
/// The root font size parsed from `[General] font=` (family, pointSize,
|
||||||
|
/// pixelSize, ...).
|
||||||
|
pub(crate) root_font_size: Option<Pixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KdeScheme {
|
impl KdeScheme {
|
||||||
@@ -54,6 +57,7 @@ impl KdeScheme {
|
|||||||
accent,
|
accent,
|
||||||
background,
|
background,
|
||||||
palette,
|
palette,
|
||||||
|
root_font_size: root_font_size(groups.get("General")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,10 +81,17 @@ impl KdeScheme {
|
|||||||
.unwrap_or(crate::ThemeVariant::Light),
|
.unwrap_or(crate::ThemeVariant::Light),
|
||||||
accent: self.accent.or(portal_accent),
|
accent: self.accent.or(portal_accent),
|
||||||
palette: self.palette,
|
palette: self.palette,
|
||||||
control_radius: None,
|
control_radius: Some(px(5.)),
|
||||||
input_height: None,
|
overlay_radius: Some(px(5.)),
|
||||||
titlebar_height: None,
|
sm_radius: None,
|
||||||
root_font_size: None,
|
control_height: Some(px(33.)),
|
||||||
|
input_height: Some(px(29.)),
|
||||||
|
menu_item_height: Some(px(29.)),
|
||||||
|
switch_height: Some(px(18.)),
|
||||||
|
switch_width: Some(px(36.)),
|
||||||
|
switch_knob: Some(px(18.)),
|
||||||
|
titlebar_height: Some(px(44.)),
|
||||||
|
root_font_size: Some(self.root_font_size.unwrap_or(px(13.33))),
|
||||||
accessibility,
|
accessibility,
|
||||||
source: SourceKind::Kde,
|
source: SourceKind::Kde,
|
||||||
}
|
}
|
||||||
@@ -95,6 +106,22 @@ fn variant_by_lightness(background: Hsla) -> crate::ThemeVariant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The root font size of a `[General]` group's `font=` QFont string.
|
||||||
|
fn root_font_size(group: Option<&HashMap<&str, &str>>) -> Option<Pixels> {
|
||||||
|
let fields: Vec<&str> = group?.get("font")?.split(',').map(str::trim).collect();
|
||||||
|
let points = fields.get(1)?.parse::<f32>().ok()?;
|
||||||
|
let pixels = fields
|
||||||
|
.get(2)
|
||||||
|
.and_then(|size| size.parse::<f32>().ok())
|
||||||
|
.unwrap_or(-1.0);
|
||||||
|
let size = if pixels > 0.0 {
|
||||||
|
pixels
|
||||||
|
} else {
|
||||||
|
points * 96.0 / 72.0
|
||||||
|
};
|
||||||
|
(size > 0.0).then(|| px(size))
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_groups(text: &str) -> HashMap<&str, HashMap<&str, &str>> {
|
fn parse_groups(text: &str) -> HashMap<&str, HashMap<&str, &str>> {
|
||||||
let mut groups: HashMap<&str, HashMap<&str, &str>> = HashMap::new();
|
let mut groups: HashMap<&str, HashMap<&str, &str>> = HashMap::new();
|
||||||
let mut section: Option<&str> = None;
|
let mut section: Option<&str> = None;
|
||||||
@@ -305,4 +332,31 @@ activeBackground=39,44,49
|
|||||||
let system = scheme.into_system(Default::default(), None, Some(crate::ThemeVariant::Light));
|
let system = scheme.into_system(Default::default(), None, Some(crate::ThemeVariant::Light));
|
||||||
assert_eq!(system.variant, crate::ThemeVariant::Light);
|
assert_eq!(system.variant, crate::ThemeVariant::Light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_scheme_states_breeze_metrics_and_the_default_font() {
|
||||||
|
let system = KdeScheme::parse(BREEZE_DARK).into_system(Default::default(), None, None);
|
||||||
|
|
||||||
|
assert_eq!(system.control_height, Some(px(33.)));
|
||||||
|
assert_eq!(system.input_height, Some(px(29.)));
|
||||||
|
assert_eq!(system.menu_item_height, Some(px(29.)));
|
||||||
|
assert_eq!(system.control_radius, Some(px(5.)));
|
||||||
|
assert_eq!(system.overlay_radius, Some(px(5.)));
|
||||||
|
assert_eq!(system.switch_height, Some(px(18.)));
|
||||||
|
assert_eq!(system.switch_width, Some(px(36.)));
|
||||||
|
assert_eq!(system.switch_knob, Some(px(18.)));
|
||||||
|
assert_eq!(system.titlebar_height, Some(px(44.)));
|
||||||
|
assert!((system.root_font_size.unwrap() - px(13.33)).abs() < px(0.01));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_stated_font_beats_the_default() {
|
||||||
|
let text = "[General]\nfont=Noto Sans,10,-1,5,50,0,0,0,0,0\n";
|
||||||
|
let system = KdeScheme::parse(text).into_system(Default::default(), None, None);
|
||||||
|
assert!((system.root_font_size.unwrap() - px(10.0 * 96.0 / 72.0)).abs() < px(0.01));
|
||||||
|
|
||||||
|
let text = "[General]\nfont=Adwaita Sans,-1,12,5,50,0,0,0,0,0\n";
|
||||||
|
let system = KdeScheme::parse(text).into_system(Default::default(), None, None);
|
||||||
|
assert_eq!(system.root_font_size, Some(px(12.)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use gpui::Hsla;
|
use gpui::{Hsla, Pixels};
|
||||||
use notify::{RecursiveMode, Watcher};
|
use notify::{RecursiveMode, Watcher};
|
||||||
|
|
||||||
use super::ThemeSource;
|
use super::ThemeSource;
|
||||||
@@ -55,11 +55,12 @@ fn detect(home: &Path, portal_reachable: bool, gsettings_reachable: bool) -> Bac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What `gsettings` states for GNOME sessions without a portal.
|
/// What `gsettings` states for GNOME sessions.
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
struct GSettingsSnapshot {
|
struct GSettingsSnapshot {
|
||||||
variant: Option<ThemeVariant>,
|
variant: Option<ThemeVariant>,
|
||||||
accent: Option<Hsla>,
|
accent: Option<Hsla>,
|
||||||
|
root_font_size: Option<Pixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gsettings_value(args: &[&str]) -> Option<String> {
|
fn gsettings_value(args: &[&str]) -> Option<String> {
|
||||||
@@ -97,23 +98,31 @@ fn read_gsettings() -> Option<GSettingsSnapshot> {
|
|||||||
"prefer-light" => Some(ThemeVariant::Light),
|
"prefer-light" => Some(ThemeVariant::Light),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
let accent = gsettings_value(&["get", "org.gnome.desktop.interface", "accent-color"])
|
let accent = gsettings_value(&["get", "org.gnome.desktop.interface", "accent-color"])
|
||||||
.and_then(|value| accent_rgb(value.trim().trim_matches('\'')));
|
.and_then(|value| accent_rgb(value.trim().trim_matches('\'')));
|
||||||
|
|
||||||
if variant.is_none() && accent.is_none() {
|
let root_font_size = gsettings_value(&["get", "org.gnome.desktop.interface", "font-name"])
|
||||||
|
.and_then(|value| font_root_px(value.trim().trim_matches('\'')));
|
||||||
|
|
||||||
|
if variant.is_none() && accent.is_none() && root_font_size.is_none() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(GSettingsSnapshot { variant, accent })
|
Some(GSettingsSnapshot {
|
||||||
|
variant,
|
||||||
|
accent,
|
||||||
|
root_font_size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font_root_px(name: &str) -> Option<Pixels> {
|
||||||
|
let points = name.split_whitespace().last()?.parse::<f32>().ok()?;
|
||||||
|
(points > 0.0).then(|| gpui::px(points * 96.0 / 72.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn platform_source() -> Box<dyn ThemeSource> {
|
pub(crate) fn platform_source() -> Box<dyn ThemeSource> {
|
||||||
let portal = portal::io::PortalClient::connect();
|
let portal = portal::io::PortalClient::connect();
|
||||||
|
let gsettings = read_gsettings();
|
||||||
let gsettings = if portal.is_none() {
|
|
||||||
read_gsettings()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let backend = home_dir()
|
let backend = home_dir()
|
||||||
.map(|home| detect(&home, portal.is_some(), gsettings.is_some()))
|
.map(|home| detect(&home, portal.is_some(), gsettings.is_some()))
|
||||||
@@ -129,6 +138,36 @@ pub(crate) fn platform_source() -> Box<dyn ThemeSource> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LinuxSource {
|
impl LinuxSource {
|
||||||
|
fn gnome_snapshot(
|
||||||
|
&self,
|
||||||
|
variant: ThemeVariant,
|
||||||
|
accent: Option<Hsla>,
|
||||||
|
accessibility: crate::system::AccessibilityPreferences,
|
||||||
|
) -> SystemTheme {
|
||||||
|
SystemTheme {
|
||||||
|
variant,
|
||||||
|
accent,
|
||||||
|
palette: PartialPalette::default(),
|
||||||
|
control_radius: Some(gpui::px(9.)),
|
||||||
|
overlay_radius: Some(gpui::px(15.)),
|
||||||
|
sm_radius: Some(gpui::px(6.)),
|
||||||
|
control_height: Some(gpui::px(34.)),
|
||||||
|
input_height: Some(gpui::px(34.)),
|
||||||
|
menu_item_height: Some(gpui::px(32.)),
|
||||||
|
switch_height: Some(gpui::px(26.)),
|
||||||
|
switch_width: Some(gpui::px(46.)),
|
||||||
|
switch_knob: Some(gpui::px(20.)),
|
||||||
|
titlebar_height: Some(gpui::px(47.)),
|
||||||
|
root_font_size: Some(
|
||||||
|
self.gsettings
|
||||||
|
.and_then(|gsettings| gsettings.root_font_size)
|
||||||
|
.unwrap_or(gpui::px(14.67)),
|
||||||
|
),
|
||||||
|
accessibility,
|
||||||
|
source: SourceKind::Gnome,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn portal_snapshot(
|
fn portal_snapshot(
|
||||||
&self,
|
&self,
|
||||||
variant: ThemeVariant,
|
variant: ThemeVariant,
|
||||||
@@ -136,31 +175,21 @@ impl LinuxSource {
|
|||||||
accessibility: crate::system::AccessibilityPreferences,
|
accessibility: crate::system::AccessibilityPreferences,
|
||||||
) -> SystemTheme {
|
) -> SystemTheme {
|
||||||
if let Some(appearance) = portal_appearance {
|
if let Some(appearance) = portal_appearance {
|
||||||
return SystemTheme {
|
return self.gnome_snapshot(
|
||||||
variant: appearance.variant(variant),
|
appearance.variant(variant),
|
||||||
accent: appearance.accent,
|
appearance.accent,
|
||||||
palette: PartialPalette::default(),
|
|
||||||
control_radius: None,
|
|
||||||
input_height: None,
|
|
||||||
titlebar_height: None,
|
|
||||||
root_font_size: None,
|
|
||||||
accessibility,
|
accessibility,
|
||||||
source: SourceKind::Gnome,
|
);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(gsettings) = self.gsettings {
|
if let Some(gsettings) = self.gsettings {
|
||||||
return SystemTheme {
|
return self.gnome_snapshot(
|
||||||
variant: gsettings.variant.unwrap_or(variant),
|
gsettings.variant.unwrap_or(variant),
|
||||||
accent: gsettings.accent,
|
gsettings.accent,
|
||||||
palette: PartialPalette::default(),
|
|
||||||
control_radius: None,
|
|
||||||
input_height: None,
|
|
||||||
titlebar_height: None,
|
|
||||||
root_font_size: None,
|
|
||||||
accessibility,
|
accessibility,
|
||||||
source: SourceKind::Gnome,
|
);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemTheme::fallback(variant)
|
SystemTheme::fallback(variant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,4 +318,52 @@ mod tests {
|
|||||||
assert_eq!(detect(&home, true, false), Backend::Kde);
|
assert_eq!(detect(&home, true, false), Backend::Kde);
|
||||||
std::fs::remove_dir_all(&home).expect("cleanup");
|
std::fs::remove_dir_all(&home).expect("cleanup");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_gsettings_font_name_states_the_root_font_size() {
|
||||||
|
assert_eq!(
|
||||||
|
font_root_px("Adwaita Sans 11"),
|
||||||
|
Some(gpui::px(11.0 * 96.0 / 72.0))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
font_root_px("Noto Sans 10"),
|
||||||
|
Some(gpui::px(10.0 * 96.0 / 72.0))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
font_root_px("Cantarell Bold 12.5"),
|
||||||
|
Some(gpui::px(12.5 * 96.0 / 72.0))
|
||||||
|
);
|
||||||
|
assert_eq!(font_root_px("Adwaita Sans"), None);
|
||||||
|
assert_eq!(font_root_px("Sans 0"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_gnome_snapshot_states_the_libadwaita_metrics() {
|
||||||
|
let source = LinuxSource {
|
||||||
|
backend: Backend::Portal,
|
||||||
|
portal: None,
|
||||||
|
gsettings: None,
|
||||||
|
file_watcher: None,
|
||||||
|
_signal: None,
|
||||||
|
};
|
||||||
|
let system = source.gnome_snapshot(
|
||||||
|
ThemeVariant::Light,
|
||||||
|
None,
|
||||||
|
crate::system::AccessibilityPreferences::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(system.source, SourceKind::Gnome);
|
||||||
|
assert_eq!(system.control_height, Some(gpui::px(34.)));
|
||||||
|
assert_eq!(system.input_height, Some(gpui::px(34.)));
|
||||||
|
assert_eq!(system.menu_item_height, Some(gpui::px(32.)));
|
||||||
|
assert_eq!(system.control_radius, Some(gpui::px(9.)));
|
||||||
|
assert_eq!(system.overlay_radius, Some(gpui::px(15.)));
|
||||||
|
assert_eq!(system.sm_radius, Some(gpui::px(6.)));
|
||||||
|
assert_eq!(system.switch_height, Some(gpui::px(26.)));
|
||||||
|
assert_eq!(system.switch_width, Some(gpui::px(46.)));
|
||||||
|
assert_eq!(system.switch_knob, Some(gpui::px(20.)));
|
||||||
|
assert_eq!(system.titlebar_height, Some(gpui::px(47.)));
|
||||||
|
// The default 11pt font when gsettings states none.
|
||||||
|
assert_eq!(system.root_font_size, Some(gpui::px(14.67)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,16 @@ use crate::system::{AccessibilityPreferences, PartialPalette, SourceKind, System
|
|||||||
const CONTROL_RADIUS_PTS: f32 = 6.0;
|
const CONTROL_RADIUS_PTS: f32 = 6.0;
|
||||||
/// The rounded-bezel text field's intrinsic height at regular size.
|
/// The rounded-bezel text field's intrinsic height at regular size.
|
||||||
const INPUT_HEIGHT_PTS: f32 = 24.0;
|
const INPUT_HEIGHT_PTS: f32 = 24.0;
|
||||||
|
/// AppKit's regular push button, measured via `NSButton::fittingSize`.
|
||||||
|
const CONTROL_HEIGHT_PTS: f32 = 24.0;
|
||||||
|
/// The standard `NSMenu` row height.
|
||||||
|
const MENU_ITEM_HEIGHT_PTS: f32 = 20.0;
|
||||||
|
/// `NSSwitch`, measured via `fittingSize`.
|
||||||
|
const SWITCH_TRACK_HEIGHT_PTS: f32 = 24.0;
|
||||||
|
const SWITCH_TRACK_WIDTH_PTS: f32 = 54.0;
|
||||||
|
const SWITCH_KNOB_PTS: f32 = 20.0;
|
||||||
|
/// The standard titled window's titlebar, measured via `contentLayoutRect`.
|
||||||
|
const TITLEBAR_HEIGHT_PTS: f32 = 32.0;
|
||||||
/// The HIG's default body text on macOS is 13 pt (system font size 13 at 1x).
|
/// 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;
|
const ROOT_FONT_SIZE_PTS: f32 = 13.0;
|
||||||
|
|
||||||
@@ -112,8 +122,15 @@ impl ThemeSource for MacosSource {
|
|||||||
accent: Self::color_to_hsla(&NSColor::controlAccentColor()),
|
accent: Self::color_to_hsla(&NSColor::controlAccentColor()),
|
||||||
palette,
|
palette,
|
||||||
control_radius: Some(gpui::px(CONTROL_RADIUS_PTS)),
|
control_radius: Some(gpui::px(CONTROL_RADIUS_PTS)),
|
||||||
|
overlay_radius: None,
|
||||||
|
sm_radius: None,
|
||||||
|
control_height: Some(gpui::px(CONTROL_HEIGHT_PTS)),
|
||||||
input_height: Some(gpui::px(INPUT_HEIGHT_PTS)),
|
input_height: Some(gpui::px(INPUT_HEIGHT_PTS)),
|
||||||
titlebar_height: None,
|
menu_item_height: Some(gpui::px(MENU_ITEM_HEIGHT_PTS)),
|
||||||
|
switch_height: Some(gpui::px(SWITCH_TRACK_HEIGHT_PTS)),
|
||||||
|
switch_width: Some(gpui::px(SWITCH_TRACK_WIDTH_PTS)),
|
||||||
|
switch_knob: Some(gpui::px(SWITCH_KNOB_PTS)),
|
||||||
|
titlebar_height: Some(gpui::px(TITLEBAR_HEIGHT_PTS)),
|
||||||
root_font_size: Some(gpui::px(ROOT_FONT_SIZE_PTS)),
|
root_font_size: Some(gpui::px(ROOT_FONT_SIZE_PTS)),
|
||||||
accessibility,
|
accessibility,
|
||||||
source: SourceKind::Macos,
|
source: SourceKind::Macos,
|
||||||
|
|||||||
@@ -60,8 +60,15 @@ impl OmarchyTheme {
|
|||||||
variant: self.variant,
|
variant: self.variant,
|
||||||
accent: self.accent,
|
accent: self.accent,
|
||||||
palette: self.palette,
|
palette: self.palette,
|
||||||
control_radius: None,
|
control_radius: Some(gpui::px(9.)),
|
||||||
input_height: None,
|
overlay_radius: Some(gpui::px(15.)),
|
||||||
|
sm_radius: Some(gpui::px(6.)),
|
||||||
|
control_height: Some(gpui::px(34.)),
|
||||||
|
input_height: Some(gpui::px(34.)),
|
||||||
|
menu_item_height: Some(gpui::px(32.)),
|
||||||
|
switch_height: Some(gpui::px(26.)),
|
||||||
|
switch_width: Some(gpui::px(46.)),
|
||||||
|
switch_knob: Some(gpui::px(20.)),
|
||||||
titlebar_height: None,
|
titlebar_height: None,
|
||||||
root_font_size: None,
|
root_font_size: None,
|
||||||
accessibility,
|
accessibility,
|
||||||
@@ -215,6 +222,23 @@ color15 = \"#635d6f\"
|
|||||||
assert_eq!(theme.variant, ThemeVariant::Light);
|
assert_eq!(theme.variant, ThemeVariant::Light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn omarchy_shares_the_gnome_control_metrics() {
|
||||||
|
let theme = OmarchyTheme::parse(TOKYO_NIGHT).expect("parses");
|
||||||
|
let system = theme.into_system(Default::default());
|
||||||
|
|
||||||
|
assert_eq!(system.control_height, Some(gpui::px(34.)));
|
||||||
|
assert_eq!(system.input_height, Some(gpui::px(34.)));
|
||||||
|
assert_eq!(system.menu_item_height, Some(gpui::px(32.)));
|
||||||
|
assert_eq!(system.control_radius, Some(gpui::px(9.)));
|
||||||
|
assert_eq!(system.overlay_radius, Some(gpui::px(15.)));
|
||||||
|
assert_eq!(system.switch_height, Some(gpui::px(26.)));
|
||||||
|
assert_eq!(system.switch_width, Some(gpui::px(46.)));
|
||||||
|
assert_eq!(system.switch_knob, Some(gpui::px(20.)));
|
||||||
|
assert_eq!(system.titlebar_height, None);
|
||||||
|
assert_eq!(system.root_font_size, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn non_toml_text_is_rejected() {
|
fn non_toml_text_is_rejected() {
|
||||||
assert!(OmarchyTheme::parse("not a theme").is_none());
|
assert!(OmarchyTheme::parse("not a theme").is_none());
|
||||||
|
|||||||
@@ -14,8 +14,18 @@ use crate::system::{AccessibilityPreferences, PartialPalette, SourceKind, System
|
|||||||
|
|
||||||
/// WinUI 11's `ControlCornerRadius`, in pixels at 100% scaling.
|
/// WinUI 11's `ControlCornerRadius`, in pixels at 100% scaling.
|
||||||
const CONTROL_RADIUS_PX: f32 = 4.0;
|
const CONTROL_RADIUS_PX: f32 = 4.0;
|
||||||
|
/// WinUI 11's `OverlayCornerRadius`, in pixels at 100% scaling.
|
||||||
|
const OVERLAY_RADIUS_PX: f32 = 8.0;
|
||||||
/// WinUI 11's default text-box height, in pixels at 100% scaling.
|
/// WinUI 11's default text-box height, in pixels at 100% scaling.
|
||||||
const INPUT_HEIGHT_PX: f32 = 32.0;
|
const INPUT_HEIGHT_PX: f32 = 32.0;
|
||||||
|
/// The default button height, in pixels at 100% scaling.
|
||||||
|
const CONTROL_HEIGHT_PX: f32 = 32.0;
|
||||||
|
/// The default menu row: item padding 4/5 + a 20px Body line + 2px item margins.
|
||||||
|
const MENU_ITEM_HEIGHT_PX: f32 = 33.0;
|
||||||
|
/// ToggleSwitch: a 40x20 track with a 12px resting knob.
|
||||||
|
const SWITCH_TRACK_HEIGHT_PX: f32 = 20.0;
|
||||||
|
const SWITCH_TRACK_WIDTH_PX: f32 = 40.0;
|
||||||
|
const SWITCH_KNOB_PX: f32 = 12.0;
|
||||||
/// The window caption's height, in pixels at 100% scaling, as Win32 reports it.
|
/// The window caption's height, in pixels at 100% scaling, as Win32 reports it.
|
||||||
const TITLEBAR_HEIGHT_PX: f32 = 32.0;
|
const TITLEBAR_HEIGHT_PX: f32 = 32.0;
|
||||||
/// WinUI 11's default body text size, in pixels at 100% scaling.
|
/// WinUI 11's default body text size, in pixels at 100% scaling.
|
||||||
@@ -159,7 +169,14 @@ impl ThemeSource for WindowsSource {
|
|||||||
..PartialPalette::default()
|
..PartialPalette::default()
|
||||||
},
|
},
|
||||||
control_radius: Some(gpui::px(CONTROL_RADIUS_PX)),
|
control_radius: Some(gpui::px(CONTROL_RADIUS_PX)),
|
||||||
|
overlay_radius: Some(gpui::px(OVERLAY_RADIUS_PX)),
|
||||||
|
sm_radius: None,
|
||||||
|
control_height: Some(gpui::px(CONTROL_HEIGHT_PX)),
|
||||||
input_height: Some(gpui::px(INPUT_HEIGHT_PX)),
|
input_height: Some(gpui::px(INPUT_HEIGHT_PX)),
|
||||||
|
menu_item_height: Some(gpui::px(MENU_ITEM_HEIGHT_PX)),
|
||||||
|
switch_height: Some(gpui::px(SWITCH_TRACK_HEIGHT_PX)),
|
||||||
|
switch_width: Some(gpui::px(SWITCH_TRACK_WIDTH_PX)),
|
||||||
|
switch_knob: Some(gpui::px(SWITCH_KNOB_PX)),
|
||||||
titlebar_height: Some(gpui::px(TITLEBAR_HEIGHT_PX)),
|
titlebar_height: Some(gpui::px(TITLEBAR_HEIGHT_PX)),
|
||||||
root_font_size: Some(gpui::px(ROOT_FONT_SIZE_PX * text_scale as f32)),
|
root_font_size: Some(gpui::px(ROOT_FONT_SIZE_PX * text_scale as f32)),
|
||||||
accessibility: self.accessibility(),
|
accessibility: self.accessibility(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! The normalized snapshot a platform adapter produces.
|
//! The normalized snapshot a platform adapter produces.
|
||||||
|
|
||||||
use gpui::{Hsla, Pixels};
|
use gpui::{Hsla, Pixels, px};
|
||||||
|
|
||||||
use crate::ThemeVariant;
|
use crate::ThemeVariant;
|
||||||
|
|
||||||
@@ -43,6 +43,17 @@ impl SourceKind {
|
|||||||
pub fn scrollbar_autohide(self) -> bool {
|
pub fn scrollbar_autohide(self) -> bool {
|
||||||
matches!(self, Self::Macos | Self::Omarchy | Self::Gnome)
|
matches!(self, Self::Macos | Self::Omarchy | Self::Gnome)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The base spacing unit:
|
||||||
|
///
|
||||||
|
/// - GNOME lays out on a 6px grid
|
||||||
|
/// - Other platform on 4px grid
|
||||||
|
pub fn spacing_unit(self) -> Pixels {
|
||||||
|
match self {
|
||||||
|
Self::Gnome | Self::Omarchy => px(6.),
|
||||||
|
_ => px(4.),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A partial palette with optional colors.
|
/// A partial palette with optional colors.
|
||||||
@@ -83,8 +94,22 @@ pub struct SystemTheme {
|
|||||||
pub palette: PartialPalette,
|
pub palette: PartialPalette,
|
||||||
/// The platform's stated control corner radius, if it has one.
|
/// The platform's stated control corner radius, if it has one.
|
||||||
pub control_radius: Option<Pixels>,
|
pub control_radius: Option<Pixels>,
|
||||||
|
/// The platform's stated menu/popover radius, if it has one.
|
||||||
|
pub overlay_radius: Option<Pixels>,
|
||||||
|
/// The platform's stated checkbox radius, if it has one.
|
||||||
|
pub sm_radius: Option<Pixels>,
|
||||||
|
/// The platform's stated control height, if it has one.
|
||||||
|
pub control_height: Option<Pixels>,
|
||||||
/// The platform's stated text-field height, if it has one.
|
/// The platform's stated text-field height, if it has one.
|
||||||
pub input_height: Option<Pixels>,
|
pub input_height: Option<Pixels>,
|
||||||
|
/// The platform's stated menu row height, if it has one.
|
||||||
|
pub menu_item_height: Option<Pixels>,
|
||||||
|
/// The platform's stated switch track height, if it has one.
|
||||||
|
pub switch_height: Option<Pixels>,
|
||||||
|
/// The platform's stated switch track width, if it has one.
|
||||||
|
pub switch_width: Option<Pixels>,
|
||||||
|
/// The platform's stated switch knob size, if it has one.
|
||||||
|
pub switch_knob: Option<Pixels>,
|
||||||
/// The platform's native titlebar height, if it states one.
|
/// The platform's native titlebar height, if it states one.
|
||||||
pub titlebar_height: Option<Pixels>,
|
pub titlebar_height: Option<Pixels>,
|
||||||
/// The platform's native root font size, if it has one.
|
/// The platform's native root font size, if it has one.
|
||||||
@@ -100,7 +125,14 @@ impl SystemTheme {
|
|||||||
accent: None,
|
accent: None,
|
||||||
palette: PartialPalette::default(),
|
palette: PartialPalette::default(),
|
||||||
control_radius: None,
|
control_radius: None,
|
||||||
|
overlay_radius: None,
|
||||||
|
sm_radius: None,
|
||||||
|
control_height: None,
|
||||||
input_height: None,
|
input_height: None,
|
||||||
|
menu_item_height: None,
|
||||||
|
switch_height: None,
|
||||||
|
switch_width: None,
|
||||||
|
switch_knob: None,
|
||||||
titlebar_height: None,
|
titlebar_height: None,
|
||||||
root_font_size: None,
|
root_font_size: None,
|
||||||
accessibility: AccessibilityPreferences::default(),
|
accessibility: AccessibilityPreferences::default(),
|
||||||
|
|||||||
+17
-8
@@ -9,18 +9,20 @@ is the first downstream consumer; its screens drive the order.
|
|||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
|
- [ ] **Platform metric adoption** — update components one at a time
|
||||||
|
through the review gate against the landed theme tokens. The stated
|
||||||
|
metrics and the radius ladder already flow through the size table,
|
||||||
|
so each stop covers border + focus chrome (`Theme::borders`), the
|
||||||
|
spacing unit (`Theme::spacing`), and the component's stated dims:
|
||||||
|
button (the API template) first, then input, select, menu, popover,
|
||||||
|
dialog, toast, tooltip, alert, combobox, command palette, switch;
|
||||||
|
per-platform targets in
|
||||||
|
[platform-metrics-audit.md](platform-metrics-audit.md).
|
||||||
|
|
||||||
## Queue — to follow
|
## Queue — to follow
|
||||||
|
|
||||||
Gaps surfaced by the signed comparison; ordered by signed's usage.
|
Gaps surfaced by the signed comparison; ordered by signed's usage.
|
||||||
|
|
||||||
- [ ] **Platform metric fidelity** — native-sizing pass over the theme and
|
|
||||||
components: stated `control_height` per platform (macOS 24 measured /
|
|
||||||
Windows 32 / GNOME ≈34 / KDE ≈33), Linux root font sizes (GNOME 14.67px,
|
|
||||||
KDE 13.33px), platform overlay/sm radii, per-source border + focus
|
|
||||||
policy (macOS/GNOME borderless fills, Windows/KDE 1px), titlebar
|
|
||||||
heights (GNOME 47), a theme spacing scale, switch/icon calibration;
|
|
||||||
ranked findings in
|
|
||||||
[platform-metrics-audit.md](platform-metrics-audit.md).
|
|
||||||
- [ ] **Virtual list** — generic virtualized variable-height rows with a
|
- [ ] **Virtual list** — generic virtualized variable-height rows with a
|
||||||
scroll handle, promoted from the editor views' pattern over gpui's
|
scroll handle, promoted from the editor views' pattern over gpui's
|
||||||
`list`; signed's PR lists and commit diff drive it.
|
`list`; signed's PR lists and commit diff drive it.
|
||||||
@@ -55,6 +57,13 @@ Gaps surfaced by the signed comparison; ordered by signed's usage.
|
|||||||
- [x] Platform sources — macOS (NSColor), Windows 11 (UISettings), GNOME
|
- [x] Platform sources — macOS (NSColor), Windows 11 (UISettings), GNOME
|
||||||
(portal), KDE (kdeglobals), Omarchy (colors.toml), neutral fallback;
|
(portal), KDE (kdeglobals), Omarchy (colors.toml), neutral fallback;
|
||||||
live watchers for each
|
live watchers for each
|
||||||
|
- [x] Platform metric fidelity — stated control/input/menu-row/switch
|
||||||
|
metrics and the full radius ladder per platform, Linux root fonts
|
||||||
|
(GNOME gsettings `font-name`, KDE `kdeglobals font=`), per-source
|
||||||
|
border + focus chrome (`Theme::borders`), and a per-platform spacing
|
||||||
|
unit (`Theme::spacing`); findings in
|
||||||
|
[platform-metrics-audit.md](platform-metrics-audit.md). Components
|
||||||
|
adopt the tokens one by one (see Next)
|
||||||
|
|
||||||
### icons (`crates/icons`)
|
### icons (`crates/icons`)
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,10 @@ density is frozen at a macOS-ish tuning.
|
|||||||
|
|
||||||
## Recommended changes, in priority order
|
## Recommended changes, in priority order
|
||||||
|
|
||||||
|
Landed 2026-11 with two deviations: Omarchy is grouped with GNOME (it
|
||||||
|
styles its apps with adwaita), and macOS's titlebar is stated at the
|
||||||
|
measured 32pt rather than queried at runtime (28pt on older builds).
|
||||||
|
|
||||||
1. **State `control_height` per platform** — add `control_height:
|
1. **State `control_height` per platform** — add `control_height:
|
||||||
Option<Pixels>` beside `input_height` on `SystemTheme`, reuse the
|
Option<Pixels>` beside `input_height` on `SystemTheme`, reuse the
|
||||||
ratio-scaling pattern: macOS 24 (measured), Windows 32, GNOME ≈34, KDE
|
ratio-scaling pattern: macOS 24 (measured), Windows 32, GNOME ≈34, KDE
|
||||||
@@ -248,9 +252,11 @@ density is frozen at a macOS-ish tuning.
|
|||||||
queried rather than a drifted floor (32 on current builds, 28 older);
|
queried rather than a drifted floor (32 on current builds, 28 older);
|
||||||
Windows stays stated.
|
Windows stays stated.
|
||||||
6. **Add a minimal spacing scale to `Theme`** driven by a per-platform base
|
6. **Add a minimal spacing scale to `Theme`** driven by a per-platform base
|
||||||
unit (GNOME 6, KDE 4/6/8, others 4); route menu/popover paddings, menu
|
unit (GNOME 6, KDE 4/6/8, others 4); route menu/popover paddings and menu
|
||||||
item min-height (GNOME 32), and the hardcoded `min_w_`/`max_w` values
|
row min-heights through it — GNOME 32 (published), Windows ≈33 (padding
|
||||||
through it.
|
4/5 + a 20px Body line + 2px item margins), KDE ≈29 (max(font, 20px
|
||||||
|
floors) + 1 + 2×4); the hardcoded `min_w_`/`max_w` surface values stay
|
||||||
|
put — they are platform-invariant.
|
||||||
7. **Calibrate switches and icons per platform** — macOS 54×24, Windows
|
7. **Calibrate switches and icons per platform** — macOS 54×24, Windows
|
||||||
40×20 with 12px knob, KDE 36×18; icon 16 on macOS/Windows.
|
40×20 with 12px knob, KDE 36×18; icon 16 on macOS/Windows.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user