use astrelis_render::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Default)]
pub enum LineStyle {
#[default]
Solid,
Dashed,
Dotted,
Wavy,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct UnderlineStyle {
pub color: Color,
pub thickness: f32,
pub style: LineStyle,
pub offset: f32,
}
impl UnderlineStyle {
pub fn solid(color: Color, thickness: f32) -> Self {
Self {
color,
thickness,
style: LineStyle::Solid,
offset: 2.0,
}
}
pub fn dashed(color: Color, thickness: f32) -> Self {
Self {
color,
thickness,
style: LineStyle::Dashed,
offset: 2.0,
}
}
pub fn dotted(color: Color, thickness: f32) -> Self {
Self {
color,
thickness,
style: LineStyle::Dotted,
offset: 2.0,
}
}
pub fn wavy(color: Color, thickness: f32) -> Self {
Self {
color,
thickness,
style: LineStyle::Wavy,
offset: 2.0,
}
}
pub fn with_offset(mut self, offset: f32) -> Self {
self.offset = offset;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StrikethroughStyle {
pub color: Color,
pub thickness: f32,
pub style: LineStyle,
pub offset: f32,
}
impl StrikethroughStyle {
pub fn solid(color: Color, thickness: f32) -> Self {
Self {
color,
thickness,
style: LineStyle::Solid,
offset: 0.0,
}
}
pub fn dashed(color: Color, thickness: f32) -> Self {
Self {
color,
thickness,
style: LineStyle::Dashed,
offset: 0.0,
}
}
pub fn dotted(color: Color, thickness: f32) -> Self {
Self {
color,
thickness,
style: LineStyle::Dotted,
offset: 0.0,
}
}
pub fn with_offset(mut self, offset: f32) -> Self {
self.offset = offset;
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextDecoration {
pub underline: Option<UnderlineStyle>,
pub strikethrough: Option<StrikethroughStyle>,
pub background: Option<Color>,
pub background_padding: [f32; 4],
}
impl Default for TextDecoration {
fn default() -> Self {
Self {
underline: None,
strikethrough: None,
background: None,
background_padding: [0.0, 0.0, 0.0, 0.0],
}
}
}
impl TextDecoration {
pub fn new() -> Self {
Self::default()
}
pub fn underline(mut self, style: UnderlineStyle) -> Self {
self.underline = Some(style);
self
}
pub fn strikethrough(mut self, style: StrikethroughStyle) -> Self {
self.strikethrough = Some(style);
self
}
pub fn background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub fn background_padding_uniform(mut self, padding: f32) -> Self {
self.background_padding = [padding; 4];
self
}
pub fn background_padding_ltrb(mut self, left: f32, top: f32, right: f32, bottom: f32) -> Self {
self.background_padding = [left, top, right, bottom];
self
}
pub fn has_decoration(&self) -> bool {
self.underline.is_some() || self.strikethrough.is_some() || self.background.is_some()
}
pub fn has_underline(&self) -> bool {
self.underline.is_some()
}
pub fn has_strikethrough(&self) -> bool {
self.strikethrough.is_some()
}
pub fn has_background(&self) -> bool {
self.background.is_some()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DecorationGeometry {
pub start: (f32, f32),
pub end: (f32, f32),
pub thickness: f32,
pub color: Color,
pub style: LineStyle,
}
impl DecorationGeometry {
pub fn new(start: (f32, f32), end: (f32, f32), thickness: f32, color: Color, style: LineStyle) -> Self {
Self {
start,
end,
thickness,
color,
style,
}
}
pub fn length(&self) -> f32 {
let dx = self.end.0 - self.start.0;
let dy = self.end.1 - self.start.1;
(dx * dx + dy * dy).sqrt()
}
pub fn center(&self) -> (f32, f32) {
((self.start.0 + self.end.0) / 2.0, (self.start.1 + self.end.1) / 2.0)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BackgroundGeometry {
pub rect: (f32, f32, f32, f32),
pub color: Color,
}
impl BackgroundGeometry {
pub fn new(x: f32, y: f32, width: f32, height: f32, color: Color) -> Self {
Self {
rect: (x, y, width, height),
color,
}
}
pub fn as_rect(&self) -> (f32, f32, f32, f32) {
self.rect
}
}
pub fn generate_decoration_geometry(
decoration: &TextDecoration,
baseline_y: f32,
line_start_x: f32,
line_end_x: f32,
line_height: f32,
) -> (
Option<BackgroundGeometry>,
Option<DecorationGeometry>,
Option<DecorationGeometry>,
) {
let mut background = None;
let mut underline = None;
let mut strikethrough = None;
if let Some(bg_color) = decoration.background {
let padding = &decoration.background_padding;
let x = line_start_x - padding[0];
let y = baseline_y - line_height + padding[1];
let width = (line_end_x - line_start_x) + padding[0] + padding[2];
let height = line_height + padding[1] + padding[3];
background = Some(BackgroundGeometry::new(x, y, width, height, bg_color));
}
if let Some(ul_style) = decoration.underline {
let y = baseline_y + ul_style.offset;
underline = Some(DecorationGeometry::new(
(line_start_x, y),
(line_end_x, y),
ul_style.thickness,
ul_style.color,
ul_style.style,
));
}
if let Some(st_style) = decoration.strikethrough {
let y = baseline_y - (line_height / 2.0) + st_style.offset;
strikethrough = Some(DecorationGeometry::new(
(line_start_x, y),
(line_end_x, y),
st_style.thickness,
st_style.color,
st_style.style,
));
}
(background, underline, strikethrough)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DecorationQuadType {
Background,
Underline {
thickness: f32,
},
Strikethrough {
thickness: f32,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct DecorationQuad {
pub bounds: (f32, f32, f32, f32),
pub color: Color,
pub quad_type: DecorationQuadType,
}
impl DecorationQuad {
pub fn new(x: f32, y: f32, width: f32, height: f32, color: Color, quad_type: DecorationQuadType) -> Self {
Self {
bounds: (x, y, width, height),
color,
quad_type,
}
}
pub fn background(x: f32, y: f32, width: f32, height: f32, color: Color) -> Self {
Self::new(x, y, width, height, color, DecorationQuadType::Background)
}
pub fn underline(x: f32, y: f32, width: f32, thickness: f32, color: Color) -> Self {
Self::new(x, y, width, thickness, color, DecorationQuadType::Underline { thickness })
}
pub fn strikethrough(x: f32, y: f32, width: f32, thickness: f32, color: Color) -> Self {
Self::new(x, y, width, thickness, color, DecorationQuadType::Strikethrough { thickness })
}
pub fn as_rect(&self) -> (f32, f32, f32, f32) {
self.bounds
}
pub fn is_background(&self) -> bool {
matches!(self.quad_type, DecorationQuadType::Background)
}
pub fn is_underline(&self) -> bool {
matches!(self.quad_type, DecorationQuadType::Underline { .. })
}
pub fn is_strikethrough(&self) -> bool {
matches!(self.quad_type, DecorationQuadType::Strikethrough { .. })
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TextBounds {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub baseline_offset: f32,
}
impl TextBounds {
pub fn new(x: f32, y: f32, width: f32, height: f32, baseline_offset: f32) -> Self {
Self { x, y, width, height, baseline_offset }
}
}
fn generate_line_quads(
quads: &mut Vec<DecorationQuad>,
x: f32,
y: f32,
width: f32,
thickness: f32,
color: Color,
style: LineStyle,
quad_type: DecorationQuadType,
) {
match style {
LineStyle::Solid => {
quads.push(DecorationQuad::new(x, y, width, thickness, color, quad_type));
}
LineStyle::Dashed => {
let dash_length = (4.0 * thickness).max(3.0);
let gap_length = (2.0 * thickness).max(2.0);
let segment_length = dash_length + gap_length;
let mut current_x = x;
while current_x < x + width {
let remaining = (x + width) - current_x;
let dash_width = dash_length.min(remaining);
if dash_width > 0.5 {
quads.push(DecorationQuad::new(
current_x,
y,
dash_width,
thickness,
color,
quad_type,
));
}
current_x += segment_length;
}
}
LineStyle::Dotted => {
let dot_size = thickness;
let dot_spacing = (2.0 * thickness).max(2.0);
let segment_length = dot_size + dot_spacing;
let mut current_x = x;
while current_x < x + width {
let remaining = (x + width) - current_x;
let dot_width = dot_size.min(remaining);
if dot_width > 0.5 {
quads.push(DecorationQuad::new(
current_x,
y,
dot_width,
thickness,
color,
quad_type,
));
}
current_x += segment_length;
}
}
LineStyle::Wavy => {
let wave_height = (thickness * 1.5).max(2.0); let wave_length = (thickness * 8.0).max(8.0); let segment_width = wave_length / 8.0;
let mut current_x = x;
let mut segment_index = 0;
while current_x < x + width {
let remaining = (x + width) - current_x;
let seg_width = segment_width.min(remaining);
if seg_width > 0.5 {
let phase = segment_index as f32 * segment_width / wave_length * 2.0 * std::f32::consts::PI;
let y_offset = phase.sin() * wave_height * 0.5;
quads.push(DecorationQuad::new(
current_x,
y + y_offset,
seg_width,
thickness,
color,
quad_type,
));
}
current_x += segment_width;
segment_index += 1;
}
}
}
}
pub fn generate_decoration_quads(bounds: &TextBounds, decoration: &TextDecoration) -> Vec<DecorationQuad> {
let mut quads = Vec::new();
if let Some(bg_color) = decoration.background {
let padding = &decoration.background_padding;
let x = bounds.x - padding[0];
let y = bounds.y - padding[1];
let width = bounds.width + padding[0] + padding[2];
let height = bounds.height + padding[1] + padding[3];
quads.push(DecorationQuad::background(x, y, width, height, bg_color));
}
if let Some(ul_style) = decoration.underline {
let baseline_y = bounds.y + bounds.baseline_offset;
let y = baseline_y + ul_style.offset;
let x = bounds.x;
let width = bounds.width;
let thickness = ul_style.thickness;
generate_line_quads(
&mut quads,
x,
y,
width,
thickness,
ul_style.color,
ul_style.style,
DecorationQuadType::Underline { thickness },
);
}
if let Some(st_style) = decoration.strikethrough {
let baseline_y = bounds.y + bounds.baseline_offset;
let y = baseline_y - (bounds.height * 0.35) + st_style.offset;
let x = bounds.x;
let width = bounds.width;
let thickness = st_style.thickness;
generate_line_quads(
&mut quads,
x,
y,
width,
thickness,
st_style.color,
st_style.style,
DecorationQuadType::Strikethrough { thickness },
);
}
quads
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_line_style_default() {
assert_eq!(LineStyle::default(), LineStyle::Solid);
}
#[test]
fn test_underline_style_solid() {
let style = UnderlineStyle::solid(Color::RED, 1.0);
assert_eq!(style.color, Color::RED);
assert_eq!(style.thickness, 1.0);
assert_eq!(style.style, LineStyle::Solid);
assert_eq!(style.offset, 2.0);
}
#[test]
fn test_underline_style_wavy() {
let style = UnderlineStyle::wavy(Color::BLUE, 2.0).with_offset(3.0);
assert_eq!(style.color, Color::BLUE);
assert_eq!(style.thickness, 2.0);
assert_eq!(style.style, LineStyle::Wavy);
assert_eq!(style.offset, 3.0);
}
#[test]
fn test_strikethrough_style_solid() {
let style = StrikethroughStyle::solid(Color::BLACK, 1.5);
assert_eq!(style.color, Color::BLACK);
assert_eq!(style.thickness, 1.5);
assert_eq!(style.style, LineStyle::Solid);
assert_eq!(style.offset, 0.0);
}
#[test]
fn test_text_decoration_default() {
let decoration = TextDecoration::default();
assert!(!decoration.has_decoration());
assert!(!decoration.has_underline());
assert!(!decoration.has_strikethrough());
assert!(!decoration.has_background());
}
#[test]
fn test_text_decoration_builder() {
let decoration = TextDecoration::new()
.underline(UnderlineStyle::solid(Color::RED, 1.0))
.strikethrough(StrikethroughStyle::solid(Color::BLACK, 1.0))
.background(Color::YELLOW);
assert!(decoration.has_decoration());
assert!(decoration.has_underline());
assert!(decoration.has_strikethrough());
assert!(decoration.has_background());
}
#[test]
fn test_decoration_geometry() {
let geom = DecorationGeometry::new((0.0, 0.0), (100.0, 0.0), 1.0, Color::RED, LineStyle::Solid);
assert_eq!(geom.length(), 100.0);
assert_eq!(geom.center(), (50.0, 0.0));
}
#[test]
fn test_background_geometry() {
let geom = BackgroundGeometry::new(10.0, 20.0, 100.0, 50.0, Color::YELLOW);
assert_eq!(geom.as_rect(), (10.0, 20.0, 100.0, 50.0));
assert_eq!(geom.color, Color::YELLOW);
}
#[test]
fn test_generate_decoration_geometry() {
let decoration = TextDecoration::new()
.underline(UnderlineStyle::solid(Color::RED, 1.0))
.strikethrough(StrikethroughStyle::solid(Color::BLACK, 1.0))
.background(Color::YELLOW);
let (bg, ul, st) = generate_decoration_geometry(&decoration, 100.0, 0.0, 200.0, 20.0);
assert!(bg.is_some());
assert!(ul.is_some());
assert!(st.is_some());
let bg = bg.unwrap();
assert_eq!(bg.color, Color::YELLOW);
let ul = ul.unwrap();
assert_eq!(ul.color, Color::RED);
assert_eq!(ul.start.0, 0.0);
assert_eq!(ul.end.0, 200.0);
let st = st.unwrap();
assert_eq!(st.color, Color::BLACK);
assert_eq!(st.start.0, 0.0);
assert_eq!(st.end.0, 200.0);
}
#[test]
fn test_background_padding() {
let decoration = TextDecoration::new()
.background(Color::YELLOW)
.background_padding_ltrb(5.0, 3.0, 5.0, 3.0);
let (bg, _, _) = generate_decoration_geometry(&decoration, 100.0, 0.0, 200.0, 20.0);
let bg = bg.unwrap();
let (x, _y, width, height) = bg.as_rect();
assert_eq!(x, -5.0); assert_eq!(width, 210.0); assert_eq!(height, 26.0); }
#[test]
fn test_solid_line_style() {
let bounds = TextBounds::new(0.0, 0.0, 100.0, 20.0, 15.0);
let decoration = TextDecoration::new()
.underline(UnderlineStyle::solid(Color::RED, 1.0));
let quads = generate_decoration_quads(&bounds, &decoration);
assert_eq!(quads.len(), 1);
assert!(quads[0].is_underline());
assert_eq!(quads[0].color, Color::RED);
}
#[test]
fn test_dashed_line_style() {
let bounds = TextBounds::new(0.0, 0.0, 100.0, 20.0, 15.0);
let decoration = TextDecoration::new()
.underline(UnderlineStyle::dashed(Color::BLUE, 2.0));
let quads = generate_decoration_quads(&bounds, &decoration);
assert!(quads.len() > 1, "Dashed line should generate multiple quads");
assert!(quads[0].is_underline());
assert_eq!(quads[0].color, Color::BLUE);
}
#[test]
fn test_dotted_line_style() {
let bounds = TextBounds::new(0.0, 0.0, 100.0, 20.0, 15.0);
let decoration = TextDecoration::new()
.underline(UnderlineStyle::dotted(Color::GREEN, 1.5));
let quads = generate_decoration_quads(&bounds, &decoration);
assert!(quads.len() > 1, "Dotted line should generate multiple quads");
assert!(quads[0].is_underline());
assert_eq!(quads[0].color, Color::GREEN);
}
#[test]
fn test_wavy_line_style() {
let bounds = TextBounds::new(0.0, 0.0, 100.0, 20.0, 15.0);
let decoration = TextDecoration::new()
.underline(UnderlineStyle::wavy(Color::YELLOW, 1.0));
let quads = generate_decoration_quads(&bounds, &decoration);
assert!(quads.len() > 1, "Wavy line should generate multiple quads");
assert!(quads[0].is_underline());
assert_eq!(quads[0].color, Color::YELLOW);
if quads.len() >= 2 {
let y_positions: Vec<f32> = quads.iter().map(|q| q.bounds.1).collect();
let all_same = y_positions.windows(2).all(|w| w[0] == w[1]);
assert!(!all_same, "Wavy line should have varying y positions");
}
}
#[test]
fn test_strikethrough_line_styles() {
let bounds = TextBounds::new(0.0, 0.0, 100.0, 20.0, 15.0);
let decoration = TextDecoration::new()
.strikethrough(StrikethroughStyle::solid(Color::BLACK, 1.0));
let quads = generate_decoration_quads(&bounds, &decoration);
assert_eq!(quads.len(), 1);
assert!(quads[0].is_strikethrough());
let decoration = TextDecoration::new()
.strikethrough(StrikethroughStyle::dashed(Color::BLACK, 1.0));
let quads = generate_decoration_quads(&bounds, &decoration);
assert!(quads.len() > 1);
assert!(quads[0].is_strikethrough());
}
#[test]
fn test_combined_decorations_with_line_styles() {
let bounds = TextBounds::new(0.0, 0.0, 100.0, 20.0, 15.0);
let decoration = TextDecoration::new()
.background(Color::YELLOW)
.underline(UnderlineStyle::wavy(Color::RED, 1.0))
.strikethrough(StrikethroughStyle::dashed(Color::BLACK, 1.0));
let quads = generate_decoration_quads(&bounds, &decoration);
assert!(quads.len() > 3, "Combined decorations should generate multiple quads");
assert!(quads[0].is_background());
assert_eq!(quads[0].color, Color::YELLOW);
}
}