use crate::GlyphPosition;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecorationStyle {
Solid,
Dashed,
Dotted,
Wavy,
}
#[derive(Debug, Clone)]
pub struct TextDecoration {
pub underline: Option<DecorationStyle>,
pub overline: Option<DecorationStyle>,
pub strikethrough: Option<DecorationStyle>,
pub color: [u8; 4],
pub thickness: f32,
}
impl Default for TextDecoration {
fn default() -> Self {
Self {
underline: None,
overline: None,
strikethrough: None,
color: [0, 0, 0, 255],
thickness: 1.0,
}
}
}
impl TextDecoration {
pub fn line_segments(
&self,
glyphs: &[GlyphPosition],
baseline_y: f32,
line_height: f32,
) -> Vec<DecorationSegment> {
if glyphs.is_empty() {
return Vec::new();
}
let x1 = glyphs.iter().map(|g| g.x).fold(f32::MAX, f32::min);
let x2 = glyphs
.iter()
.map(|g| g.x + g.width)
.fold(f32::MIN, f32::max);
let mut segs: Vec<DecorationSegment> = Vec::new();
if let Some(style) = self.underline {
let y = baseline_y + self.thickness;
segs.push(DecorationSegment {
x1,
y1: y,
x2,
y2: y,
style,
color: self.color,
thickness: self.thickness,
});
}
if let Some(style) = self.overline {
let ascent = glyphs.iter().map(|g| g.height).fold(0.0_f32, f32::max);
let y = baseline_y - ascent;
segs.push(DecorationSegment {
x1,
y1: y,
x2,
y2: y,
style,
color: self.color,
thickness: self.thickness,
});
}
if let Some(style) = self.strikethrough {
let y = baseline_y - line_height * 0.25;
segs.push(DecorationSegment {
x1,
y1: y,
x2,
y2: y,
style,
color: self.color,
thickness: self.thickness,
});
}
segs
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DecorationSegment {
pub x1: f32,
pub y1: f32,
pub x2: f32,
pub y2: f32,
pub style: DecorationStyle,
pub color: [u8; 4],
pub thickness: f32,
}
#[cfg(test)]
mod tests {
use super::*;
fn fake_glyphs() -> Vec<GlyphPosition> {
vec![
GlyphPosition {
byte_offset: 0,
x: 0.0,
y: 0.0,
width: 10.0,
height: 16.0,
},
GlyphPosition {
byte_offset: 1,
x: 10.0,
y: 0.0,
width: 10.0,
height: 16.0,
},
GlyphPosition {
byte_offset: 2,
x: 20.0,
y: 0.0,
width: 10.0,
height: 16.0,
},
]
}
#[test]
fn decoration_underline_segment() {
let dec = TextDecoration {
underline: Some(DecorationStyle::Solid),
..TextDecoration::default()
};
let segs = dec.line_segments(&fake_glyphs(), 12.0, 16.0);
assert_eq!(segs.len(), 1, "one segment per decoration type");
let seg = &segs[0];
assert_eq!(seg.style, DecorationStyle::Solid);
assert!((seg.x1 - 0.0).abs() < f32::EPSILON);
assert!((seg.x2 - 30.0).abs() < f32::EPSILON);
}
#[test]
fn decoration_multiple_types() {
let dec = TextDecoration {
underline: Some(DecorationStyle::Solid),
overline: Some(DecorationStyle::Dashed),
strikethrough: Some(DecorationStyle::Dotted),
..TextDecoration::default()
};
let segs = dec.line_segments(&fake_glyphs(), 12.0, 16.0);
assert_eq!(segs.len(), 3);
}
#[test]
fn decoration_empty_glyphs_returns_empty() {
let dec = TextDecoration {
underline: Some(DecorationStyle::Solid),
..TextDecoration::default()
};
assert!(dec.line_segments(&[], 12.0, 16.0).is_empty());
}
#[test]
fn decoration_no_decorations_no_segments() {
let dec = TextDecoration::default();
assert!(dec.line_segments(&fake_glyphs(), 12.0, 16.0).is_empty());
}
}