chore: audit all components #2
@@ -36,10 +36,11 @@ impl Render for Harness {
|
||||
}
|
||||
}
|
||||
|
||||
/// A medium input is exactly `input_height`, and sits taller than the
|
||||
/// medium button: native text fields are a touch taller than push buttons.
|
||||
/// The medium input renders at the theme's stated field height and the
|
||||
/// medium button at the stated control height; the two match wherever the
|
||||
/// platform states equal values (macOS, Windows, GNOME).
|
||||
#[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);
|
||||
let state = cx.update(|cx| InputState::new_with_text("", cx));
|
||||
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")
|
||||
.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);
|
||||
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!((button.size.height - button_height).abs() <= gpui::px(0.5));
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ use crate::ThemeVariant;
|
||||
pub struct ThemeDefinition {
|
||||
pub mode: Option<ThemeVariant>,
|
||||
pub control_radius: Option<f32>,
|
||||
pub overlay_radius: Option<f32>,
|
||||
pub sm_radius: Option<f32>,
|
||||
pub accent: Option<String>,
|
||||
pub accent_foreground: Option<String>,
|
||||
pub background: Option<String>,
|
||||
@@ -221,13 +223,15 @@ mod tests {
|
||||
#[test]
|
||||
fn parses_a_full_definition() {
|
||||
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();
|
||||
assert_eq!(def.mode, Some(crate::ThemeVariant::Dark));
|
||||
assert!(def.accent().is_some());
|
||||
assert!(def.info().is_some());
|
||||
assert!(def.foreground().is_none());
|
||||
assert_eq!(def.overlay_radius, Some(15.0));
|
||||
assert_eq!(def.sm_radius, Some(6.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+124
-13
@@ -48,6 +48,9 @@ pub struct Theme {
|
||||
pub colors: ColorTokens,
|
||||
pub ramps: Ramps,
|
||||
pub radius: Radius,
|
||||
/// Per-platform border and focus chrome.
|
||||
pub borders: Borders,
|
||||
pub spacing: Spacing,
|
||||
pub elevations: Elevations,
|
||||
/// Overlay transition timing; gpui's animation primitives do the rest.
|
||||
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.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Shadow {
|
||||
@@ -289,6 +345,12 @@ pub struct ControlMetrics {
|
||||
pub plate_inset: Pixels,
|
||||
/// The band height of multi-segment strips.
|
||||
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.
|
||||
@@ -309,6 +371,7 @@ impl SizeTable {
|
||||
avatar: f32,
|
||||
strip: f32,
|
||||
inset: f32,
|
||||
floor_icon: bool,
|
||||
}
|
||||
|
||||
let small = ClassFactors {
|
||||
@@ -319,6 +382,7 @@ impl SizeTable {
|
||||
avatar: 1.5,
|
||||
strip: 1.5,
|
||||
inset: 0.125,
|
||||
floor_icon: false,
|
||||
};
|
||||
|
||||
let medium = ClassFactors {
|
||||
@@ -329,6 +393,7 @@ impl SizeTable {
|
||||
avatar: 2.0,
|
||||
strip: 2.0,
|
||||
inset: 0.25,
|
||||
floor_icon: true,
|
||||
};
|
||||
|
||||
let large = ClassFactors {
|
||||
@@ -339,21 +404,36 @@ impl SizeTable {
|
||||
avatar: 3.0,
|
||||
strip: 3.0,
|
||||
inset: 0.375,
|
||||
floor_icon: true,
|
||||
};
|
||||
|
||||
let metrics = |f: &ClassFactors| ControlMetrics {
|
||||
control_height: rem_size * f.height,
|
||||
input_height: rem_size * (f.height + 0.125),
|
||||
padding_x: rem_size * f.padding,
|
||||
text_size: rem_size * f.text,
|
||||
icon_size: rem_size * f.icon,
|
||||
dense_icon_size: rem_size * f.icon * 0.75,
|
||||
indicator_size: rem_size * f.height * 0.7,
|
||||
avatar_size: rem_size * f.avatar,
|
||||
caption_size: rem_size * f.text,
|
||||
dense_padding_x: rem_size * f.padding * 0.75,
|
||||
plate_inset: rem_size * f.inset,
|
||||
strip_height: rem_size * f.strip,
|
||||
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,
|
||||
input_height: rem_size * (f.height + 0.125),
|
||||
padding_x: rem_size * f.padding,
|
||||
text_size: rem_size * f.text,
|
||||
icon_size,
|
||||
dense_icon_size: icon_size * 0.75,
|
||||
indicator_size: rem_size * f.height * 0.7,
|
||||
avatar_size: rem_size * f.avatar,
|
||||
caption_size: rem_size * f.text,
|
||||
dense_padding_x: rem_size * f.padding * 0.75,
|
||||
plate_inset: rem_size * f.inset,
|
||||
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 {
|
||||
@@ -383,6 +463,35 @@ impl SizeTable {
|
||||
self.medium.input_height = medium_input_height;
|
||||
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.
|
||||
@@ -408,6 +517,8 @@ impl Theme {
|
||||
colors: resolved.colors,
|
||||
ramps: resolved.ramps,
|
||||
radius: resolved.radius,
|
||||
borders: resolved.borders,
|
||||
spacing: resolved.spacing,
|
||||
elevations: resolved.elevations,
|
||||
motion: resolved.motion,
|
||||
scrollbar: resolved.scrollbar,
|
||||
|
||||
+152
-3
@@ -1,7 +1,9 @@
|
||||
use crate::definition::ThemeDefinition;
|
||||
use crate::ramps::{AccentShades, Oklch, contrast_foreground, derive_shades, mix};
|
||||
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;
|
||||
|
||||
/// 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 ramps: Ramps,
|
||||
pub radius: Radius,
|
||||
pub borders: Borders,
|
||||
pub spacing: Spacing,
|
||||
pub elevations: Elevations,
|
||||
pub motion: Motion,
|
||||
pub scrollbar: Scrollbar,
|
||||
@@ -198,18 +202,35 @@ pub(crate) fn resolve(
|
||||
//
|
||||
// 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(control_height) = system.control_height {
|
||||
sizes.set_control_height(control_height);
|
||||
}
|
||||
|
||||
if let Some(input_height) = system.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.
|
||||
let titlebar_height = system
|
||||
.titlebar_height
|
||||
.unwrap_or_else(|| sizes.medium.control_height.max(px(TITLEBAR_HEIGHT_MIN_PX)));
|
||||
|
||||
let mut radius = Radius::default();
|
||||
|
||||
if let Some(control) = preset
|
||||
.and_then(|def| def.control_radius)
|
||||
.map(px)
|
||||
@@ -218,6 +239,22 @@ pub(crate) fn resolve(
|
||||
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 {
|
||||
variant,
|
||||
ramps: Ramps {
|
||||
@@ -238,6 +275,10 @@ pub(crate) fn resolve(
|
||||
},
|
||||
colors,
|
||||
radius,
|
||||
borders: Borders::for_platform(system.source),
|
||||
spacing: Spacing {
|
||||
unit: system.source.spacing_unit(),
|
||||
},
|
||||
elevations: Elevations::for_platform(system.source, variant),
|
||||
motion: Motion::default(),
|
||||
scrollbar: Scrollbar {
|
||||
@@ -283,7 +324,14 @@ mod tests {
|
||||
accent,
|
||||
palette,
|
||||
control_radius: None,
|
||||
overlay_radius: None,
|
||||
sm_radius: None,
|
||||
control_height: None,
|
||||
input_height: None,
|
||||
menu_item_height: None,
|
||||
switch_height: None,
|
||||
switch_width: None,
|
||||
switch_knob: None,
|
||||
titlebar_height: None,
|
||||
root_font_size: None,
|
||||
accessibility: AccessibilityPreferences::default(),
|
||||
@@ -308,7 +356,10 @@ mod tests {
|
||||
fn empty_system_resolves_to_the_neutral_fallback() {
|
||||
for variant in [ThemeVariant::Light, ThemeVariant::Dark] {
|
||||
let resolved = resolve(
|
||||
&system(variant, PartialPalette::default(), None),
|
||||
&SystemTheme {
|
||||
source: SourceKind::Fallback,
|
||||
..system(variant, PartialPalette::default(), 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.plate_inset, px(4.));
|
||||
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.));
|
||||
}
|
||||
}
|
||||
@@ -396,6 +462,89 @@ mod tests {
|
||||
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]
|
||||
fn overlay_is_translucent_and_overridable() {
|
||||
let light = resolve(
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use gpui::Hsla;
|
||||
use gpui::{Hsla, Pixels, px};
|
||||
|
||||
use crate::system::{PartialPalette, SourceKind, SystemTheme};
|
||||
|
||||
@@ -14,6 +14,9 @@ pub(crate) struct KdeScheme {
|
||||
pub(crate) accent: Option<Hsla>,
|
||||
pub(crate) background: Option<Hsla>,
|
||||
pub(crate) palette: PartialPalette,
|
||||
/// The root font size parsed from `[General] font=` (family, pointSize,
|
||||
/// pixelSize, ...).
|
||||
pub(crate) root_font_size: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl KdeScheme {
|
||||
@@ -54,6 +57,7 @@ impl KdeScheme {
|
||||
accent,
|
||||
background,
|
||||
palette,
|
||||
root_font_size: root_font_size(groups.get("General")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,10 +81,17 @@ impl KdeScheme {
|
||||
.unwrap_or(crate::ThemeVariant::Light),
|
||||
accent: self.accent.or(portal_accent),
|
||||
palette: self.palette,
|
||||
control_radius: None,
|
||||
input_height: None,
|
||||
titlebar_height: None,
|
||||
root_font_size: None,
|
||||
control_radius: Some(px(5.)),
|
||||
overlay_radius: Some(px(5.)),
|
||||
sm_radius: 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,
|
||||
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>> {
|
||||
let mut groups: HashMap<&str, HashMap<&str, &str>> = HashMap::new();
|
||||
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));
|
||||
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 gpui::Hsla;
|
||||
use gpui::{Hsla, Pixels};
|
||||
use notify::{RecursiveMode, Watcher};
|
||||
|
||||
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)]
|
||||
struct GSettingsSnapshot {
|
||||
variant: Option<ThemeVariant>,
|
||||
accent: Option<Hsla>,
|
||||
root_font_size: Option<Pixels>,
|
||||
}
|
||||
|
||||
fn gsettings_value(args: &[&str]) -> Option<String> {
|
||||
@@ -97,23 +98,31 @@ fn read_gsettings() -> Option<GSettingsSnapshot> {
|
||||
"prefer-light" => Some(ThemeVariant::Light),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let accent = gsettings_value(&["get", "org.gnome.desktop.interface", "accent-color"])
|
||||
.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;
|
||||
}
|
||||
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> {
|
||||
let portal = portal::io::PortalClient::connect();
|
||||
|
||||
let gsettings = if portal.is_none() {
|
||||
read_gsettings()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let gsettings = read_gsettings();
|
||||
|
||||
let backend = home_dir()
|
||||
.map(|home| detect(&home, portal.is_some(), gsettings.is_some()))
|
||||
@@ -129,6 +138,36 @@ pub(crate) fn platform_source() -> Box<dyn ThemeSource> {
|
||||
}
|
||||
|
||||
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(
|
||||
&self,
|
||||
variant: ThemeVariant,
|
||||
@@ -136,31 +175,21 @@ impl LinuxSource {
|
||||
accessibility: crate::system::AccessibilityPreferences,
|
||||
) -> SystemTheme {
|
||||
if let Some(appearance) = portal_appearance {
|
||||
return SystemTheme {
|
||||
variant: appearance.variant(variant),
|
||||
accent: appearance.accent,
|
||||
palette: PartialPalette::default(),
|
||||
control_radius: None,
|
||||
input_height: None,
|
||||
titlebar_height: None,
|
||||
root_font_size: None,
|
||||
return self.gnome_snapshot(
|
||||
appearance.variant(variant),
|
||||
appearance.accent,
|
||||
accessibility,
|
||||
source: SourceKind::Gnome,
|
||||
};
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(gsettings) = self.gsettings {
|
||||
return SystemTheme {
|
||||
variant: gsettings.variant.unwrap_or(variant),
|
||||
accent: gsettings.accent,
|
||||
palette: PartialPalette::default(),
|
||||
control_radius: None,
|
||||
input_height: None,
|
||||
titlebar_height: None,
|
||||
root_font_size: None,
|
||||
return self.gnome_snapshot(
|
||||
gsettings.variant.unwrap_or(variant),
|
||||
gsettings.accent,
|
||||
accessibility,
|
||||
source: SourceKind::Gnome,
|
||||
};
|
||||
);
|
||||
}
|
||||
|
||||
SystemTheme::fallback(variant)
|
||||
}
|
||||
}
|
||||
@@ -289,4 +318,52 @@ mod tests {
|
||||
assert_eq!(detect(&home, true, false), Backend::Kde);
|
||||
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;
|
||||
/// The rounded-bezel text field's intrinsic height at regular size.
|
||||
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).
|
||||
const ROOT_FONT_SIZE_PTS: f32 = 13.0;
|
||||
|
||||
@@ -112,8 +122,15 @@ impl ThemeSource for MacosSource {
|
||||
accent: Self::color_to_hsla(&NSColor::controlAccentColor()),
|
||||
palette,
|
||||
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)),
|
||||
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)),
|
||||
accessibility,
|
||||
source: SourceKind::Macos,
|
||||
|
||||
@@ -60,8 +60,15 @@ impl OmarchyTheme {
|
||||
variant: self.variant,
|
||||
accent: self.accent,
|
||||
palette: self.palette,
|
||||
control_radius: None,
|
||||
input_height: None,
|
||||
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: None,
|
||||
root_font_size: None,
|
||||
accessibility,
|
||||
@@ -215,6 +222,23 @@ color15 = \"#635d6f\"
|
||||
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]
|
||||
fn non_toml_text_is_rejected() {
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
const TITLEBAR_HEIGHT_PX: f32 = 32.0;
|
||||
/// WinUI 11's default body text size, in pixels at 100% scaling.
|
||||
@@ -159,7 +169,14 @@ impl ThemeSource for WindowsSource {
|
||||
..PartialPalette::default()
|
||||
},
|
||||
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)),
|
||||
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)),
|
||||
root_font_size: Some(gpui::px(ROOT_FONT_SIZE_PX * text_scale as f32)),
|
||||
accessibility: self.accessibility(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! The normalized snapshot a platform adapter produces.
|
||||
|
||||
use gpui::{Hsla, Pixels};
|
||||
use gpui::{Hsla, Pixels, px};
|
||||
|
||||
use crate::ThemeVariant;
|
||||
|
||||
@@ -43,6 +43,17 @@ impl SourceKind {
|
||||
pub fn scrollbar_autohide(self) -> bool {
|
||||
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.
|
||||
@@ -83,8 +94,22 @@ pub struct SystemTheme {
|
||||
pub palette: PartialPalette,
|
||||
/// The platform's stated control corner radius, if it has one.
|
||||
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.
|
||||
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.
|
||||
pub titlebar_height: Option<Pixels>,
|
||||
/// The platform's native root font size, if it has one.
|
||||
@@ -100,7 +125,14 @@ impl SystemTheme {
|
||||
accent: None,
|
||||
palette: PartialPalette::default(),
|
||||
control_radius: None,
|
||||
overlay_radius: None,
|
||||
sm_radius: None,
|
||||
control_height: None,
|
||||
input_height: None,
|
||||
menu_item_height: None,
|
||||
switch_height: None,
|
||||
switch_width: None,
|
||||
switch_knob: None,
|
||||
titlebar_height: None,
|
||||
root_font_size: None,
|
||||
accessibility: AccessibilityPreferences::default(),
|
||||
|
||||
+17
-8
@@ -9,18 +9,20 @@ is the first downstream consumer; its screens drive the order.
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
scroll handle, promoted from the editor views' pattern over gpui's
|
||||
`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
|
||||
(portal), KDE (kdeglobals), Omarchy (colors.toml), neutral fallback;
|
||||
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`)
|
||||
|
||||
|
||||
@@ -229,6 +229,10 @@ density is frozen at a macOS-ish tuning.
|
||||
|
||||
## 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:
|
||||
Option<Pixels>` beside `input_height` on `SystemTheme`, reuse the
|
||||
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);
|
||||
Windows stays stated.
|
||||
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
|
||||
item min-height (GNOME 32), and the hardcoded `min_w_`/`max_w` values
|
||||
through it.
|
||||
unit (GNOME 6, KDE 4/6/8, others 4); route menu/popover paddings and menu
|
||||
row min-heights through it — GNOME 32 (published), Windows ≈33 (padding
|
||||
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
|
||||
40×20 with 12px knob, KDE 36×18; icon 16 on macOS/Windows.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user