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!(f, "size {size} is out of the supported range"),
161 Self::InvalidWidth(width) => {
162 write!(f, "width {width} is not a finite, non-negative number")
163 }
164 Self::InvalidSpan(range) => write!(
165 f,
166 "span {range:?} is reversed, outside the text, or splits a character"
167 ),
168 Self::InvalidRange(range) => write!(
169 f,
170 "range {range:?} is reversed, outside its text, or splits a character"
171 ),
172 Self::UnreadableFont => f.write_str("a font chosen for the text cannot be read"),
173 }
174 }
175}
176
177impl std::error::Error for ParagraphError {}
178
179pub(crate) fn check_font_size(size: f32) -> Result<f32, ParagraphError> {
180 in_range(size, MAX_FONT_PX)
181}
182
183pub(crate) fn check_line_height(line_height: f32) -> Result<f32, ParagraphError> {
184 in_range(line_height, MAX_LINE_HEIGHT)
185}
186
187fn in_range(value: f32, max: f32) -> Result<f32, ParagraphError> {
189 if value > 0.0 && value <= max {
190 Ok(value)
191 } else {
192 Err(ParagraphError::InvalidSize(value))
193 }
194}
195
196pub(crate) fn check_width(width: Option<f32>) -> Result<Option<f32>, ParagraphError> {
197 match width {
198 Some(w) if !(w.is_finite() && w >= 0.0) => Err(ParagraphError::InvalidWidth(w)),
199 _ => Ok(width),
200 }
201}
202
203pub(crate) fn check_span(text: &str, span: &Span) -> Result<(), ParagraphError> {
204 if !is_char_range(text, &span.range) {
205 return Err(ParagraphError::InvalidSpan(span.range.clone()));
206 }
207 span.size_px.map(check_font_size).transpose()?;
208 Ok(())
209}
210
211pub(crate) fn is_char_range(text: &str, range: &Range<usize>) -> bool {
213 range.start <= range.end
214 && range.end <= text.len()
215 && text.is_char_boundary(range.start)
216 && text.is_char_boundary(range.end)
217}