zest_widget/widget/
span.rs1use super::Widget;
14use alloc::{borrow::Cow, vec::Vec};
15use core::marker::PhantomData;
16use embedded_graphics::{
17 mono_font::MonoFont, pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
18};
19use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase};
20use zest_theme::Theme;
21
22#[derive(Clone)]
28pub struct Span<'a, C: PixelColor> {
29 pub text: Cow<'a, str>,
31 pub color: Option<C>,
33 pub font: Option<&'a MonoFont<'a>>,
35}
36
37impl<'a, C: PixelColor> Span<'a, C> {
38 pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
41 Self {
42 text: text.into(),
43 color: None,
44 font: None,
45 }
46 }
47
48 #[must_use]
50 pub fn color(mut self, color: C) -> Self {
51 self.color = Some(color);
52 self
53 }
54
55 #[must_use]
57 pub fn font(mut self, font: &'a MonoFont<'a>) -> Self {
58 self.font = Some(font);
59 self
60 }
61}
62
63pub struct SpanGroup<'a, C: PixelColor, M: Clone> {
66 rect: Rectangle,
67 spans: Vec<Span<'a, C>>,
68 line_spacing: u32,
69 width: Length,
70 height: Length,
71 _phantom: PhantomData<M>,
72}
73
74impl<'a, C: PixelColor, M: Clone> SpanGroup<'a, C, M> {
75 pub fn new() -> Self {
77 Self {
78 rect: Rectangle::zero(),
79 spans: Vec::new(),
80 line_spacing: 2,
81 width: Length::Fill,
82 height: Length::Fill,
83 _phantom: PhantomData,
84 }
85 }
86
87 #[must_use]
89 pub fn push(mut self, span: Span<'a, C>) -> Self {
90 self.spans.push(span);
91 self
92 }
93
94 #[must_use]
97 pub fn span(mut self, text: impl Into<Cow<'a, str>>) -> Self {
98 self.spans.push(Span::new(text));
99 self
100 }
101
102 #[must_use]
104 pub fn line_spacing(mut self, spacing: u32) -> Self {
105 self.line_spacing = spacing;
106 self
107 }
108
109 #[must_use]
111 pub fn width(mut self, width: impl Into<Length>) -> Self {
112 self.width = width.into();
113 self
114 }
115
116 #[must_use]
118 pub fn height(mut self, height: impl Into<Length>) -> Self {
119 self.height = height.into();
120 self
121 }
122
123 fn line_height(&self, fallback: u32) -> u32 {
127 let mut h = fallback;
128 for span in &self.spans {
129 if let Some(font) = span.font {
130 h = h.max(font.character_size.height);
131 }
132 }
133 h
134 }
135}
136
137impl<'a, C: PixelColor, M: Clone> Default for SpanGroup<'a, C, M> {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143impl<'a, C: PixelColor, M: Clone> Widget<C, M> for SpanGroup<'a, C, M> {
144 fn measure(&mut self, constraints: Constraints) -> Size {
145 let w = self
146 .width
147 .resolve(constraints.max.width, constraints.max.width);
148 let h = self
149 .height
150 .resolve(constraints.max.height, constraints.max.height);
151 constraints.clamp(Size::new(w, h))
152 }
153
154 fn preferred_size(&self) -> (Length, Length) {
155 (self.width, self.height)
156 }
157
158 fn arrange(&mut self, rect: Rectangle) {
159 self.rect = rect;
160 }
161
162 fn rect(&self) -> Rectangle {
163 self.rect
164 }
165
166 fn handle_touch(&mut self, _point: Point, _phase: TouchPhase) -> Option<M> {
167 None
168 }
169
170 fn draw<'t>(
171 &self,
172 renderer: &mut dyn Renderer<C>,
173 theme: &Theme<'t, C>,
174 ) -> Result<(), RenderError> {
175 let default_font = theme.default_font();
176 let default_color = theme.background.on_base;
177 let line_h = self.line_height(default_font.character_size.height);
178
179 let left = self.rect.top_left.x;
180 let right = self.rect.top_left.x + self.rect.size.width as i32;
181 let bottom = self.rect.top_left.y + self.rect.size.height as i32;
182
183 let mut pen_x = left;
185 let mut pen_y = self.rect.top_left.y;
186
187 for span in &self.spans {
188 let font = span.font.unwrap_or(default_font);
189 let color = span.color.unwrap_or(default_color);
190 let glyph_w = font.character_size.width as i32;
191 let glyph_h = font.character_size.height as i32;
192
193 for word in split_keep_spaces(&span.text) {
197 let word_w = word.chars().count() as i32 * glyph_w;
198 if pen_x > left && pen_x + word_w > right {
201 pen_x = left;
202 pen_y += line_h as i32 + self.line_spacing as i32;
203 }
204 if pen_y >= bottom {
205 return Ok(());
206 }
207 let to_draw = if pen_x == left {
210 word.trim_start_matches(' ')
211 } else {
212 word
213 };
214 if !to_draw.is_empty() {
215 let baseline = pen_y + glyph_h;
218 renderer.draw_text(
219 to_draw,
220 Point::new(pen_x, baseline),
221 font,
222 color,
223 Alignment::Left,
224 )?;
225 pen_x += to_draw.chars().count() as i32 * glyph_w;
226 }
227 }
228 }
229 Ok(())
230 }
231}
232
233fn split_keep_spaces(s: &str) -> Vec<&str> {
238 let mut out = Vec::new();
239 let mut start = 0;
240 let bytes = s.as_bytes();
241 let mut i = 0;
242 while i < bytes.len() {
243 if bytes[i] == b' ' && i > start {
244 out.push(&s[start..i]);
245 start = i;
246 }
247 i += 1;
248 }
249 if start < s.len() {
250 out.push(&s[start..]);
251 }
252 out
253}