Color scheme and some ui cleanup
parent
6b492c9a62
commit
97aa5cb20d
@ -0,0 +1,411 @@
|
|||||||
|
//! Theme color generator for Bevy Feathers.
|
||||||
|
//!
|
||||||
|
//! # Main entrypoint
|
||||||
|
//!
|
||||||
|
//! [`build`] takes [`Seeds`] and a [`Mode`] and returns a complete Feathers
|
||||||
|
//! [`ThemeProps`](bevy::feathers::theme::ThemeProps).
|
||||||
|
//!
|
||||||
|
//! ```rust,ignore
|
||||||
|
//! // Auto-assign roles from a 5-swatch Coolors palette (ranked by chroma).
|
||||||
|
//! let seeds = Seeds::from_coolors([color1, color2, color3, color4, color5]);
|
||||||
|
//! let theme = build(seeds, Mode::Dark);
|
||||||
|
//! // Hand `theme` to Feathers.
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Construct [`Seeds`] directly if you need manual role assignment.
|
||||||
|
|
||||||
|
use bevy::color::{Alpha, Color, Oklcha};
|
||||||
|
use bevy::feathers::theme::{ThemeProps, ThemeToken};
|
||||||
|
use bevy::feathers::tokens as ft;
|
||||||
|
use bevy::platform::collections::HashMap;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Color math
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn relative_luminance(c: Color) -> f32 {
|
||||||
|
let lin = c.to_linear();
|
||||||
|
0.2126 * lin.red + 0.7152 * lin.green + 0.0722 * lin.blue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contrast_ratio(a: Color, b: Color) -> f32 {
|
||||||
|
let (la, lb) = (relative_luminance(a), relative_luminance(b));
|
||||||
|
let (hi, lo) = if la >= lb { (la, lb) } else { (lb, la) };
|
||||||
|
(hi + 0.05) / (lo + 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pick the softest foreground at the given hue/chroma that meets `target`
|
||||||
|
/// WCAG contrast on `bg`. Falls back to pure white/black if nothing passes.
|
||||||
|
fn on_color(bg: Color, hue: f32, chroma: f32, target: f32) -> Color {
|
||||||
|
let bg_lum = relative_luminance(bg);
|
||||||
|
let mut best: Option<(f32, Color)> = None;
|
||||||
|
for i in 0..=100 {
|
||||||
|
let l = i as f32 / 100.0;
|
||||||
|
let cand = Color::oklcha(l, chroma, hue, 1.0);
|
||||||
|
if contrast_ratio(cand, bg) >= target {
|
||||||
|
let d = (relative_luminance(cand) - bg_lum).abs();
|
||||||
|
if best.map_or(true, |(bd, _)| d < bd) {
|
||||||
|
best = Some((d, cand));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best.map(|(_, c)| c)
|
||||||
|
.unwrap_or_else(|| Color::oklcha(if bg_lum < 0.5 { 1.0 } else { 0.0 }, 0.0, hue, 1.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lighter in dark mode, darker in light mode — keeps hue/chroma.
|
||||||
|
fn elevate(c: Color, mode: Mode, amount: f32) -> Color {
|
||||||
|
let o = Oklcha::from(c);
|
||||||
|
let l = match mode {
|
||||||
|
Mode::Dark => o.lightness + amount,
|
||||||
|
Mode::Light => o.lightness - amount,
|
||||||
|
};
|
||||||
|
Color::from(o.with_lightness(l.clamp(0.0, 1.0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum Mode {
|
||||||
|
Dark,
|
||||||
|
Light,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The three seed colors the generator needs. Use `from_coolors` to
|
||||||
|
/// auto-assign roles from a 5-swatch palette.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Seeds {
|
||||||
|
pub primary: Color,
|
||||||
|
pub secondary: Color,
|
||||||
|
pub accent: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Seeds {
|
||||||
|
/// Assign roles from an unordered 5-swatch palette (e.g. from Coolors).
|
||||||
|
/// Most-chromatic swatch becomes accent, next primary, next secondary.
|
||||||
|
pub fn from_coolors(palette: [Color; 5]) -> Self {
|
||||||
|
let mut by_chroma: Vec<Oklcha> = palette.iter().map(|c| Oklcha::from(*c)).collect();
|
||||||
|
by_chroma.sort_by(|a, b| b.chroma.total_cmp(&a.chroma));
|
||||||
|
let pick = |o: Oklcha| Color::from(o);
|
||||||
|
Seeds {
|
||||||
|
accent: pick(by_chroma[0]),
|
||||||
|
primary: pick(by_chroma[1]),
|
||||||
|
secondary: pick(by_chroma[2]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Lightness anchors
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct Anchors {
|
||||||
|
window: f32,
|
||||||
|
sunken: f32,
|
||||||
|
surface: f32,
|
||||||
|
raised: f32,
|
||||||
|
border: f32,
|
||||||
|
control: f32,
|
||||||
|
text: f32,
|
||||||
|
text_dim: f32,
|
||||||
|
text_disabled: f32,
|
||||||
|
accent: f32,
|
||||||
|
status: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Anchors {
|
||||||
|
fn for_mode(mode: Mode) -> Self {
|
||||||
|
match mode {
|
||||||
|
Mode::Dark => Anchors {
|
||||||
|
window: 0.15,
|
||||||
|
sunken: 0.12,
|
||||||
|
surface: 0.19,
|
||||||
|
raised: 0.24,
|
||||||
|
border: 0.32,
|
||||||
|
control: 0.27,
|
||||||
|
text: 0.96,
|
||||||
|
text_dim: 0.74,
|
||||||
|
text_disabled: 0.46,
|
||||||
|
accent: 0.62,
|
||||||
|
status: 0.62,
|
||||||
|
},
|
||||||
|
Mode::Light => Anchors {
|
||||||
|
window: 0.965,
|
||||||
|
sunken: 0.93,
|
||||||
|
surface: 0.99,
|
||||||
|
raised: 1.00,
|
||||||
|
border: 0.85,
|
||||||
|
control: 0.96,
|
||||||
|
text: 0.22,
|
||||||
|
text_dim: 0.45,
|
||||||
|
text_disabled: 0.70,
|
||||||
|
accent: 0.55,
|
||||||
|
status: 0.52,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Theme builder
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Build a complete Feathers `ThemeProps` from seed colors.
|
||||||
|
///
|
||||||
|
/// All colors are derived in OKLCH so hue never drifts, neutrals carry a
|
||||||
|
/// whisper of the primary hue, and text colors are WCAG-contrast-checked.
|
||||||
|
pub fn build(seeds: Seeds, mode: Mode) -> ThemeProps {
|
||||||
|
let a = Anchors::for_mode(mode);
|
||||||
|
|
||||||
|
let primary = Oklcha::from(seeds.primary);
|
||||||
|
let accent = Oklcha::from(seeds.accent);
|
||||||
|
|
||||||
|
let neutral_hue = primary.hue;
|
||||||
|
let neutral = |l: f32| Color::oklcha(l, 0.012, neutral_hue, 1.0);
|
||||||
|
let shade = |seed: Oklcha, l: f32| Color::oklcha(l, seed.chroma, seed.hue, 1.0);
|
||||||
|
|
||||||
|
let status_chroma = ((primary.chroma + accent.chroma) * 0.5).clamp(0.08, 0.16);
|
||||||
|
let status = |hue: f32| Color::oklcha(a.status, status_chroma, hue, 1.0);
|
||||||
|
|
||||||
|
let control = neutral(a.control);
|
||||||
|
let sunken = neutral(a.sunken);
|
||||||
|
let border = neutral(a.border);
|
||||||
|
let text = neutral(a.text);
|
||||||
|
let text_dim = neutral(a.text_dim);
|
||||||
|
let text_disabled = neutral(a.text_disabled);
|
||||||
|
|
||||||
|
let accent_bg = shade(accent, a.accent);
|
||||||
|
let accent_hover = elevate(accent_bg, mode, 0.05);
|
||||||
|
let accent_pressed = elevate(accent_bg, mode, 0.10);
|
||||||
|
let focus_ring = shade(accent, if mode == Mode::Dark { 0.70 } else { 0.55 });
|
||||||
|
|
||||||
|
let btn_text = on_color(control, neutral_hue, 0.0, 4.5);
|
||||||
|
let accent_text = on_color(accent_bg, accent.hue, 0.03, 4.5);
|
||||||
|
|
||||||
|
let mut colors: HashMap<ThemeToken, Color> = HashMap::new();
|
||||||
|
macro_rules! set {
|
||||||
|
($tok:expr, $color:expr) => {
|
||||||
|
colors.insert($tok.clone(), $color)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window / pane surfaces
|
||||||
|
set!(ft::WINDOW_BG, neutral(a.window));
|
||||||
|
set!(ft::PANE_HEADER_BG, neutral(a.window));
|
||||||
|
set!(ft::PANE_HEADER_BORDER, border);
|
||||||
|
set!(ft::PANE_HEADER_TEXT, text);
|
||||||
|
set!(ft::PANE_HEADER_DIVIDER, border);
|
||||||
|
set!(ft::PANE_BODY_BG, neutral(a.surface));
|
||||||
|
set!(ft::SUBPANE_HEADER_BG, neutral(a.raised));
|
||||||
|
set!(ft::SUBPANE_HEADER_BORDER, border);
|
||||||
|
set!(ft::SUBPANE_HEADER_TEXT, text);
|
||||||
|
set!(ft::SUBPANE_BODY_BG, neutral(a.surface));
|
||||||
|
set!(ft::SUBPANE_BODY_BORDER, border);
|
||||||
|
set!(ft::GROUP_HEADER_BG, neutral(a.raised));
|
||||||
|
set!(ft::GROUP_HEADER_BORDER, border);
|
||||||
|
set!(ft::GROUP_HEADER_TEXT, text);
|
||||||
|
set!(ft::GROUP_BODY_BG, neutral(a.raised));
|
||||||
|
set!(ft::GROUP_BODY_BORDER, border);
|
||||||
|
|
||||||
|
// Focus ring
|
||||||
|
set!(ft::FOCUS_RING, focus_ring);
|
||||||
|
|
||||||
|
// Text
|
||||||
|
set!(ft::TEXT_MAIN, text);
|
||||||
|
set!(ft::TEXT_DIM, text_dim);
|
||||||
|
|
||||||
|
// Normal buttons
|
||||||
|
set!(ft::BUTTON_BG, control);
|
||||||
|
set!(ft::BUTTON_BG_HOVER, elevate(control, mode, 0.04));
|
||||||
|
set!(ft::BUTTON_BG_PRESSED, elevate(control, mode, 0.08));
|
||||||
|
set!(ft::BUTTON_BG_DISABLED, control.with_alpha(0.4));
|
||||||
|
set!(ft::BUTTON_TEXT, btn_text);
|
||||||
|
set!(ft::BUTTON_TEXT_DISABLED, btn_text.with_alpha(0.5));
|
||||||
|
|
||||||
|
// Primary (accent) buttons
|
||||||
|
set!(ft::BUTTON_PRIMARY_BG, accent_bg);
|
||||||
|
set!(ft::BUTTON_PRIMARY_BG_HOVER, accent_hover);
|
||||||
|
set!(ft::BUTTON_PRIMARY_BG_PRESSED, accent_pressed);
|
||||||
|
set!(ft::BUTTON_PRIMARY_BG_DISABLED, accent_bg.with_alpha(0.4));
|
||||||
|
set!(ft::BUTTON_PRIMARY_TEXT, accent_text);
|
||||||
|
set!(
|
||||||
|
ft::BUTTON_PRIMARY_TEXT_DISABLED,
|
||||||
|
accent_text.with_alpha(0.5)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Plain (transparent) buttons
|
||||||
|
set!(ft::BUTTON_PLAIN_BG, Color::NONE);
|
||||||
|
set!(ft::BUTTON_PLAIN_BG_HOVER, neutral(a.raised));
|
||||||
|
set!(ft::BUTTON_PLAIN_BG_PRESSED, control);
|
||||||
|
set!(ft::BUTTON_PLAIN_BG_DISABLED, Color::NONE);
|
||||||
|
|
||||||
|
// Sliders
|
||||||
|
set!(ft::SLIDER_BG, sunken);
|
||||||
|
set!(ft::SLIDER_BG_HOVER, elevate(sunken, mode, 0.03));
|
||||||
|
set!(ft::SLIDER_BG_PRESSED, elevate(sunken, mode, 0.06));
|
||||||
|
set!(ft::SLIDER_BG_DISABLED, sunken.with_alpha(0.5));
|
||||||
|
set!(ft::SLIDER_BAR, accent_bg);
|
||||||
|
set!(ft::SLIDER_BAR_HOVER, accent_hover);
|
||||||
|
set!(ft::SLIDER_BAR_PRESSED, accent_pressed);
|
||||||
|
set!(ft::SLIDER_BAR_DISABLED, accent_bg.with_alpha(0.5));
|
||||||
|
set!(ft::SLIDER_TEXT, on_color(sunken, neutral_hue, 0.0, 4.5));
|
||||||
|
set!(ft::SLIDER_TEXT_DISABLED, text_disabled.with_alpha(0.5));
|
||||||
|
|
||||||
|
// Scrollbar
|
||||||
|
set!(ft::SCROLLBAR_BG, sunken);
|
||||||
|
set!(ft::SCROLLBAR_THUMB, accent_bg);
|
||||||
|
set!(ft::SCROLLBAR_THUMB_HOVER, accent_hover);
|
||||||
|
|
||||||
|
// Checkboxes
|
||||||
|
set!(ft::CHECKBOX_BG, control);
|
||||||
|
set!(ft::CHECKBOX_BG_HOVER, elevate(control, mode, 0.04));
|
||||||
|
set!(ft::CHECKBOX_BG_PRESSED, elevate(control, mode, 0.08));
|
||||||
|
set!(ft::CHECKBOX_BG_DISABLED, control.with_alpha(0.4));
|
||||||
|
set!(ft::CHECKBOX_BG_CHECKED, accent_bg);
|
||||||
|
set!(ft::CHECKBOX_BG_CHECKED_HOVER, accent_hover);
|
||||||
|
set!(ft::CHECKBOX_BG_CHECKED_PRESSED, accent_pressed);
|
||||||
|
set!(ft::CHECKBOX_BG_CHECKED_DISABLED, accent_bg.with_alpha(0.4));
|
||||||
|
set!(ft::CHECKBOX_BORDER, border);
|
||||||
|
set!(ft::CHECKBOX_BORDER_HOVER, elevate(border, mode, 0.04));
|
||||||
|
set!(ft::CHECKBOX_BORDER_PRESSED, elevate(border, mode, 0.08));
|
||||||
|
set!(ft::CHECKBOX_BORDER_DISABLED, border.with_alpha(0.4));
|
||||||
|
set!(ft::CHECKBOX_BORDER_CHECKED, accent_bg);
|
||||||
|
set!(ft::CHECKBOX_BORDER_CHECKED_HOVER, accent_hover);
|
||||||
|
set!(ft::CHECKBOX_BORDER_CHECKED_PRESSED, accent_pressed);
|
||||||
|
set!(
|
||||||
|
ft::CHECKBOX_BORDER_CHECKED_DISABLED,
|
||||||
|
accent_bg.with_alpha(0.4)
|
||||||
|
);
|
||||||
|
set!(ft::CHECKBOX_MARK, accent_text);
|
||||||
|
set!(ft::CHECKBOX_MARK_DISABLED, text_disabled);
|
||||||
|
set!(ft::CHECKBOX_TEXT, text);
|
||||||
|
set!(ft::CHECKBOX_TEXT_DISABLED, text_disabled.with_alpha(0.5));
|
||||||
|
|
||||||
|
// Radio buttons
|
||||||
|
set!(ft::RADIO_BORDER, border);
|
||||||
|
set!(ft::RADIO_BORDER_HOVER, elevate(border, mode, 0.04));
|
||||||
|
set!(ft::RADIO_BORDER_PRESSED, elevate(border, mode, 0.08));
|
||||||
|
set!(ft::RADIO_BORDER_DISABLED, border.with_alpha(0.4));
|
||||||
|
set!(ft::RADIO_BORDER_CHECKED, accent_bg);
|
||||||
|
set!(ft::RADIO_BORDER_CHECKED_HOVER, accent_hover);
|
||||||
|
set!(ft::RADIO_BORDER_CHECKED_PRESSED, accent_pressed);
|
||||||
|
set!(ft::RADIO_BORDER_CHECKED_DISABLED, accent_bg.with_alpha(0.4));
|
||||||
|
set!(ft::RADIO_MARK, accent_bg);
|
||||||
|
set!(ft::RADIO_MARK_HOVER, accent_hover);
|
||||||
|
set!(ft::RADIO_MARK_PRESSED, accent_pressed);
|
||||||
|
set!(ft::RADIO_MARK_DISABLED, border.with_alpha(0.4));
|
||||||
|
set!(ft::RADIO_TEXT, text);
|
||||||
|
set!(ft::RADIO_TEXT_DISABLED, text_disabled.with_alpha(0.5));
|
||||||
|
|
||||||
|
// Toggle switches
|
||||||
|
let slide = text; // high-contrast nub that slides
|
||||||
|
set!(ft::SWITCH_BG, control);
|
||||||
|
set!(ft::SWITCH_BG_HOVER, elevate(control, mode, 0.04));
|
||||||
|
set!(ft::SWITCH_BG_PRESSED, elevate(control, mode, 0.08));
|
||||||
|
set!(ft::SWITCH_BG_DISABLED, control.with_alpha(0.4));
|
||||||
|
set!(ft::SWITCH_BG_CHECKED, accent_bg);
|
||||||
|
set!(ft::SWITCH_BG_CHECKED_HOVER, accent_hover);
|
||||||
|
set!(ft::SWITCH_BG_CHECKED_PRESSED, accent_pressed);
|
||||||
|
set!(ft::SWITCH_BG_CHECKED_DISABLED, accent_bg.with_alpha(0.4));
|
||||||
|
set!(ft::SWITCH_BORDER, border);
|
||||||
|
set!(ft::SWITCH_BORDER_HOVER, elevate(border, mode, 0.04));
|
||||||
|
set!(ft::SWITCH_BORDER_PRESSED, elevate(border, mode, 0.08));
|
||||||
|
set!(ft::SWITCH_BORDER_DISABLED, border.with_alpha(0.4));
|
||||||
|
set!(ft::SWITCH_BORDER_CHECKED, accent_bg);
|
||||||
|
set!(ft::SWITCH_BORDER_CHECKED_HOVER, accent_hover);
|
||||||
|
set!(ft::SWITCH_BORDER_CHECKED_PRESSED, accent_pressed);
|
||||||
|
set!(
|
||||||
|
ft::SWITCH_BORDER_CHECKED_DISABLED,
|
||||||
|
accent_bg.with_alpha(0.4)
|
||||||
|
);
|
||||||
|
set!(ft::SWITCH_SLIDE_BG, slide);
|
||||||
|
set!(ft::SWITCH_SLIDE_BG_HOVER, slide);
|
||||||
|
set!(ft::SWITCH_SLIDE_BG_PRESSED, slide);
|
||||||
|
set!(ft::SWITCH_SLIDE_BG_DISABLED, text_disabled);
|
||||||
|
set!(ft::SWITCH_SLIDE_BG_CHECKED, slide);
|
||||||
|
set!(ft::SWITCH_SLIDE_BG_CHECKED_HOVER, slide);
|
||||||
|
set!(ft::SWITCH_SLIDE_BG_CHECKED_PRESSED, slide);
|
||||||
|
set!(ft::SWITCH_SLIDE_BG_CHECKED_DISABLED, text_disabled);
|
||||||
|
set!(ft::SWITCH_SLIDE_BORDER, slide);
|
||||||
|
set!(ft::SWITCH_SLIDE_BORDER_HOVER, slide);
|
||||||
|
set!(ft::SWITCH_SLIDE_BORDER_PRESSED, slide);
|
||||||
|
set!(ft::SWITCH_SLIDE_BORDER_DISABLED, text_disabled);
|
||||||
|
set!(ft::SWITCH_SLIDE_BORDER_CHECKED, slide);
|
||||||
|
set!(ft::SWITCH_SLIDE_BORDER_CHECKED_HOVER, slide);
|
||||||
|
set!(ft::SWITCH_SLIDE_BORDER_CHECKED_PRESSED, slide);
|
||||||
|
set!(ft::SWITCH_SLIDE_BORDER_CHECKED_DISABLED, text_disabled);
|
||||||
|
|
||||||
|
// Color plane
|
||||||
|
set!(ft::COLOR_PLANE_BG, sunken);
|
||||||
|
|
||||||
|
// Menus
|
||||||
|
let menu_bg = neutral(a.surface);
|
||||||
|
set!(ft::MENU_BG, menu_bg);
|
||||||
|
set!(ft::MENU_BORDER, border);
|
||||||
|
set!(ft::MENUITEM_BG_HOVER, elevate(menu_bg, mode, 0.03));
|
||||||
|
set!(ft::MENUITEM_BG_PRESSED, elevate(menu_bg, mode, 0.06));
|
||||||
|
set!(ft::MENUITEM_BG_FOCUSED, elevate(menu_bg, mode, 0.06));
|
||||||
|
set!(ft::MENUITEM_TEXT, text);
|
||||||
|
set!(ft::MENUITEM_TEXT_DISABLED, text_disabled.with_alpha(0.5));
|
||||||
|
|
||||||
|
// Text input
|
||||||
|
set!(ft::TEXT_INPUT_BG, sunken);
|
||||||
|
set!(ft::TEXT_INPUT_LABEL_BG, control);
|
||||||
|
set!(ft::TEXT_INPUT_TEXT, text);
|
||||||
|
set!(ft::TEXT_INPUT_TEXT_DISABLED, text_disabled.with_alpha(0.5));
|
||||||
|
set!(ft::TEXT_INPUT_CURSOR, focus_ring);
|
||||||
|
set!(ft::TEXT_INPUT_SELECTION, accent_bg.with_alpha(0.5));
|
||||||
|
set!(ft::TEXT_INPUT_SELECTION_UNFOCUSED, Color::NONE);
|
||||||
|
set!(ft::TEXT_INPUT_X_AXIS, status(27.0));
|
||||||
|
set!(ft::TEXT_INPUT_Y_AXIS, status(150.0));
|
||||||
|
set!(ft::TEXT_INPUT_Z_AXIS, status(270.0));
|
||||||
|
|
||||||
|
// Listview
|
||||||
|
set!(ft::LISTROW_BG, Color::NONE);
|
||||||
|
set!(ft::LISTROW_BG_HOVER, control.with_alpha(0.4));
|
||||||
|
set!(ft::LISTROW_BG_SELECTED, control);
|
||||||
|
set!(ft::LISTROW_TEXT, text);
|
||||||
|
set!(ft::LISTROW_TEXT_DISABLED, text_disabled.with_alpha(0.5));
|
||||||
|
|
||||||
|
ThemeProps { color: colors }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use bevy::color::Srgba;
|
||||||
|
use bevy::feathers::tokens as ft;
|
||||||
|
|
||||||
|
fn hex(s: &str) -> Color {
|
||||||
|
Color::from(Srgba::hex(s).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn demo_seeds() -> Seeds {
|
||||||
|
Seeds::from_coolors([
|
||||||
|
hex("264653"),
|
||||||
|
hex("2A9D8F"),
|
||||||
|
hex("E9C46A"),
|
||||||
|
hex("F4A261"),
|
||||||
|
hex("E76F51"),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_meets_contrast_in_both_modes() {
|
||||||
|
for mode in [Mode::Dark, Mode::Light] {
|
||||||
|
let t = build(demo_seeds(), mode);
|
||||||
|
let get = |tok: &ThemeToken| *t.color.get(tok).unwrap();
|
||||||
|
assert!(contrast_ratio(get(&ft::TEXT_MAIN), get(&ft::WINDOW_BG)) >= 4.5);
|
||||||
|
assert!(
|
||||||
|
contrast_ratio(get(&ft::BUTTON_PRIMARY_TEXT), get(&ft::BUTTON_PRIMARY_BG)) >= 4.5
|
||||||
|
);
|
||||||
|
assert!(contrast_ratio(get(&ft::BUTTON_TEXT), get(&ft::BUTTON_BG)) >= 4.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue