use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Rgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Rgb {
#[must_use]
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
#[derive(pleme_allvariants_derive::AllVariants)]
#[serde(rename_all = "lowercase")]
pub enum UnderlineStyle {
#[default]
None,
Single,
Double,
Curly,
Dotted,
Dashed,
}
impl UnderlineStyle {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::Single => "single",
Self::Double => "double",
Self::Curly => "curly",
Self::Dotted => "dotted",
Self::Dashed => "dashed",
}
}
}
impl fmt::Display for UnderlineStyle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum UnderlineColor {
#[default]
Default,
Indexed(u8),
Rgb(Rgb),
}
impl fmt::Display for UnderlineColor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Default => f.write_str("default"),
Self::Indexed(n) => write!(f, "indexed({n})"),
Self::Rgb(c) => write!(f, "#{:02x}{:02x}{:02x}", c.r, c.g, c.b),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct UnderlineMetrics {
pub cell_width: f32,
pub underline_y: f32,
pub thickness: f32,
pub baseline: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct DecorationRect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct SegmentRun {
pub band: DecorationRect,
pub period: f32,
pub duty: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct CurlyBand {
pub rect: DecorationRect,
pub period: f32,
pub amplitude: f32,
pub thickness: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum UnderlineGeometry {
None,
Single(DecorationRect),
Double {
upper: DecorationRect,
lower: DecorationRect,
},
Run(SegmentRun),
Curly(CurlyBand),
}
pub const DOTTED_PERIOD_PER_THICKNESS: f32 = 2.0;
pub const DOTTED_DUTY: f32 = 0.5;
pub const DASHED_PERIODS_PER_CELL: f32 = 2.0;
pub const DASHED_DUTY: f32 = 0.75;
#[must_use]
pub fn emit_underline_rects(style: UnderlineStyle, metrics: UnderlineMetrics) -> UnderlineGeometry {
let stroke = DecorationRect {
x: 0.0,
y: metrics.underline_y,
width: metrics.cell_width,
height: metrics.thickness,
};
match style {
UnderlineStyle::None => UnderlineGeometry::None,
UnderlineStyle::Single => UnderlineGeometry::Single(stroke),
UnderlineStyle::Double => UnderlineGeometry::Double {
upper: DecorationRect {
y: metrics.underline_y - 2.0 * metrics.thickness,
..stroke
},
lower: stroke,
},
UnderlineStyle::Curly => {
let amplitude = ((metrics.underline_y - metrics.baseline) / 2.0)
.max(metrics.thickness);
UnderlineGeometry::Curly(CurlyBand {
rect: DecorationRect {
x: 0.0,
y: metrics.underline_y - 2.0 * amplitude,
width: metrics.cell_width,
height: 2.0 * amplitude + metrics.thickness,
},
period: metrics.cell_width,
amplitude,
thickness: metrics.thickness,
})
}
UnderlineStyle::Dotted => UnderlineGeometry::Run(SegmentRun {
band: stroke,
period: DOTTED_PERIOD_PER_THICKNESS * metrics.thickness,
duty: DOTTED_DUTY,
}),
UnderlineStyle::Dashed => UnderlineGeometry::Run(SegmentRun {
band: stroke,
period: metrics.cell_width / DASHED_PERIODS_PER_CELL,
duty: DASHED_DUTY,
}),
}
}
#[must_use]
pub fn overline_rect(metrics: UnderlineMetrics) -> DecorationRect {
DecorationRect {
x: 0.0,
y: 0.0,
width: metrics.cell_width,
height: metrics.thickness,
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::float_cmp)]
use super::*;
#[test]
fn vocabulary_trait_surface() {
fn pin<T: Copy + Eq + std::hash::Hash + Send + Sync + Default + fmt::Debug>() {}
fn pin_no_default<T: Copy + Eq + std::hash::Hash + Send + Sync + fmt::Debug>() {}
pin::<UnderlineStyle>();
pin::<UnderlineColor>();
pin_no_default::<Rgb>();
}
#[test]
fn defaults_match_mado_vocabulary() {
assert_eq!(UnderlineStyle::default(), UnderlineStyle::None);
assert_eq!(UnderlineColor::default(), UnderlineColor::Default);
}
#[test]
fn serde_wire_shape_is_pinned() {
let style_wire: Vec<(UnderlineStyle, &str)> = UnderlineStyle::ALL
.iter()
.map(|&s| {
(
s,
match s {
UnderlineStyle::None => "\"none\"",
UnderlineStyle::Single => "\"single\"",
UnderlineStyle::Double => "\"double\"",
UnderlineStyle::Curly => "\"curly\"",
UnderlineStyle::Dotted => "\"dotted\"",
UnderlineStyle::Dashed => "\"dashed\"",
},
)
})
.collect();
let mut failures: Vec<(UnderlineStyle, String)> = Vec::new();
for (style, expected) in style_wire {
let got = serde_json::to_string(&style).expect("serialize");
if got != expected {
failures.push((style, got.clone()));
}
let back: UnderlineStyle = serde_json::from_str(&got).expect("roundtrip");
if back != style {
failures.push((style, got));
}
}
assert!(failures.is_empty(), "style wire drift: {failures:?}");
let color_wire = [
(UnderlineColor::Default, "\"default\""),
(UnderlineColor::Indexed(9), "{\"indexed\":9}"),
(
UnderlineColor::Rgb(Rgb::new(10, 11, 12)),
"{\"rgb\":{\"r\":10,\"g\":11,\"b\":12}}",
),
];
let mut color_failures: Vec<(UnderlineColor, String)> = Vec::new();
for (color, expected) in color_wire {
let got = serde_json::to_string(&color).expect("serialize");
if got != expected {
color_failures.push((color, got.clone()));
}
let back: UnderlineColor = serde_json::from_str(&got).expect("roundtrip");
if back != color {
color_failures.push((color, got));
}
}
assert!(color_failures.is_empty(), "color wire drift: {color_failures:?}");
}
#[test]
fn display_matches_mado_wire() {
assert_eq!(UnderlineStyle::Curly.to_string(), "curly");
assert_eq!(UnderlineStyle::None.as_str(), "none");
assert_eq!(UnderlineColor::Default.to_string(), "default");
assert_eq!(UnderlineColor::Indexed(9).to_string(), "indexed(9)");
assert_eq!(
UnderlineColor::Rgb(Rgb::new(0x0a, 0x0b, 0x0c)).to_string(),
"#0a0b0c"
);
}
#[test]
fn overline_sits_on_top_edge() {
let m = UnderlineMetrics {
cell_width: 10.0,
underline_y: 17.0,
thickness: 2.0,
baseline: 15.0,
};
let r = overline_rect(m);
assert_eq!(r.y, 0.0);
assert_eq!(r.x, 0.0);
assert_eq!(r.width, m.cell_width);
assert_eq!(r.height, m.thickness);
}
}