1use densa_core::Color;
2use std::borrow::Cow;
3use std::fmt;
4use std::ops::Range;
5
6pub const MAX_FONT_PX: f32 = 4096.0;
9
10pub const MAX_LINE_HEIGHT: f32 = 16.0;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15#[non_exhaustive]
16pub enum TextAlign {
17 #[default]
19 Start,
20 End,
22 Left,
24 Center,
26 Right,
28 Justify,
30}
31
32#[derive(Debug, Clone, PartialEq)]
37#[non_exhaustive]
38pub struct ParagraphStyle {
39 pub family: Cow<'static, str>,
41 pub size_px: f32,
43 pub line_height: f32,
45 pub color: Color,
47 pub align: TextAlign,
49 pub max_width: Option<f32>,
51}
52
53impl ParagraphStyle {
54 pub fn new(size_px: f32) -> Self {
56 Self {
57 family: Cow::Borrowed("monospace"),
58 size_px,
59 line_height: 1.3,
60 color: Color::WHITE,
61 align: TextAlign::Start,
62 max_width: None,
63 }
64 }
65
66 #[must_use]
68 pub fn with_family(mut self, family: impl Into<Cow<'static, str>>) -> Self {
69 self.family = family.into();
70 self
71 }
72
73 #[must_use]
75 pub fn with_color(mut self, color: Color) -> Self {
76 self.color = color;
77 self
78 }
79
80 #[must_use]
82 pub fn with_align(mut self, align: TextAlign) -> Self {
83 self.align = align;
84 self
85 }
86
87 #[must_use]
89 pub fn with_line_height(mut self, line_height: f32) -> Self {
90 self.line_height = line_height;
91 self
92 }
93
94 #[must_use]
96 pub fn with_max_width(mut self, max_width: Option<f32>) -> Self {
97 self.max_width = max_width;
98 self
99 }
100}
101
102#[derive(Debug, Clone, PartialEq)]
104#[non_exhaustive]
105pub struct Span {
106 pub range: Range<usize>,
108 pub color: Option<Color>,
110 pub size_px: Option<f32>,
112}
113
114impl Span {
115 pub fn new(range: Range<usize>) -> Self {
117 Self {
118 range,
119 color: None,
120 size_px: None,
121 }
122 }
123
124 #[must_use]
126 pub fn with_color(mut self, color: Color) -> Self {
127 self.color = Some(color);
128 self
129 }
130
131 #[must_use]
133 pub fn with_size(mut self, size_px: f32) -> Self {
134 self.size_px = Some(size_px);
135 self
136 }
137}
138
139#[derive(Debug, Clone, PartialEq)]
141#[non_exhaustive]
142pub enum ParagraphError {
143 InvalidSize(f32),
146 InvalidWidth(f32),
148 InvalidSpan(Range<usize>),
150 InvalidRange(Range<usize>),
153 UnreadableFont,
155}
156
157impl fmt::Display for ParagraphError {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 match self {
160 Self::InvalidSize(size) => write!(
161 f,
162 "size {size} is out of range: font sizes go above 0 up to {MAX_FONT_PX} px, \
163 line heights above 0 up to {MAX_LINE_HEIGHT} times the font size"
164 ),
165 Self::InvalidWidth(width) => {
166 write!(f, "width {width} is not a finite, non-negative number")
167 }
168 Self::InvalidSpan(range) => write!(
169 f,
170 "span {range:?} is reversed, outside the text, or splits a character"
171 ),
172 Self::InvalidRange(range) => write!(
173 f,
174 "range {range:?} is reversed, outside its text, or splits a character"
175 ),
176 Self::UnreadableFont => f.write_str("a font chosen for the text cannot be read"),
177 }
178 }
179}
180
181impl std::error::Error for ParagraphError {}
182
183pub(crate) fn check_font_size(size: f32) -> Result<f32, ParagraphError> {
184 in_range(size, MAX_FONT_PX)
185}
186
187pub(crate) fn check_line_height(line_height: f32) -> Result<f32, ParagraphError> {
188 in_range(line_height, MAX_LINE_HEIGHT)
189}
190
191fn in_range(value: f32, max: f32) -> Result<f32, ParagraphError> {
193 if value > 0.0 && value <= max {
194 Ok(value)
195 } else {
196 Err(ParagraphError::InvalidSize(value))
197 }
198}
199
200pub(crate) fn check_width(width: Option<f32>) -> Result<Option<f32>, ParagraphError> {
201 match width {
202 Some(w) if !(w.is_finite() && w >= 0.0) => Err(ParagraphError::InvalidWidth(w)),
203 _ => Ok(width),
204 }
205}
206
207pub(crate) fn check_span(text: &str, span: &Span) -> Result<(), ParagraphError> {
208 if !is_char_range(text, &span.range) {
209 return Err(ParagraphError::InvalidSpan(span.range.clone()));
210 }
211 span.size_px.map(check_font_size).transpose()?;
212 Ok(())
213}
214
215pub(crate) fn is_char_range(text: &str, range: &Range<usize>) -> bool {
217 range.start <= range.end
218 && range.end <= text.len()
219 && text.is_char_boundary(range.start)
220 && text.is_char_boundary(range.end)
221}