599 lines
22 KiB
Rust
599 lines
22 KiB
Rust
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 gpui::px;
|
|
|
|
/// 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 elevations: Elevations,
|
|
pub motion: Motion,
|
|
pub scrollbar: Scrollbar,
|
|
/// The platform's native root font size; the fallback theme uses 16.
|
|
pub root_font_size: gpui::Pixels,
|
|
pub sizes: SizeTable,
|
|
pub accessibility: crate::system::AccessibilityPreferences,
|
|
}
|
|
|
|
/// Overlay order (later wins): neutral base → system → preset definition.
|
|
pub(crate) fn resolve(
|
|
system: &SystemTheme,
|
|
preset: Option<&ThemeDefinition>,
|
|
mode_override: Option<ThemeVariant>,
|
|
) -> 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<gpui::Hsla>, system: Option<gpui::Hsla>, base: gpui::Hsla| {
|
|
def.or(system).unwrap_or(base)
|
|
};
|
|
|
|
let def_color = |field: fn(&ThemeDefinition) -> Option<gpui::Hsla>| 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_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(input_height) = system.input_height {
|
|
sizes.set_input_height(input_height);
|
|
}
|
|
|
|
let mut radius = Radius::default();
|
|
if let Some(control) = preset
|
|
.and_then(|def| def.control_radius)
|
|
.map(px)
|
|
.or(system.control_radius)
|
|
{
|
|
radius.control = control;
|
|
}
|
|
|
|
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,
|
|
elevations: Elevations::for_platform(system.source, variant),
|
|
motion: Motion::default(),
|
|
scrollbar: Scrollbar::default(),
|
|
root_font_size,
|
|
sizes,
|
|
accessibility,
|
|
}
|
|
}
|
|
|
|
#[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<gpui::Hsla>,
|
|
) -> SystemTheme {
|
|
SystemTheme {
|
|
variant,
|
|
accent,
|
|
palette,
|
|
control_radius: None,
|
|
input_height: None,
|
|
root_font_size: None,
|
|
accessibility: AccessibilityPreferences::default(),
|
|
source: SourceKind::Macos,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn metric_priority_is_definition_then_system_then_fallback() {
|
|
let mut stated = system(ThemeVariant::Dark, PartialPalette::default(), None);
|
|
stated.control_radius = Some(px(4.));
|
|
|
|
let resolved = resolve(&stated, None, None);
|
|
assert_eq!(resolved.radius.control, px(4.));
|
|
|
|
let def = ThemeDefinition::parse_toml("control_radius = 12.0\n").unwrap();
|
|
let overridden = resolve(&stated, Some(&def), None);
|
|
assert_eq!(overridden.radius.control, px(12.));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_system_resolves_to_the_neutral_fallback() {
|
|
for variant in [ThemeVariant::Light, ThemeVariant::Dark] {
|
|
let resolved = resolve(
|
|
&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.));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_stated_root_font_size_scales_the_size_table() {
|
|
// macOS states 13pt; the medium control shrinks to native proportions.
|
|
let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None);
|
|
stated.root_font_size = Some(px(13.));
|
|
let resolved = resolve(&stated, None, None);
|
|
assert_eq!(resolved.root_font_size, px(13.));
|
|
assert_eq!(resolved.sizes.medium.control_height, px(22.75));
|
|
assert_eq!(resolved.sizes.medium.text_size, px(13.));
|
|
assert_eq!(resolved.sizes.small.text_size, px(11.05));
|
|
}
|
|
|
|
#[test]
|
|
fn a_stated_input_height_overrides_the_derived_one_and_scales() {
|
|
let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None);
|
|
stated.input_height = Some(px(24.));
|
|
let resolved = resolve(&stated, None, None);
|
|
|
|
// The medium class takes the stated height verbatim, and the other
|
|
// classes keep their derived ratio to the medium class.
|
|
assert_eq!(resolved.sizes.medium.input_height, px(24.));
|
|
|
|
// Derive what small would be without the statement, scale it by the
|
|
// stated-to-derived ratio, and compare.
|
|
let derived = SizeTable::with_root_font_size(px(16.));
|
|
let scale = px(24.) / derived.medium.input_height;
|
|
let expected_small = derived.small.input_height * scale;
|
|
assert!((resolved.sizes.small.input_height - expected_small).abs() < px(0.01));
|
|
}
|
|
|
|
#[test]
|
|
fn 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::Dark, 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(), 1);
|
|
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));
|
|
}
|
|
}
|