use super::element::{Element, Rotation};
use super::font::FontSpec;
use super::length::Length;
use super::palette::ThemeColor;
use super::{LineElement, TextElement};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TitleLocation {
#[default]
Outside,
Inside,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct AxisTheme {
pub title: Element<TextElement>,
pub text: Element<TextElement>,
pub line: Element<LineElement>,
pub ticks: Element<LineElement>,
pub ticks_minor: Element<LineElement>,
pub tick_length: Option<Length>,
pub tick_length_minor: Option<Length>,
pub tick_gap: Option<Length>,
pub title_gap: Option<Length>,
pub title_location: Option<TitleLocation>,
}
pub const DEFAULT_TICK_LENGTH_PT: f64 = 2.75;
pub const DEFAULT_MINOR_TICK_LENGTH_PT: f64 = 1.375;
pub const DEFAULT_TICK_GAP_PT: f64 = 2.2;
pub const DEFAULT_TITLE_GAP_PT: f64 = 2.75;
pub const DEFAULT_AXIS_TITLE_SIZE_PT: f64 = super::element::DEFAULT_TEXT_SIZE_PT;
fn build_axis_concrete_defaults() -> AxisTheme {
let line = super::element::line_concrete_defaults();
AxisTheme {
title: Element::Set(TextElement {
size_pt: Some(Length::Abs(DEFAULT_AXIS_TITLE_SIZE_PT)),
angle: Some(Rotation::Along),
color: Some(ThemeColor::Ink),
font: FontSpec::default(),
..TextElement::default()
}),
text: Element::Set(TextElement {
size_pt: Some(Length::Rel(0.8)),
color: Some(ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.7)),
font: FontSpec::default(),
..TextElement::default()
}),
line: Element::Blank,
ticks: Element::Set(LineElement {
color: Some(ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.8)),
..line.clone()
}),
ticks_minor: Element::Set(LineElement {
color: Some(ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.8)),
..line
}),
tick_length: Some(Length::Abs(DEFAULT_TICK_LENGTH_PT)),
tick_length_minor: Some(Length::Abs(DEFAULT_MINOR_TICK_LENGTH_PT)),
tick_gap: Some(Length::Abs(DEFAULT_TICK_GAP_PT)),
title_gap: Some(Length::Abs(DEFAULT_TITLE_GAP_PT)),
title_location: Some(TitleLocation::Outside),
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PerAxis {
pub all: AxisTheme,
pub by_channel: [AxisTheme; 2],
pub by_channel_side: [[AxisTheme; 2]; 2],
}
impl PerAxis {
pub fn new(root: AxisTheme) -> Self {
Self {
all: root,
by_channel: [AxisTheme::default(), AxisTheme::default()],
by_channel_side: [
[AxisTheme::default(), AxisTheme::default()],
[AxisTheme::default(), AxisTheme::default()],
],
}
}
pub fn resolve(&self, ch: u8, side: u8) -> ResolvedAxis {
self.resolve_with_root(ch, side, None)
}
pub fn resolve_with_root(
&self,
ch: u8,
side: u8,
root_text: Option<&TextElement>,
) -> ResolvedAxis {
let ci = ch as usize;
let si = side as usize;
debug_assert!(ci < 2 && si < 2, "channel/side out of range: {ch}, {side}");
let defaults = axis_concrete_defaults();
let axis_root_text = self.all.text.as_set();
let root_line = self.all.line.as_set();
let by_ch = &self.by_channel[ci];
let by_cs = &self.by_channel_side[ci][si];
let title = cascade_element_chain(
[&by_cs.title, &by_ch.title, &self.all.title],
defaults.title.as_set(),
&[axis_root_text, root_text],
);
let text = cascade_element_chain(
[&by_cs.text, &by_ch.text, &self.all.text],
defaults.text.as_set(),
&[root_text],
);
let line = cascade_line_chain(
[&by_cs.line, &by_ch.line, &self.all.line],
defaults.line.as_set(),
None,
);
let ticks = cascade_line_chain(
[&by_cs.ticks, &by_ch.ticks, &self.all.ticks],
defaults.ticks.as_set(),
root_line,
);
let ticks_minor = cascade_line_chain(
[
&by_cs.ticks_minor,
&by_ch.ticks_minor,
&self.all.ticks_minor,
],
defaults.ticks_minor.as_set(),
root_line,
);
let tick_length = by_cs
.tick_length
.or(by_ch.tick_length)
.or(self.all.tick_length)
.or(defaults.tick_length)
.expect("axis_concrete_defaults sets tick_length");
let tick_length_minor = by_cs
.tick_length_minor
.or(by_ch.tick_length_minor)
.or(self.all.tick_length_minor)
.or(defaults.tick_length_minor)
.expect("axis_concrete_defaults sets tick_length_minor");
let tick_gap = by_cs
.tick_gap
.or(by_ch.tick_gap)
.or(self.all.tick_gap)
.or(defaults.tick_gap)
.expect("axis_concrete_defaults sets tick_gap");
let title_gap = by_cs
.title_gap
.or(by_ch.title_gap)
.or(self.all.title_gap)
.or(defaults.title_gap)
.expect("axis_concrete_defaults sets title_gap");
let title_location = by_cs
.title_location
.or(by_ch.title_location)
.or(self.all.title_location)
.or(defaults.title_location)
.expect("axis_concrete_defaults sets title_location");
ResolvedAxis {
title,
text,
line,
ticks,
ticks_minor,
tick_length,
tick_length_minor,
tick_gap,
title_gap,
title_location,
}
}
}
impl Default for PerAxis {
fn default() -> Self {
Self::new(AxisTheme::default())
}
}
impl AxisTheme {
pub fn resolved(&self) -> ResolvedAxis {
PerAxis::new(self.clone()).resolve(0, 0)
}
}
#[derive(Debug, Clone)]
pub struct ResolvedAxis {
pub title: Option<TextElement>,
pub text: Option<TextElement>,
pub line: Option<LineElement>,
pub ticks: Option<LineElement>,
pub ticks_minor: Option<LineElement>,
pub tick_length: Length,
pub tick_length_minor: Length,
pub tick_gap: Length,
pub title_gap: Length,
pub title_location: TitleLocation,
}
fn cascade_element_chain<const N: usize>(
chain: [&Element<TextElement>; N],
axis_default: Option<&TextElement>,
extra_roots: &[Option<&TextElement>],
) -> Option<TextElement> {
let mut merged: Option<TextElement> = None;
for e in chain {
match e {
Element::Blank => return None,
Element::Set(v) => {
merged = Some(match merged {
Some(m) => m.cascade(v),
None => v.clone(),
});
}
Element::Inherit => {}
}
}
if let Some(d) = axis_default {
merged = Some(match merged {
Some(m) => m.cascade(d),
None => d.clone(),
});
}
for r in extra_roots.iter().flatten() {
merged = Some(match merged {
Some(m) => m.cascade(r),
None => (*r).clone(),
});
}
merged
}
fn cascade_line_chain<const N: usize>(
chain: [&Element<LineElement>; N],
axis_default: Option<&LineElement>,
extra_root: Option<&LineElement>,
) -> Option<LineElement> {
let mut merged: Option<LineElement> = None;
for e in chain {
match e {
Element::Blank => return None,
Element::Set(v) => {
merged = Some(match merged {
Some(m) => m.cascade(v),
None => v.clone(),
});
}
Element::Inherit => {}
}
}
if let Some(d) = axis_default {
merged = Some(match merged {
Some(m) => m.cascade(d),
None => d.clone(),
});
}
if let Some(r) = extra_root {
merged = Some(match merged {
Some(m) => m.cascade(r),
None => r.clone(),
});
}
merged
}
static AXIS_CONCRETE_DEFAULTS: std::sync::LazyLock<AxisTheme> =
std::sync::LazyLock::new(build_axis_concrete_defaults);
pub fn axis_concrete_defaults() -> AxisTheme {
AXIS_CONCRETE_DEFAULTS.clone()
}