use crate::style::computed::{CounterStyle, CounterStyleSystem, ListStyleType, VerticalAlign};
use crate::types::{Color, CornerRadii, EdgeSizes};
use super::engine::{CenteredStroke, InlineBox, InlineBoxPaint};
#[derive(Debug, Clone, Copy, Default)]
pub(crate) enum BuiltInBulletSlot {
#[default]
List,
StandaloneInside,
}
#[derive(Debug, Clone, Copy)]
enum BuiltInBulletShape {
Disc,
Circle,
Square,
}
impl BuiltInBulletShape {
const CIRCLE_STROKE_WIDTH: f32 = crate::fonts::PT_PER_CSS_PX;
fn from_style(style: &ListStyleType) -> Option<Self> {
match style {
ListStyleType::Disc => Some(Self::Disc),
ListStyleType::Circle => Some(Self::Circle),
ListStyleType::Square => Some(Self::Square),
_ => None,
}
}
fn paint(self, color: Color, size: f32) -> InlineBoxPaint {
let border_radii = match self {
Self::Disc | Self::Circle => CornerRadii::circular(size / 2.0),
Self::Square => CornerRadii::default(),
};
InlineBoxPaint {
background_color: matches!(self, Self::Disc | Self::Square).then_some(color),
border_radii,
centered_stroke: matches!(self, Self::Circle)
.then(|| CenteredStroke::solid(Self::CIRCLE_STROKE_WIDTH, color)),
..InlineBoxPaint::default()
}
}
}
#[derive(Debug, Clone, Copy)]
struct BuiltInBulletMetrics {
size: f32,
advance: f32,
center_above_baseline: f32,
}
impl BuiltInBulletMetrics {
fn from_font_size(font_size: f32, slot: BuiltInBulletSlot) -> Self {
let size = ((font_size / crate::fonts::PT_PER_CSS_PX) * 0.32).floor()
* crate::fonts::PT_PER_CSS_PX;
let advance = match slot {
BuiltInBulletSlot::List => font_size * (10.0 / 11.0),
BuiltInBulletSlot::StandaloneInside => font_size * (4.0 / 3.0),
};
Self {
size,
advance,
center_above_baseline: size - crate::fonts::PT_PER_CSS_PX / 2.0,
}
}
}
pub(crate) fn build_list_bullet_marker(
list_style_type: &ListStyleType,
font_size: f32,
color: Color,
slot: BuiltInBulletSlot,
) -> Option<InlineBox> {
let shape = BuiltInBulletShape::from_style(list_style_type)?;
let metrics = BuiltInBulletMetrics::from_font_size(font_size, slot);
Some(InlineBox {
width: metrics.size,
height: metrics.size,
margin_right: metrics.advance - metrics.size,
paint: shape.paint(color, metrics.size),
vertical_align: VerticalAlign::Baseline,
baseline_ascent: Some(metrics.center_above_baseline + metrics.size / 2.0),
padding: EdgeSizes::ZERO,
..InlineBox::default()
})
}
pub(crate) fn format_list_marker(list_style_type: &ListStyleType, index: i32) -> String {
match list_style_type {
ListStyleType::Disc => "\u{2022} ".to_string(),
ListStyleType::Circle => "\u{25E6} ".to_string(),
ListStyleType::Square => "\u{25AA} ".to_string(),
ListStyleType::Decimal => format!("{index}. "),
ListStyleType::DecimalLeadingZero => {
if index < 0 {
format!("-{:02}. ", (index as i64).unsigned_abs())
} else {
format!("{index:02}. ")
}
}
ListStyleType::LowerAlpha => format_positive_marker(index, to_alpha_lower),
ListStyleType::UpperAlpha => format_positive_marker(index, to_alpha_upper),
ListStyleType::LowerRoman => format_positive_marker(index, to_roman_lower),
ListStyleType::UpperRoman => format_positive_marker(index, to_roman_upper),
ListStyleType::CjkDecimal if index > 0 => {
format!("{}、", to_cjk_decimal(index as usize))
}
ListStyleType::CjkDecimal => format!("{index}、"),
ListStyleType::String(marker) => marker.clone(),
ListStyleType::CounterStyle(style) => format_custom_counter(style, index, true),
ListStyleType::Custom(_) => format!("{index}. "),
ListStyleType::None => String::new(),
}
}
fn format_positive_marker(index: i32, formatter: fn(usize) -> String) -> String {
if index <= 0 {
format!("{index}. ")
} else {
format!("{}. ", formatter(index as usize))
}
}
pub(crate) fn to_alpha_lower(n: usize) -> String {
if n == 0 {
return "a".to_string();
}
let mut result = String::new();
let mut val = n;
while val > 0 {
val -= 1;
result.insert(0, (b'a' + (val % 26) as u8) as char);
val /= 26;
}
result
}
pub(crate) fn to_alpha_upper(n: usize) -> String {
to_alpha_lower(n).to_uppercase()
}
pub(crate) fn to_roman_lower(n: usize) -> String {
let vals = [
(1000, "m"),
(900, "cm"),
(500, "d"),
(400, "cd"),
(100, "c"),
(90, "xc"),
(50, "l"),
(40, "xl"),
(10, "x"),
(9, "ix"),
(5, "v"),
(4, "iv"),
(1, "i"),
];
let mut result = String::new();
let mut remaining = n;
for &(value, numeral) in &vals {
while remaining >= value {
result.push_str(numeral);
remaining -= value;
}
}
if result.is_empty() {
"0".to_string()
} else {
result
}
}
pub(crate) fn to_roman_upper(n: usize) -> String {
to_roman_lower(n).to_uppercase()
}
fn to_cjk_decimal(n: usize) -> String {
const DIGITS: [&str; 10] = ["〇", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
if n == 0 {
return DIGITS[0].to_string();
}
n.to_string()
.chars()
.filter_map(|ch| ch.to_digit(10).map(|d| DIGITS[d as usize]))
.collect()
}
pub(crate) fn format_counter_value(style: &ListStyleType, value: i32) -> String {
if value <= 0 && !matches!(style, ListStyleType::CounterStyle(_)) {
return value.to_string();
}
let n = value as usize;
match style {
ListStyleType::DecimalLeadingZero => format!("{n:02}"),
ListStyleType::LowerAlpha => to_alpha_lower(n),
ListStyleType::UpperAlpha => to_alpha_upper(n),
ListStyleType::LowerRoman => to_roman_lower(n),
ListStyleType::UpperRoman => to_roman_upper(n),
ListStyleType::CjkDecimal => to_cjk_decimal(n),
ListStyleType::CounterStyle(custom) => format_custom_counter(custom, value, false),
_ => value.to_string(),
}
}
fn format_custom_counter(style: &CounterStyle, value: i32, include_affixes: bool) -> String {
let negative = value < 0;
let abs_value = (value as i64).unsigned_abs() as usize;
let mut representation = match style.system {
CounterStyleSystem::Cyclic if !style.symbols.is_empty() => {
let index = if abs_value == 0 {
0
} else {
(abs_value - 1) % style.symbols.len()
};
style.symbols[index].clone()
}
CounterStyleSystem::Cyclic | CounterStyleSystem::ExtendsDecimal => abs_value.to_string(),
};
if let Some((width, pad_symbol)) = &style.pad {
while representation.chars().count() < *width {
representation.insert_str(0, pad_symbol);
}
}
if negative {
representation = format!("{}{}{}", style.negative.0, representation, style.negative.1);
}
if include_affixes {
format!("{}{}{}", style.prefix, representation, style.suffix)
} else {
representation
}
}