use crate::definition::ThemeDefinition; use crate::ramps::{AccentShades, Oklch, contrast_foreground, derive_shades, mix}; use crate::system::SystemTheme; 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 /// macOS and Linux titlebars draw at regardless of the control scale. const TITLEBAR_HEIGHT_MIN_PX: f32 = 34.0; /// Derived state shades for the colors that get pressed and hovered. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Ramps { pub primary: AccentShades, pub secondary: AccentShades, pub destructive: AccentShades, pub success: AccentShades, pub warning: AccentShades, pub info: AccentShades, } /// Everything a component paints from, after the overlay chain. #[derive(Clone, Copy, Debug, PartialEq)] pub struct ResolvedTheme { pub variant: ThemeVariant, pub colors: ColorTokens, pub ramps: Ramps, pub radius: Radius, pub borders: Borders, pub spacing: Spacing, pub elevations: Elevations, pub motion: Motion, pub scrollbar: Scrollbar, /// The height the window's titlebar strips draw at, per the platform. pub titlebar_height: gpui::Pixels, /// The platform's native root font size; the fallback theme uses 16. pub root_font_size: gpui::Pixels, /// The platform's monospace family, for code surfaces. pub mono_font: &'static str, pub sizes: SizeTable, pub accessibility: crate::system::AccessibilityPreferences, } /// Overlay order (later wins): neutral base → system → preset definition. pub(crate) fn resolve( system: &SystemTheme, preset: Option<&ThemeDefinition>, mode_override: Option, ) -> ResolvedTheme { let variant = mode_override .or(preset.and_then(|def| def.mode)) .unwrap_or(system.variant); let base = match variant { ThemeVariant::Light => ColorTokens::light(), ThemeVariant::Dark => ColorTokens::dark(), }; let system_palette = system.palette; let pick = |def: Option, system: Option, base: gpui::Hsla| { def.or(system).unwrap_or(base) }; let def_color = |field: fn(&ThemeDefinition) -> Option| preset.and_then(field); let accent = pick(def_color(|d| d.accent()), system.accent, base.accent); let primary = pick(def_color(|d| d.primary()), system.accent, base.primary); let destructive = pick( def_color(|d| d.destructive()), system_palette.destructive, base.destructive, ); let success = pick( def_color(|d| d.success()), system_palette.success, base.success, ); let warning = pick( def_color(|d| d.warning()), system_palette.warning, base.warning, ); let info = pick(def_color(|d| d.info()), system_palette.info, base.info); let mut colors = ColorTokens { background: pick( def_color(|d| d.background()), system_palette.background, base.background, ), foreground: pick( def_color(|d| d.foreground()), system_palette.foreground, base.foreground, ), surface: pick( def_color(|d| d.surface()), system_palette.surface, base.surface, ), surface_foreground: pick( def_color(|d| d.surface_foreground()), system_palette.surface_foreground, base.surface_foreground, ), primary, primary_foreground: pick( def_color(|d| d.primary_foreground()), system_palette.primary_foreground, contrast_foreground(primary), ), secondary: pick( def_color(|d| d.secondary()), system_palette.secondary, base.secondary, ), secondary_foreground: pick( def_color(|d| d.secondary_foreground()), system_palette.secondary_foreground, base.secondary_foreground, ), muted: pick(def_color(|d| d.muted()), system_palette.muted, base.muted), muted_hover: pick( def_color(|d| d.muted_hover()), system_palette.muted_hover, base.muted_hover, ), muted_foreground: pick( def_color(|d| d.muted_foreground()), system_palette.muted_foreground, base.muted_foreground, ), accent, accent_foreground: pick( def_color(|d| d.accent_foreground()), system_palette.accent_foreground, contrast_foreground(accent), ), destructive, destructive_foreground: pick( def_color(|d| d.destructive_foreground()), None, contrast_foreground(destructive), ), success, success_foreground: pick( def_color(|d| d.success_foreground()), None, contrast_foreground(success), ), warning, warning_foreground: pick( def_color(|d| d.warning_foreground()), None, contrast_foreground(warning), ), info, info_foreground: pick( def_color(|d| d.info_foreground()), None, contrast_foreground(info), ), border: pick( def_color(|d| d.border()), system_palette.border, base.border, ), input: pick(def_color(|d| d.input()), system_palette.input, base.input), ring: pick(def_color(|d| d.ring()), system_palette.ring, base.ring), selection: pick( def_color(|d| d.selection()), system_palette.selection, base.selection, ), overlay: pick( def_color(|d| d.overlay()), system_palette.overlay, base.overlay, ), scrollbar_thumb: pick( def_color(|d| d.scrollbar_thumb()), system_palette.scrollbar_thumb, base.scrollbar_thumb, ), }; let accessibility = system.accessibility; if accessibility.high_contrast { colors.border = mix( Oklch::from_hsla(colors.border), Oklch::from_hsla(colors.foreground), 0.35, ) .to_hsla(); } // The platform's root font size scales every control metric: // a medium control's label is exactly the root font size. // // Fallback: 16 (the web default gpui also uses for rems). let root_font_size = system.root_font_size.unwrap_or(px(16.)); let mut sizes = SizeTable::with_root_font_size(root_font_size); if let Some(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) .or(system.control_radius) { 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 { primary: derive_shades(primary, colors.background, variant == ThemeVariant::Dark), secondary: derive_shades( colors.secondary, colors.background, variant == ThemeVariant::Dark, ), destructive: derive_shades( destructive, colors.background, variant == ThemeVariant::Dark, ), success: derive_shades(success, colors.background, variant == ThemeVariant::Dark), warning: derive_shades(warning, colors.background, variant == ThemeVariant::Dark), info: derive_shades(info, colors.background, variant == ThemeVariant::Dark), }, colors, radius, borders: Borders::for_platform(system.source, variant), spacing: Spacing { unit: system.source.spacing_unit(), }, elevations: Elevations::for_platform(system.source, variant), motion: Motion::default(), scrollbar: Scrollbar { autohide: system.source.scrollbar_autohide(), ..Scrollbar::default() }, titlebar_height, root_font_size, mono_font: mono_font(), sizes, accessibility, } } /// The platform's monospace family, for code surfaces. fn mono_font() -> &'static str { if cfg!(target_os = "macos") { "Menlo" } else if cfg!(target_os = "windows") { "Consolas" } else { "monospace" } } #[cfg(test)] mod tests { use super::*; use crate::system::{AccessibilityPreferences, PartialPalette, SourceKind}; use gpui::{Rgba, rgb}; fn hsla(hex: u32) -> gpui::Hsla { gpui::Hsla::from(rgb(hex)) } fn system( variant: ThemeVariant, palette: PartialPalette, accent: Option, ) -> SystemTheme { SystemTheme { variant, 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(), source: SourceKind::Macos, } } #[test] fn empty_system_resolves_to_the_neutral_fallback() { for variant in [ThemeVariant::Light, ThemeVariant::Dark] { let resolved = resolve( &SystemTheme { source: SourceKind::Fallback, ..system(variant, PartialPalette::default(), None) }, None, None, ); let base = match variant { ThemeVariant::Light => ColorTokens::light(), ThemeVariant::Dark => ColorTokens::dark(), }; assert_eq!(resolved.colors.background, base.background); assert_eq!(resolved.colors.accent, base.accent); assert_eq!(resolved.ramps.primary.base, base.primary); assert_eq!(resolved.ramps.secondary.base, base.secondary); assert_eq!(resolved.radius.sm, px(4.)); assert_eq!(resolved.radius.control, px(6.)); assert_eq!(resolved.root_font_size, px(16.)); assert_eq!(resolved.sizes.medium.control_height, px(28.)); assert_eq!(resolved.sizes.medium.input_height, px(30.)); assert_eq!(resolved.sizes.medium.text_size, px(16.)); assert_eq!(resolved.sizes.large.avatar_size, px(48.)); assert_eq!(resolved.sizes.small.caption_size, px(13.6 * 0.85)); assert_eq!(resolved.sizes.medium.caption_size, px(16.)); assert_eq!(resolved.sizes.large.caption_size, px(16.)); 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.)); } } #[test] fn a_stated_titlebar_height_wins_over_the_control_scale() { let mut stated = system(ThemeVariant::Dark, PartialPalette::default(), None); stated.titlebar_height = Some(px(32.)); assert_eq!(resolve(&stated, None, None).titlebar_height, px(32.)); } #[test] fn a_stated_root_font_size_scales_the_size_table() { // macOS states 13pt; the medium control shrinks to native proportions. let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None); stated.root_font_size = Some(px(13.)); let resolved = resolve(&stated, None, None); assert_eq!(resolved.root_font_size, px(13.)); assert_eq!(resolved.sizes.medium.control_height, px(22.75)); assert_eq!(resolved.sizes.medium.text_size, px(13.)); assert_eq!(resolved.sizes.small.text_size, px(11.05)); } #[test] fn scrollbar_autohide_follows_the_platform_source() { let mut stated = system(ThemeVariant::Dark, PartialPalette::default(), None); stated.source = SourceKind::Macos; assert!(resolve(&stated, None, None).scrollbar.autohide); stated.source = SourceKind::Gnome; assert!(resolve(&stated, None, None).scrollbar.autohide); stated.source = SourceKind::Omarchy; assert!(resolve(&stated, None, None).scrollbar.autohide); stated.source = SourceKind::Windows; assert!(!resolve(&stated, None, None).scrollbar.autohide); stated.source = SourceKind::Kde; assert!(!resolve(&stated, None, None).scrollbar.autohide); stated.source = SourceKind::Fallback; assert!(!resolve(&stated, None, None).scrollbar.autohide); } #[test] fn a_stated_input_height_overrides_the_derived_one_and_scales() { let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None); stated.input_height = Some(px(24.)); let resolved = resolve(&stated, None, None); assert_eq!(resolved.sizes.medium.input_height, px(24.)); let derived = SizeTable::with_root_font_size(px(16.)); let scale = px(24.) / derived.medium.input_height; let expected_small = (derived.small.input_height * scale).floor(); assert_eq!(resolved.sizes.small.input_height, expected_small); } #[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; let expected_small = (derived.small.control_height * scale).floor(); let expected_large = (derived.large.control_height * scale).floor(); assert_eq!(resolved.sizes.small.control_height, expected_small); assert_eq!(resolved.sizes.large.control_height, expected_large); } #[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 dark_mode_separates_overlays_by_border() { for source in [SourceKind::Macos, SourceKind::Gnome, SourceKind::Omarchy] { let mut stated = system(ThemeVariant::Dark, PartialPalette::default(), None); stated.source = source; let resolved = resolve(&stated, None, None); assert!( resolved.borders.overlays, "{source:?} frames floating layers in dark" ); assert!(resolved.elevations.popover.shadows().is_empty()); assert!(resolved.elevations.dialog.shadows().is_empty()); } } #[test] fn overlay_is_translucent_and_overridable() { let light = resolve( &system(ThemeVariant::Light, PartialPalette::default(), None), None, None, ); let dark = resolve( &system(ThemeVariant::Dark, PartialPalette::default(), None), None, None, ); // Both scrims are translucent black; dark is denser for contrast. assert!(light.colors.overlay.a > 0.0 && light.colors.overlay.a < 0.5); assert!(dark.colors.overlay.a > light.colors.overlay.a); let def = ThemeDefinition::parse_toml("overlay = \"#000000\"").unwrap(); let overridden = resolve( &system(ThemeVariant::Light, PartialPalette::default(), None), Some(&def), None, ); assert_eq!(overridden.colors.overlay, hsla(0x000000)); assert_eq!(overridden.colors.overlay.a, 1.0); } #[test] fn elevations_and_motion_resolve_with_defaults() { let resolved = resolve( &system(ThemeVariant::Light, PartialPalette::default(), None), None, None, ); // The popover shadow is softer and tighter than the dialog's. assert!( resolved.elevations.popover.layers[0].blur_radius < resolved.elevations.dialog.layers[0].blur_radius ); assert!(resolved.motion.overlay > std::time::Duration::ZERO); let shadows = resolved.elevations.popover.shadows(); assert_eq!(shadows.len(), 2); assert!(!shadows[0].inset); } #[test] fn elevations_follow_the_platform_in_light_mode_and_share_one_dark_stack() { let popover = |source| { let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None); stated.source = source; resolve(&stated, None, None).elevations.popover }; // macOS paints a soft layered stack; Linux keeps a compact single one. assert_eq!(popover(SourceKind::Macos).shadows().len(), 2); assert_eq!(popover(SourceKind::Gnome).shadows().len(), 1); assert!( popover(SourceKind::Macos).layers[0].color.a < popover(SourceKind::Gnome).layers[0].color.a ); // Dark mode is one shared stack, however the adapter detected. let dark = |source| { let mut stated = system(ThemeVariant::Dark, PartialPalette::default(), None); stated.source = source; resolve(&stated, None, None).elevations }; assert_eq!(dark(SourceKind::Macos), dark(SourceKind::Windows)); assert_eq!(dark(SourceKind::Macos), dark(SourceKind::Fallback)); } #[test] fn system_colors_override_the_base_but_leave_the_rest_intact() { let palette = PartialPalette { background: Some(hsla(0x112233)), border: Some(hsla(0x445566)), ..PartialPalette::default() }; let resolved = resolve( &system(ThemeVariant::Dark, palette, Some(hsla(0xff8800))), None, None, ); assert_eq!(resolved.colors.background, hsla(0x112233)); assert_eq!(resolved.colors.border, hsla(0x445566)); assert_eq!(resolved.colors.accent, hsla(0xff8800)); assert_eq!(resolved.colors.foreground, ColorTokens::dark().foreground); } #[test] fn a_system_stated_accent_foreground_beats_the_derivation() { // What macOS reports via alternateSelectedControlTextColor: white, // even where the derivation would pick black (light accent). let palette = PartialPalette { accent_foreground: Some(hsla(0xffffff)), ..PartialPalette::default() }; let resolved = resolve( &system(ThemeVariant::Light, palette, Some(hsla(0xffd60a))), None, None, ); assert_eq!(resolved.colors.accent, hsla(0xffd60a)); assert_eq!(resolved.colors.accent_foreground, hsla(0xffffff)); // Without the stated foreground the derivation applies. let derived = resolve( &system( ThemeVariant::Light, PartialPalette::default(), Some(hsla(0xffd60a)), ), None, None, ); assert_eq!(derived.colors.accent_foreground, hsla(0x000000)); } #[test] fn preset_beats_system_and_the_mode_override_beats_everything() { let def = ThemeDefinition::parse_toml( "mode = \"dark\"\nbackground = \"#1a1b26\"\naccent = \"#7aa2f7\"\ncontrol_radius = 12.0\n", ) .unwrap(); let palette = PartialPalette { background: Some(hsla(0xffffff)), ..PartialPalette::default() }; let resolved = resolve( &system(ThemeVariant::Light, palette, None), Some(&def), None, ); assert_eq!(resolved.variant, ThemeVariant::Dark); // Compare in RGB space: Hsla round-trips are lossy at low saturation. let bg = Rgba::from(resolved.colors.background); assert!((bg.r - 0x1a as f32 / 255.0).abs() < 0.001); assert!((bg.b - 0x26 as f32 / 255.0).abs() < 0.001); let accent = Rgba::from(resolved.colors.accent); assert!((accent.r - 0x7a as f32 / 255.0).abs() < 0.001); // And the metric override beats the radius default. assert_eq!(resolved.radius.control, px(12.)); let forced = resolve( &system(ThemeVariant::Light, palette, None), Some(&def), Some(ThemeVariant::Light), ); assert_eq!(forced.variant, ThemeVariant::Light); // The override flips the variant (and with it the base roles), but // the preset's explicit background still wins over the base. let forced_bg = Rgba::from(forced.colors.background); assert!((forced_bg.r - 0x1a as f32 / 255.0).abs() < 0.001); } #[test] fn an_accent_only_override_derives_foregrounds_and_ramps() { let def = ThemeDefinition::parse_toml("mode = \"dark\"\naccent = \"#7aa2f7\"\n").unwrap(); let resolved = resolve( &system(ThemeVariant::Dark, PartialPalette::default(), None), Some(&def), None, ); // The definition sets accent only; primary still falls back to the // dark base. accent_foreground/primary_foreground are derived per // color, and the ramp tracks its own base. let accent = Rgba::from(resolved.colors.accent); assert!((accent.r - 0x7a as f32 / 255.0).abs() < 0.001); assert_eq!( resolved.colors.primary, ColorTokens::dark().primary, "primary should stay at the base until a definition names it" ); assert_eq!(resolved.ramps.primary.base, resolved.colors.primary); assert_eq!(resolved.ramps.destructive.base, resolved.colors.destructive); assert_ne!(resolved.ramps.primary.hover, resolved.ramps.primary.base); assert_ne!(resolved.ramps.primary.subtle, resolved.ramps.primary.base); } #[test] fn system_accent_becomes_primary() { let resolved = resolve( &system( ThemeVariant::Dark, PartialPalette::default(), Some(hsla(0x7aa2f7)), ), None, None, ); let primary = Rgba::from(resolved.colors.primary); assert!((primary.r - 0x7a as f32 / 255.0).abs() < 0.001); assert_eq!(resolved.ramps.primary.base, resolved.colors.primary); assert_eq!( resolved.colors.primary_foreground, resolved.colors.accent_foreground ); } #[test] fn high_contrast_sharpens_the_border_toward_the_foreground() { let mut a11y = AccessibilityPreferences::default(); let plain = resolve( &system(ThemeVariant::Dark, PartialPalette::default(), None), None, None, ); a11y.high_contrast = true; let contrasted = resolve( &SystemTheme { accessibility: a11y, ..system(ThemeVariant::Dark, PartialPalette::default(), None) }, None, None, ); let plain_border = Oklch::from_hsla(plain.colors.border); let contrasted_border = Oklch::from_hsla(contrasted.colors.border); let foreground_l = Oklch::from_hsla(plain.colors.foreground).l; assert!((contrasted_border.l - foreground_l).abs() < (plain_border.l - foreground_l).abs()); } #[test] fn every_resolved_role_is_distinct_enough_to_paint_with() { for variant in [ThemeVariant::Light, ThemeVariant::Dark] { let resolved = resolve( &system(variant, PartialPalette::default(), None), None, None, ); assert_ne!(resolved.colors.background, resolved.colors.foreground); assert_ne!(resolved.colors.border, resolved.colors.background); assert_ne!(resolved.colors.muted_foreground, resolved.colors.foreground); assert!(resolved.colors.ring != resolved.colors.background); } } #[test] fn info_stays_calm_when_the_accent_warms_up() { // The whole point of the info role: a warm platform accent (GNOME // red, Omarchy red themes) must not make Info toasts read as Danger. let red_accent = hsla(0xdc2626); let resolved = resolve( &system( ThemeVariant::Dark, PartialPalette::default(), Some(red_accent), ), None, None, ); assert_eq!(resolved.ramps.primary.base, red_accent); assert_ne!(resolved.colors.info, resolved.colors.destructive); assert_ne!(resolved.ramps.info.base, resolved.ramps.destructive.base); } #[test] fn info_follows_the_severity_overlay_chain() { let palette = PartialPalette { info: Some(hsla(0x155e75)), ..PartialPalette::default() }; let system_stated = resolve(&system(ThemeVariant::Dark, palette, None), None, None); assert_eq!(system_stated.colors.info, hsla(0x155e75)); assert_eq!(system_stated.ramps.info.base, hsla(0x155e75)); let def = ThemeDefinition::parse_toml("info = \"#0ea5e9\"\n").unwrap(); let preset_stated = resolve(&system(ThemeVariant::Dark, palette, None), Some(&def), None); assert_eq!(preset_stated.colors.info, hsla(0x0ea5e9)); } }