use crate::geometry::Insets;
use crate::style::{Color, Font, Rgba, Weight};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OsFamily {
MacOs,
Windows,
Gnome,
}
impl OsFamily {
pub fn ui_font_families(&self) -> &'static [&'static str] {
match self {
OsFamily::MacOs => &[
"SF Pro Text",
"SF Pro",
".AppleSystemUIFont",
"Helvetica Neue",
],
OsFamily::Windows => &["Segoe UI"],
OsFamily::Gnome => &["Cantarell", "Ubuntu"],
}
}
pub fn fallback_font_families(&self) -> &'static [&'static str] {
match self {
OsFamily::MacOs => &["DejaVu Sans", "Liberation Sans", "Noto Sans", "Arial"],
OsFamily::Windows => &["Liberation Sans", "DejaVu Sans", "Noto Sans", "Arial"],
OsFamily::Gnome => &["Cantarell", "Ubuntu", "DejaVu Sans", "Liberation Sans"],
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ThemeMode {
#[default]
Auto,
Light,
Dark,
}
impl ThemeMode {
fn is_dark(self, system_is_dark: bool) -> bool {
match self {
ThemeMode::Auto => system_is_dark,
ThemeMode::Light => false,
ThemeMode::Dark => true,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Preset {
OldSchoolTerminal,
HighContrast,
Solarized,
Nord,
}
impl Preset {
pub fn theme(self) -> Theme {
match self {
Preset::OldSchoolTerminal => Theme {
background: Color::rgb(0, 0, 0),
label: Color::rgb(51, 255, 51),
secondary_label: Color::rgb(0, 170, 0),
accent: Color::rgb(0, 200, 0),
separator: Color::rgb(0, 90, 0),
row_highlight: Color::Accent,
row_font: Font::mono(13.0, Weight::Regular),
header_font: Font::mono(13.0, Weight::Bold),
row_height: 22.0,
corner_radius: 0.0,
padding: Insets::symmetric(6.0, 4.0),
column_gap: 10.0,
},
Preset::HighContrast => Theme {
background: Color::rgb(0, 0, 0),
label: Color::rgb(255, 255, 255),
secondary_label: Color::rgb(220, 220, 220),
accent: Color::rgb(255, 255, 0),
separator: Color::rgb(255, 255, 255),
row_highlight: Color::Accent,
row_font: Font::system(13.0, Weight::Regular),
header_font: Font::system(13.0, Weight::Bold),
row_height: 24.0,
corner_radius: 0.0,
padding: Insets::symmetric(8.0, 5.0),
column_gap: 12.0,
},
Preset::Solarized => Theme {
background: Color::rgb(0, 43, 54), label: Color::rgb(147, 161, 161), secondary_label: Color::rgb(88, 110, 117), accent: Color::rgb(38, 139, 210), separator: Color::rgb(7, 54, 66), row_highlight: Color::Accent,
row_font: Font::system(13.0, Weight::Regular),
header_font: Font::system(13.0, Weight::Bold),
row_height: 24.0,
corner_radius: 6.0,
padding: Insets::symmetric(8.0, 5.0),
column_gap: 12.0,
},
Preset::Nord => Theme {
background: Color::rgb(46, 52, 64), label: Color::rgb(216, 222, 233), secondary_label: Color::rgb(143, 153, 172), accent: Color::rgb(136, 192, 208), separator: Color::rgb(59, 66, 82), row_highlight: Color::Accent,
row_font: Font::system(13.0, Weight::Regular),
header_font: Font::system(13.0, Weight::Bold),
row_height: 24.0,
corner_radius: 8.0,
padding: Insets::symmetric(8.0, 5.0),
column_gap: 12.0,
},
}
}
}
#[derive(Clone, Debug)]
pub enum ThemeSource {
System(ThemeMode),
MacOs(ThemeMode),
Windows(ThemeMode),
Gnome(ThemeMode),
Preset(Preset),
Custom(Box<Theme>),
}
impl Default for ThemeSource {
fn default() -> Self {
ThemeSource::System(ThemeMode::Auto)
}
}
impl ThemeSource {
pub fn injects_system(&self) -> bool {
matches!(self, ThemeSource::System(_))
}
pub fn forced_family(&self) -> Option<OsFamily> {
match self {
ThemeSource::MacOs(_) => Some(OsFamily::MacOs),
ThemeSource::Windows(_) => Some(OsFamily::Windows),
ThemeSource::Gnome(_) => Some(OsFamily::Gnome),
ThemeSource::System(_) | ThemeSource::Preset(_) | ThemeSource::Custom(_) => None,
}
}
pub fn resolve(&self, host: OsFamily, system_is_dark: bool) -> Theme {
match self {
ThemeSource::System(mode) => Theme::for_family(host, mode.is_dark(system_is_dark)),
ThemeSource::MacOs(mode) => {
Theme::for_family(OsFamily::MacOs, mode.is_dark(system_is_dark))
}
ThemeSource::Windows(mode) => {
Theme::for_family(OsFamily::Windows, mode.is_dark(system_is_dark))
}
ThemeSource::Gnome(mode) => {
Theme::for_family(OsFamily::Gnome, mode.is_dark(system_is_dark))
}
ThemeSource::Preset(p) => p.theme(),
ThemeSource::Custom(theme) => (**theme).clone(),
}
}
}
#[derive(Clone, Debug)]
pub struct Theme {
pub background: Color,
pub label: Color,
pub secondary_label: Color,
pub accent: Color,
pub separator: Color,
pub row_highlight: Color,
pub row_font: Font,
pub header_font: Font,
pub row_height: f32,
pub corner_radius: f32,
pub padding: Insets,
pub column_gap: f32,
}
impl Default for Theme {
fn default() -> Self {
Theme::light()
}
}
const ACCENT_FALLBACK: Rgba = Rgba::opaque(0, 122, 255);
pub(crate) const MACOS_ROW_HEIGHT: f32 = 22.0;
const MACOS_MENU_FONT_SIZE: f32 = 13.0;
const MACOS_VERTICAL_INSET: f32 = 4.0;
pub(crate) const MACOS_LEADING_INSET: f32 = 14.0;
const MACOS_COLUMN_GAP: f32 = 8.0;
pub(crate) const MACOS_CORNER_RADIUS: f32 = 6.0;
pub(crate) const MACOS_SF_TRACKING_FRACTION: f32 = 0.0;
pub(crate) fn macos_sf_tracking(size: f32) -> f32 {
size * MACOS_SF_TRACKING_FRACTION
}
const SEGOE_UI_TRACKING: f32 = 0.0;
const CANTARELL_TRACKING: f32 = 0.0;
impl Theme {
pub fn light() -> Self {
Theme {
background: Color::rgb(246, 246, 246),
label: Color::rgb(0, 0, 0),
secondary_label: Color::rgb(140, 140, 140),
accent: Color::Accent,
separator: Color::rgb(210, 210, 210),
row_highlight: Color::Accent,
row_font: Font::system(13.0, Weight::Regular),
header_font: Font::system(13.0, Weight::Bold),
row_height: 22.0,
corner_radius: 8.0,
padding: Insets::symmetric(6.0, 5.0),
column_gap: 10.0,
}
}
pub fn dark() -> Self {
Theme {
background: Color::rgb(40, 40, 40),
label: Color::rgb(255, 255, 255),
secondary_label: Color::rgb(150, 150, 150),
separator: Color::rgb(70, 70, 70),
..Theme::light()
}
}
pub fn native() -> Self {
Theme {
background: Color::Rgba(246, 246, 246, 209),
..Theme::light()
}
}
pub fn native_dark() -> Self {
Theme {
background: Color::Rgba(40, 40, 40, 204),
..Theme::dark()
}
}
pub fn macos(dark: bool) -> Theme {
let base = if dark { Theme::dark() } else { Theme::light() };
Theme {
background: if dark {
Color::Rgba(40, 40, 40, 204)
} else {
Color::Rgba(246, 246, 246, 209)
},
corner_radius: MACOS_CORNER_RADIUS,
row_height: MACOS_ROW_HEIGHT,
padding: Insets::symmetric(MACOS_LEADING_INSET, MACOS_VERTICAL_INSET),
column_gap: MACOS_COLUMN_GAP,
row_font: Font::system(MACOS_MENU_FONT_SIZE, Weight::Regular)
.with_letter_spacing(macos_sf_tracking(MACOS_MENU_FONT_SIZE))
.with_optical_size(MACOS_MENU_FONT_SIZE),
header_font: Font::system(MACOS_MENU_FONT_SIZE, Weight::Bold)
.with_letter_spacing(macos_sf_tracking(MACOS_MENU_FONT_SIZE))
.with_optical_size(MACOS_MENU_FONT_SIZE),
..base
}
}
pub fn windows(dark: bool) -> Theme {
let base = if dark { Theme::dark() } else { Theme::light() };
Theme {
background: if dark {
Color::Rgba(43, 43, 43, 214)
} else {
Color::Rgba(249, 249, 249, 214)
},
corner_radius: 8.0,
row_height: 28.0,
padding: Insets::symmetric(6.0, 4.0),
column_gap: 12.0,
row_font: Font::system(14.0, Weight::Regular).with_letter_spacing(SEGOE_UI_TRACKING),
header_font: Font::system(14.0, Weight::Bold).with_letter_spacing(SEGOE_UI_TRACKING),
..base
}
}
pub fn gnome(dark: bool) -> Theme {
if dark {
Theme {
background: Color::rgb(48, 48, 48),
label: Color::rgb(255, 255, 255),
secondary_label: Color::rgb(154, 153, 150),
separator: Color::rgb(61, 61, 61),
accent: Color::Accent,
row_highlight: Color::Accent,
row_font: Font::system(11.0, Weight::Regular)
.with_letter_spacing(CANTARELL_TRACKING),
header_font: Font::system(11.0, Weight::Bold)
.with_letter_spacing(CANTARELL_TRACKING),
row_height: 30.0,
corner_radius: 12.0,
padding: Insets::symmetric(6.0, 6.0),
column_gap: 12.0,
}
} else {
Theme {
background: Color::rgb(250, 250, 250),
label: Color::rgb(0, 0, 0),
secondary_label: Color::rgb(119, 118, 123),
separator: Color::rgb(226, 226, 226),
accent: Color::Accent,
row_highlight: Color::Accent,
row_font: Font::system(11.0, Weight::Regular)
.with_letter_spacing(CANTARELL_TRACKING),
header_font: Font::system(11.0, Weight::Bold)
.with_letter_spacing(CANTARELL_TRACKING),
row_height: 30.0,
corner_radius: 12.0,
padding: Insets::symmetric(6.0, 6.0),
column_gap: 12.0,
}
}
}
pub fn for_family(family: OsFamily, dark: bool) -> Theme {
match family {
OsFamily::MacOs => Theme::macos(dark),
OsFamily::Windows => Theme::windows(dark),
OsFamily::Gnome => Theme::gnome(dark),
}
}
pub fn make_opaque(&mut self) {
let (r, g, b) = match self.background {
Color::Rgba(r, g, b, _) => (r, g, b),
_ => return,
};
self.background = Color::Rgba(r, g, b, 255);
}
pub fn resolve(&self, color: Color) -> Rgba {
match color {
Color::Rgba(r, g, b, a) => Rgba::new(r, g, b, a),
Color::Label => literal(self.label, Rgba::BLACK),
Color::SecondaryLabel => literal(self.secondary_label, Rgba::opaque(140, 140, 140)),
Color::Accent => literal(self.accent, ACCENT_FALLBACK),
Color::Separator => literal(self.separator, Rgba::opaque(210, 210, 210)),
Color::SystemRed => Rgba::opaque(255, 59, 48),
Color::SystemOrange => Rgba::opaque(255, 149, 0),
Color::SystemGreen => Rgba::opaque(52, 199, 89),
Color::SystemYellow => Rgba::opaque(255, 204, 0),
}
}
}
fn literal(color: Color, fallback: Rgba) -> Rgba {
match color {
Color::Rgba(r, g, b, a) => Rgba::new(r, g, b, a),
_ => fallback,
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum GutterPolicy {
#[default]
Auto,
Always,
Never,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TrailingGutterPolicy {
#[default]
Auto,
Always,
Never,
}
#[derive(Clone, Debug, Default)]
pub struct MenuOptions {
pub min_width: Option<f32>,
pub max_width: Option<f32>,
pub theme: ThemeSource,
pub gutter: GutterPolicy,
pub trailing_gutter: TrailingGutterPolicy,
}
impl MenuOptions {
pub fn theme(mut self, t: ThemeSource) -> Self {
self.theme = t;
self
}
pub fn gutter(mut self, g: GutterPolicy) -> Self {
self.gutter = g;
self
}
pub fn trailing_gutter(mut self, g: TrailingGutterPolicy) -> Self {
self.trailing_gutter = g;
self
}
pub fn min_width(mut self, w: f32) -> Self {
if w.is_nan() {
return self;
}
let w = w.max(0.0);
self.min_width = Some(w);
if let Some(max) = self.max_width {
if max < w {
self.max_width = Some(w);
}
}
self
}
pub fn max_width(mut self, w: f32) -> Self {
if w.is_nan() {
return self;
}
let mut w = w.max(0.0);
if let Some(min) = self.min_width {
if w < min {
w = min;
}
}
self.max_width = Some(w);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn literal_color_passes_through() {
let t = Theme::light();
assert_eq!(t.resolve(Color::Rgba(1, 2, 3, 4)), Rgba::new(1, 2, 3, 4));
assert_eq!(t.resolve(Color::rgb(10, 20, 30)), Rgba::opaque(10, 20, 30));
}
#[test]
fn label_resolves_per_theme() {
assert_eq!(Theme::light().resolve(Color::Label), Rgba::BLACK);
assert_eq!(Theme::dark().resolve(Color::Label), Rgba::WHITE);
}
#[test]
fn separator_differs_between_light_and_dark() {
let light = Theme::light().resolve(Color::Separator);
let dark = Theme::dark().resolve(Color::Separator);
assert_ne!(light, dark);
}
#[test]
fn per_os_themes_are_distinct() {
let mac = Theme::macos(false);
let win = Theme::windows(false);
let gnome = Theme::gnome(false);
assert_eq!(mac.corner_radius, 6.0);
assert_eq!(win.corner_radius, 8.0);
assert_eq!(gnome.corner_radius, 12.0);
assert!(win.row_height > mac.row_height);
assert!(gnome.row_height > mac.row_height);
assert!(matches!(mac.background, Color::Rgba(_, _, _, a) if a < 255));
assert!(matches!(win.background, Color::Rgba(_, _, _, a) if a < 255));
assert!(matches!(gnome.background, Color::Rgba(_, _, _, 255)));
assert_ne!(
Theme::gnome(true).resolve(Color::Label),
Theme::gnome(false).resolve(Color::Label)
);
assert_ne!(
Theme::macos(true).resolve(Color::Separator),
Theme::macos(false).resolve(Color::Separator)
);
}
#[test]
fn system_palette_applies_only_set_fields() {
use crate::platform::SystemPalette;
let mut theme = Theme::macos(false);
let base_bg = theme.background;
let pal = SystemPalette {
label: Some((10, 20, 30, 255)),
separator: Some((1, 2, 3, 40)),
..SystemPalette::default()
};
pal.apply_to(&mut theme);
assert_eq!(theme.resolve(Color::Label), Rgba::new(10, 20, 30, 255));
assert_eq!(theme.resolve(Color::Separator), Rgba::new(1, 2, 3, 40));
assert_eq!(theme.background, base_bg);
}
#[test]
fn system_colors_are_fixed_regardless_of_theme() {
assert_eq!(
Theme::light().resolve(Color::SystemRed),
Theme::dark().resolve(Color::SystemRed)
);
assert_eq!(
Theme::light().resolve(Color::SystemRed),
Rgba::opaque(255, 59, 48)
);
}
#[test]
fn accent_does_not_recurse_and_has_a_fallback() {
assert_eq!(Theme::light().resolve(Color::Accent), ACCENT_FALLBACK);
}
#[test]
fn custom_accent_is_honored() {
let mut theme = Theme::light();
theme.accent = Color::rgb(200, 0, 100);
assert_eq!(theme.resolve(Color::Accent), Rgba::opaque(200, 0, 100));
}
fn background_alpha(theme: &Theme) -> u8 {
match theme.background {
Color::Rgba(_, _, _, a) => a,
other => panic!("expected a literal Rgba background, got {other:?}"),
}
}
#[test]
fn light_and_dark_backgrounds_are_opaque() {
assert_eq!(background_alpha(&Theme::light()), 255);
assert_eq!(background_alpha(&Theme::dark()), 255);
}
#[test]
fn native_backgrounds_are_translucent() {
assert!(background_alpha(&Theme::native()) < 255);
assert!(background_alpha(&Theme::native_dark()) < 255);
}
#[test]
fn native_mirrors_light_and_dark_for_non_background_fields() {
let light = Theme::light();
let native = Theme::native();
assert_eq!(native.label, light.label);
assert_eq!(native.accent, light.accent);
assert_eq!(native.separator, light.separator);
assert_eq!(native.row_highlight, light.row_highlight);
assert_eq!(native.row_font, light.row_font);
assert_eq!(native.corner_radius, light.corner_radius);
let dark = Theme::dark();
let native_dark = Theme::native_dark();
assert_eq!(native_dark.label, dark.label);
assert_eq!(native_dark.separator, dark.separator);
}
#[test]
fn system_source_resolves_to_host_family_and_appearance() {
let dark = ThemeSource::System(ThemeMode::Auto).resolve(OsFamily::MacOs, true);
assert_eq!(dark.resolve(Color::Label), Rgba::WHITE);
assert!(matches!(dark.background, Color::Rgba(_, _, _, a) if a < 255));
let light = ThemeSource::System(ThemeMode::Auto).resolve(OsFamily::MacOs, false);
assert_eq!(light.resolve(Color::Label), Rgba::BLACK);
}
#[test]
fn theme_source_resolves() {
assert_eq!(
ThemeSource::System(ThemeMode::Auto)
.resolve(OsFamily::MacOs, true)
.resolve(Color::Label),
Rgba::WHITE
);
assert_eq!(
ThemeSource::System(ThemeMode::Auto)
.resolve(OsFamily::MacOs, false)
.resolve(Color::Label),
Rgba::BLACK
);
assert_eq!(
ThemeSource::System(ThemeMode::Dark)
.resolve(OsFamily::MacOs, false)
.resolve(Color::Label),
Rgba::WHITE
);
let win = ThemeSource::Windows(ThemeMode::Light).resolve(OsFamily::MacOs, true);
assert_eq!(win.corner_radius, 8.0);
assert!(!ThemeSource::Windows(ThemeMode::Light).injects_system());
assert!(ThemeSource::System(ThemeMode::Auto).injects_system());
}
#[test]
fn presets_render_distinct_skins() {
assert!(!ThemeSource::Preset(Preset::Nord).injects_system());
let term = Preset::OldSchoolTerminal.theme();
assert_eq!(term.resolve(Color::Label), Rgba::opaque(51, 255, 51));
assert!(matches!(term.background, Color::Rgba(0, 0, 0, 255)));
let nord = Preset::Nord.theme();
assert_ne!(nord.resolve(Color::Label), term.resolve(Color::Label));
}
#[test]
fn menu_options_builder_sets_theme_and_gutter() {
let opts = MenuOptions::default()
.theme(ThemeSource::Preset(Preset::Nord))
.gutter(GutterPolicy::Always);
assert!(matches!(opts.theme, ThemeSource::Preset(Preset::Nord)));
assert!(matches!(opts.gutter, GutterPolicy::Always));
}
#[test]
fn menu_options_trailing_gutter_defaults_to_auto() {
let opts = MenuOptions::default();
assert!(matches!(opts.trailing_gutter, TrailingGutterPolicy::Auto));
}
#[test]
fn menu_options_builder_sets_trailing_gutter() {
let opts = MenuOptions::default().trailing_gutter(TrailingGutterPolicy::Never);
assert!(matches!(opts.trailing_gutter, TrailingGutterPolicy::Never));
}
#[test]
fn menu_options_width_clamps_negative() {
let opts = MenuOptions::default().min_width(-5.0).max_width(-1.0);
assert_eq!(opts.min_width, Some(0.0));
assert_eq!(opts.max_width, Some(0.0));
}
#[test]
fn menu_options_width_rejects_nan() {
let opts = MenuOptions::default()
.min_width(f32::NAN)
.max_width(f32::NAN);
assert_eq!(opts.min_width, None);
assert_eq!(opts.max_width, None);
}
#[test]
fn menu_options_max_below_min_is_raised_to_min() {
let opts = MenuOptions::default().max_width(50.0).min_width(100.0);
assert_eq!(opts.min_width, Some(100.0));
assert_eq!(opts.max_width, Some(100.0));
}
#[test]
fn menu_options_min_above_existing_max_raises_max() {
let opts = MenuOptions::default().min_width(80.0).max_width(20.0);
assert_eq!(opts.min_width, Some(80.0));
assert_eq!(opts.max_width, Some(80.0));
}
#[test]
fn menu_options_normal_widths_pass_through() {
let opts = MenuOptions::default().min_width(100.0).max_width(300.0);
assert_eq!(opts.min_width, Some(100.0));
assert_eq!(opts.max_width, Some(300.0));
}
#[test]
fn os_family_ui_font_families_name_the_target_os_font() {
assert_eq!(OsFamily::Windows.ui_font_families(), &["Segoe UI"]);
assert_eq!(
OsFamily::MacOs.ui_font_families(),
&[
"SF Pro Text",
"SF Pro",
".AppleSystemUIFont",
"Helvetica Neue"
]
);
assert_eq!(OsFamily::Gnome.ui_font_families(), &["Cantarell", "Ubuntu"]);
assert!(!OsFamily::MacOs.fallback_font_families().is_empty());
assert!(!OsFamily::Windows.fallback_font_families().is_empty());
assert!(!OsFamily::Gnome.fallback_font_families().is_empty());
}
#[test]
fn forced_family_identifies_only_the_os_forcing_sources() {
assert_eq!(
ThemeSource::MacOs(ThemeMode::Auto).forced_family(),
Some(OsFamily::MacOs)
);
assert_eq!(
ThemeSource::Windows(ThemeMode::Auto).forced_family(),
Some(OsFamily::Windows)
);
assert_eq!(
ThemeSource::Gnome(ThemeMode::Auto).forced_family(),
Some(OsFamily::Gnome)
);
assert_eq!(ThemeSource::System(ThemeMode::Auto).forced_family(), None);
assert_eq!(ThemeSource::Preset(Preset::Nord).forced_family(), None);
assert_eq!(ThemeSource::Custom(Box::default()).forced_family(), None);
}
#[test]
fn macos_metrics_match_nsmenu_reference() {
assert_eq!(MACOS_ROW_HEIGHT, 22.0); assert_eq!(MACOS_MENU_FONT_SIZE, 13.0); assert_eq!(MACOS_VERTICAL_INSET, 4.0); assert_eq!(MACOS_LEADING_INSET, 14.0); assert_eq!(MACOS_COLUMN_GAP, 8.0); assert_eq!(MACOS_CORNER_RADIUS, 6.0);
let mac = Theme::macos(false);
assert_eq!(mac.row_height, MACOS_ROW_HEIGHT);
assert_eq!(mac.corner_radius, MACOS_CORNER_RADIUS);
assert_eq!(mac.column_gap, MACOS_COLUMN_GAP);
assert_eq!(mac.padding.left, MACOS_LEADING_INSET);
assert_eq!(mac.padding.right, MACOS_LEADING_INSET);
assert_eq!(mac.padding.top, MACOS_VERTICAL_INSET);
assert_eq!(mac.padding.bottom, MACOS_VERTICAL_INSET);
assert_eq!(mac.row_font.size, MACOS_MENU_FONT_SIZE);
assert_eq!(mac.header_font.size, MACOS_MENU_FONT_SIZE);
let dark = Theme::macos(true);
assert_eq!(mac.row_height, dark.row_height);
assert_eq!(mac.corner_radius, dark.corner_radius);
assert_eq!(mac.column_gap, dark.column_gap);
assert_eq!(mac.padding.left, dark.padding.left);
assert_eq!(mac.padding.top, dark.padding.top);
assert_eq!(mac.row_font, dark.row_font);
assert_eq!(mac.header_font, dark.header_font);
}
#[test]
fn forced_theme_tracking_travels_with_the_preset() {
let mac = Theme::macos(false);
assert_eq!(
mac.row_font.letter_spacing,
macos_sf_tracking(MACOS_MENU_FONT_SIZE)
);
assert_eq!(
mac.header_font.letter_spacing,
macos_sf_tracking(MACOS_MENU_FONT_SIZE)
);
assert_eq!(
Theme::macos(true).row_font.letter_spacing,
macos_sf_tracking(MACOS_MENU_FONT_SIZE)
);
for win in [Theme::windows(false), Theme::windows(true)] {
assert_eq!(win.row_font.letter_spacing, SEGOE_UI_TRACKING);
assert_eq!(win.header_font.letter_spacing, SEGOE_UI_TRACKING);
assert_eq!(win.row_font.letter_spacing, 0.0);
}
for gnome in [Theme::gnome(false), Theme::gnome(true)] {
assert_eq!(gnome.row_font.letter_spacing, CANTARELL_TRACKING);
assert_eq!(gnome.header_font.letter_spacing, CANTARELL_TRACKING);
assert_eq!(gnome.row_font.letter_spacing, 0.0);
}
}
#[test]
fn macos_preset_sets_optical_size_others_leave_it_default() {
for mac in [Theme::macos(false), Theme::macos(true)] {
assert_eq!(mac.row_font.optical_size, Some(MACOS_MENU_FONT_SIZE));
assert_eq!(mac.header_font.optical_size, Some(MACOS_MENU_FONT_SIZE));
}
for t in [
Theme::windows(false),
Theme::windows(true),
Theme::gnome(false),
Theme::gnome(true),
] {
assert_eq!(t.row_font.optical_size, None);
assert_eq!(t.header_font.optical_size, None);
}
}
#[test]
fn macos_tracking_reconciles_system_and_forced_paths() {
let preset = Theme::macos(false).row_font.letter_spacing;
assert_eq!(preset, macos_sf_tracking(MACOS_MENU_FONT_SIZE));
let mut font = Theme::macos(false).row_font;
let once = font.letter_spacing;
font.letter_spacing = macos_sf_tracking(font.size);
assert_eq!(font.letter_spacing, once);
assert_eq!(MACOS_SF_TRACKING_FRACTION, 0.0);
assert_eq!(macos_sf_tracking(13.0), 0.0);
assert_eq!(macos_sf_tracking(13.0), macos_sf_tracking(15.0));
}
#[test]
fn menu_options_struct_literal_still_compiles() {
let opts = MenuOptions {
min_width: Some(10.0),
max_width: Some(20.0),
theme: ThemeSource::default(),
gutter: GutterPolicy::Never,
trailing_gutter: TrailingGutterPolicy::Never,
};
assert_eq!(opts.min_width, Some(10.0));
}
}