use densa_core::Color;
use std::borrow::Cow;
use std::fmt;
use std::ops::Range;
pub const MAX_FONT_PX: f32 = 4096.0;
pub const MAX_LINE_HEIGHT: f32 = 16.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum TextAlign {
#[default]
Start,
End,
Left,
Center,
Right,
Justify,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ParagraphStyle {
pub family: Cow<'static, str>,
pub size_px: f32,
pub line_height: f32,
pub color: Color,
pub align: TextAlign,
pub max_width: Option<f32>,
}
impl ParagraphStyle {
pub fn new(size_px: f32) -> Self {
Self {
family: Cow::Borrowed("monospace"),
size_px,
line_height: 1.3,
color: Color::WHITE,
align: TextAlign::Start,
max_width: None,
}
}
#[must_use]
pub fn with_family(mut self, family: impl Into<Cow<'static, str>>) -> Self {
self.family = family.into();
self
}
#[must_use]
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
#[must_use]
pub fn with_align(mut self, align: TextAlign) -> Self {
self.align = align;
self
}
#[must_use]
pub fn with_line_height(mut self, line_height: f32) -> Self {
self.line_height = line_height;
self
}
#[must_use]
pub fn with_max_width(mut self, max_width: Option<f32>) -> Self {
self.max_width = max_width;
self
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Span {
pub range: Range<usize>,
pub color: Option<Color>,
pub size_px: Option<f32>,
}
impl Span {
pub fn new(range: Range<usize>) -> Self {
Self {
range,
color: None,
size_px: None,
}
}
#[must_use]
pub fn with_color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
#[must_use]
pub fn with_size(mut self, size_px: f32) -> Self {
self.size_px = Some(size_px);
self
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ParagraphError {
InvalidSize(f32),
InvalidWidth(f32),
InvalidSpan(Range<usize>),
InvalidRange(Range<usize>),
UnreadableFont,
}
impl fmt::Display for ParagraphError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidSize(size) => write!(f, "size {size} is out of the supported range"),
Self::InvalidWidth(width) => {
write!(f, "width {width} is not a finite, non-negative number")
}
Self::InvalidSpan(range) => write!(
f,
"span {range:?} is reversed, outside the text, or splits a character"
),
Self::InvalidRange(range) => write!(
f,
"range {range:?} is reversed, outside its text, or splits a character"
),
Self::UnreadableFont => f.write_str("a font chosen for the text cannot be read"),
}
}
}
impl std::error::Error for ParagraphError {}
pub(crate) fn check_font_size(size: f32) -> Result<f32, ParagraphError> {
in_range(size, MAX_FONT_PX)
}
pub(crate) fn check_line_height(line_height: f32) -> Result<f32, ParagraphError> {
in_range(line_height, MAX_LINE_HEIGHT)
}
fn in_range(value: f32, max: f32) -> Result<f32, ParagraphError> {
if value > 0.0 && value <= max {
Ok(value)
} else {
Err(ParagraphError::InvalidSize(value))
}
}
pub(crate) fn check_width(width: Option<f32>) -> Result<Option<f32>, ParagraphError> {
match width {
Some(w) if !(w.is_finite() && w >= 0.0) => Err(ParagraphError::InvalidWidth(w)),
_ => Ok(width),
}
}
pub(crate) fn check_span(text: &str, span: &Span) -> Result<(), ParagraphError> {
if !is_char_range(text, &span.range) {
return Err(ParagraphError::InvalidSpan(span.range.clone()));
}
span.size_px.map(check_font_size).transpose()?;
Ok(())
}
pub(crate) fn is_char_range(text: &str, range: &Range<usize>) -> bool {
range.start <= range.end
&& range.end <= text.len()
&& text.is_char_boundary(range.start)
&& text.is_char_boundary(range.end)
}