use std::sync::Arc;
use crate::color::{lerp_color, rgb, Color, ColorSpace};
#[derive(Clone, Debug, PartialEq)]
pub enum LinetypeStep {
Dash(f64),
Marker(Arc<str>),
Gap(f64),
}
impl LinetypeStep {
pub fn is_marker(&self) -> bool {
matches!(self, LinetypeStep::Marker(_))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Length {
Abs(f64),
Rel(f64),
}
impl Length {
#[inline]
pub fn resolve(self, parent_pt: f64) -> f64 {
match self {
Length::Abs(v) => v,
Length::Rel(m) => parent_pt * m,
}
}
#[inline]
pub fn is_abs(self) -> bool {
matches!(self, Length::Abs(_))
}
}
#[inline]
pub const fn pt(v: f64) -> Length {
Length::Abs(v)
}
#[inline]
pub const fn rel(v: f64) -> Length {
Length::Rel(v)
}
impl Default for Length {
fn default() -> Self {
Length::Rel(1.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Margin {
pub top: Length,
pub right: Length,
pub bottom: Length,
pub left: Length,
}
impl Margin {
#[inline]
pub const fn all(v: Length) -> Self {
Self {
top: v,
right: v,
bottom: v,
left: v,
}
}
#[inline]
pub const fn new(top: Length, right: Length, bottom: Length, left: Length) -> Self {
Self {
top,
right,
bottom,
left,
}
}
#[inline]
pub fn resolve(&self, parent_pt: f64) -> (f64, f64, f64, f64) {
(
self.top.resolve(parent_pt),
self.right.resolve(parent_pt),
self.bottom.resolve(parent_pt),
self.left.resolve(parent_pt),
)
}
#[inline]
pub fn resolve_for_direction(&self, parent_pt: f64, is_rtl: bool) -> (f64, f64, f64, f64) {
let (t, r, b, l) = self.resolve(parent_pt);
if is_rtl {
(t, l, b, r)
} else {
(t, r, b, l)
}
}
pub const ZERO: Margin = Margin::all(Length::Abs(0.0));
}
impl Default for Margin {
fn default() -> Self {
Self::ZERO
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Palette {
pub paper: Color,
pub ink: Color,
pub accent: Color,
}
impl Palette {
#[inline]
pub const fn new(paper: Color, ink: Color, accent: Color) -> Self {
Self { paper, ink, accent }
}
}
impl Default for Palette {
fn default() -> Self {
Self {
paper: rgb(1.0, 1.0, 1.0),
ink: rgb(0.0, 0.0, 0.0),
accent: rgb(0.20, 0.45, 0.85),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ThemeColor {
Fixed(Color),
Paper,
Ink,
Accent,
Mix(Box<ThemeColor>, Box<ThemeColor>, f64, ColorSpace),
Alpha(Box<ThemeColor>, f32),
}
impl ThemeColor {
pub fn resolve(&self, palette: &Palette) -> Color {
match self {
ThemeColor::Fixed(c) => *c,
ThemeColor::Paper => palette.paper,
ThemeColor::Ink => palette.ink,
ThemeColor::Accent => palette.accent,
ThemeColor::Mix(a, b, t, space) => {
lerp_color(a.resolve(palette), b.resolve(palette), *t, *space)
}
ThemeColor::Alpha(inner, a) => {
let c = inner.resolve(palette);
let [r, g, b, alpha] = c.components;
Color::new([r, g, b, alpha * a])
}
}
}
#[inline]
pub fn mix(a: ThemeColor, b: ThemeColor, t: f64) -> Self {
ThemeColor::Mix(Box::new(a), Box::new(b), t, ColorSpace::Srgb)
}
#[inline]
pub fn mix_in(a: ThemeColor, b: ThemeColor, t: f64, space: ColorSpace) -> Self {
ThemeColor::Mix(Box::new(a), Box::new(b), t, space)
}
#[inline]
pub fn alpha(inner: ThemeColor, a: f32) -> Self {
ThemeColor::Alpha(Box::new(inner), a)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum HAlign {
#[default]
Start,
Center,
End,
Justify,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum VAlign {
Top,
#[default]
Middle,
Baseline,
Bottom,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mix_addresses_palette_greys_by_channel_fraction() {
let grey =
ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.08).resolve(&Palette::default());
for c in &grey.components[0..3] {
assert!((c - 0.92).abs() < 1e-5, "expected grey92, got {grey:?}");
}
}
fn primary_palette() -> Palette {
Palette::new(rgb(1.0, 0.0, 0.0), rgb(0.0, 0.0, 1.0), rgb(0.0, 1.0, 0.0))
}
#[track_caller]
fn assert_components(got: Color, want: [f32; 4]) {
for (i, (g, w)) in got.components.iter().zip(want.iter()).enumerate() {
assert!(
(g - w).abs() < 1e-6,
"component {i}: got {got:?}, want {want:?}"
);
}
}
#[test]
fn a_mix_nested_in_a_mix_resolves_before_the_outer_blend() {
let c = ThemeColor::mix(
ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.5),
ThemeColor::Ink,
0.5,
)
.resolve(&Palette::default());
assert_components(c, [0.25, 0.25, 0.25, 1.0]);
}
#[test]
fn mixing_two_mixes_averages_their_resolved_colors() {
let c = ThemeColor::mix(
ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.25),
ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.75),
0.5,
)
.resolve(&Palette::default());
assert_components(c, [0.5, 0.5, 0.5, 1.0]);
}
#[test]
fn nested_mixes_address_the_supplied_palette_anchors() {
let c = ThemeColor::mix(
ThemeColor::mix(ThemeColor::Paper, ThemeColor::Accent, 0.5),
ThemeColor::Ink,
0.5,
)
.resolve(&primary_palette());
assert_components(c, [0.25, 0.25, 0.5, 1.0]);
}
#[test]
fn nested_alphas_multiply_rather_than_replace() {
let c = ThemeColor::alpha(ThemeColor::alpha(ThemeColor::Accent, 0.5), 0.5)
.resolve(&primary_palette());
assert_components(c, [0.0, 1.0, 0.0, 0.25]);
}
#[test]
fn alpha_scales_a_fixed_colors_own_alpha() {
let c = ThemeColor::alpha(ThemeColor::Fixed(Color::new([0.2, 0.4, 0.6, 0.5])), 0.5)
.resolve(&Palette::default());
assert_components(c, [0.2, 0.4, 0.6, 0.25]);
}
#[test]
fn alpha_wrapping_a_mix_keeps_the_blended_channels() {
let c = ThemeColor::alpha(
ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.25),
0.4,
)
.resolve(&Palette::default());
assert_components(c, [0.75, 0.75, 0.75, 0.4]);
}
#[test]
fn a_mix_interpolates_the_alpha_of_its_nested_operands() {
let c = ThemeColor::mix(
ThemeColor::alpha(ThemeColor::Paper, 0.0),
ThemeColor::Paper,
0.5,
)
.resolve(&Palette::default());
assert_components(c, [1.0, 1.0, 1.0, 0.5]);
}
#[test]
fn mix_defaults_to_srgb_and_mix_in_overrides_it() {
assert!(matches!(
ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.5),
ThemeColor::Mix(_, _, _, ColorSpace::Srgb)
));
let ok = ThemeColor::mix_in(ThemeColor::Paper, ThemeColor::Ink, 0.5, ColorSpace::Oklab);
assert!(matches!(ok, ThemeColor::Mix(_, _, _, ColorSpace::Oklab)));
let palette = Palette::default();
assert_ne!(
ok.resolve(&palette),
ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.5).resolve(&palette)
);
}
}