anathema_default_widgets/
text.rs1use std::ops::ControlFlow;
2
3use anathema_geometry::{LocalPos, Size};
4use anathema_value_resolver::{AttributeStorage, ValueKind};
5use anathema_widgets::error::Result;
6use anathema_widgets::layout::text::{ProcessResult, Segment, Strings};
7use anathema_widgets::layout::{Constraints, LayoutCtx, PositionCtx};
8use anathema_widgets::paint::{Glyphs, PaintCtx, SizePos};
9use anathema_widgets::{LayoutForEach, PaintChildren, PositionChildren, Widget, WidgetId};
10
11use crate::{LEFT, RIGHT};
12
13pub(crate) const WRAP: &str = "wrap";
14pub(crate) const TEXT_ALIGN: &str = "text_align";
15
16#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)]
32pub enum TextAlignment {
33 #[default]
35 Left,
36 Centre,
38 Right,
40}
41
42impl TryFrom<&ValueKind<'_>> for TextAlignment {
43 type Error = ();
44
45 fn try_from(value: &ValueKind<'_>) -> Result<Self, Self::Error> {
46 let s = value.as_str().ok_or(())?;
47 match s {
48 LEFT => Ok(TextAlignment::Left),
49 RIGHT => Ok(TextAlignment::Right),
50 "centre" | "center" => Ok(TextAlignment::Centre),
51 _ => Err(()),
52 }
53 }
54}
55
56#[derive(Debug, Default)]
69pub struct Text {
70 strings: Strings,
71}
72
73impl Widget for Text {
74 fn layout<'bp>(
75 &mut self,
76 mut children: LayoutForEach<'_, 'bp>,
77 constraints: Constraints,
78 id: WidgetId,
79 ctx: &mut LayoutCtx<'_, 'bp>,
80 ) -> Result<Size> {
81 let attributes = ctx.attributes(id);
82 let wrap = attributes.get_as(WRAP).unwrap_or_default();
83 let size = constraints.max_size();
84 self.strings = Strings::new(size, wrap);
85 self.strings.set_style(id);
86
87 if let Some(text) = attributes.value() {
89 text.strings(|s| match self.strings.add_str(s) {
90 ProcessResult::Break => false,
91 ProcessResult::Continue => true,
92 });
93 }
94
95 _ = children.each(ctx, |ctx, child, _| {
97 let Some(_span) = child.try_to_ref::<Span>() else {
98 return Ok(ControlFlow::Continue(()));
99 };
100 self.strings.set_style(child.id());
101
102 let attributes = ctx.attributes(child.id());
103 if let Some(text) = attributes.value() {
104 text.strings(|s| match self.strings.add_str(s) {
105 ProcessResult::Break => false,
106 ProcessResult::Continue => true,
107 });
108 Ok(ControlFlow::Continue(()))
109 } else {
110 Ok(ControlFlow::Break(()))
111 }
112 })?;
113
114 Ok(self.strings.finish())
115 }
116
117 fn paint<'bp>(
118 &mut self,
119 _: PaintChildren<'_, 'bp>,
120 id: WidgetId,
121 attribute_storage: &AttributeStorage<'bp>,
122 mut ctx: PaintCtx<'_, SizePos>,
123 ) {
124 let lines = self.strings.lines();
125 let alignment = attribute_storage.get(id).get_as(TEXT_ALIGN).unwrap_or_default();
126
127 let mut pos = LocalPos::ZERO;
128 let mut style = attribute_storage.get(id);
129
130 for line in lines {
131 let x = match alignment {
132 TextAlignment::Left => 0,
133 TextAlignment::Centre => ctx.local_size.width / 2 - line.width / 2,
134 TextAlignment::Right => ctx.local_size.width - line.width,
135 };
136
137 pos.x = x;
138
139 for entry in line.entries {
140 match entry {
141 Segment::Str(s) => {
142 let glyphs = Glyphs::new(s);
143 if let Some(new_pos) = ctx.place_glyphs(glyphs, pos) {
144 for x in pos.x..new_pos.x {
148 ctx.set_attributes(style, (x, pos.y).into());
149 }
150 pos = new_pos;
151 }
152 }
153 Segment::SetStyle(attribute_id) => style = attribute_storage.get(attribute_id),
154 }
155 }
156 pos.y += 1;
157 pos.x = 0;
158 }
159 }
160
161 fn position<'bp>(&mut self, _: PositionChildren<'_, 'bp>, _: WidgetId, _: &AttributeStorage<'bp>, _: PositionCtx) {
162 }
165}
166
167#[derive(Default, Copy, Clone)]
168pub struct Span;
169
170impl Widget for Span {
171 fn layout<'bp>(
172 &mut self,
173 _: LayoutForEach<'_, 'bp>,
174 _: Constraints,
175 _: WidgetId,
176 _: &mut LayoutCtx<'_, 'bp>,
177 ) -> Result<Size> {
178 Ok(Size::ZERO)
180 }
181
182 fn position<'bp>(&mut self, _: PositionChildren<'_, 'bp>, _: WidgetId, _: &AttributeStorage<'bp>, _: PositionCtx) {
183 }
186}
187
188#[cfg(test)]
189mod test {
190 use crate::testing::TestRunner;
191
192 #[test]
193 fn word_wrap_excessive_space() {
194 let src = "text 'hello how are you'";
195 let expected = "
196 ╔════════════════╗
197 ║hello how ║
198 ║are you ║
199 ║ ║
200 ║ ║
201 ║ ║
202 ║ ║
203 ╚════════════════╝";
204
205 TestRunner::new(src, (16, 6)).instance().render_assert(expected);
206 }
207
208 #[test]
209 fn word_wrap() {
210 let src = "text 'hello how are you'";
211 let expected = r#"
212 ╔════════════════╗
213 ║hello how are ║
214 ║you ║
215 ║ ║
216 ╚════════════════╝
217 "#;
218
219 TestRunner::new(src, (16, 3)).instance().render_assert(expected);
220 }
221
222 #[test]
223 fn break_word_wrap() {
224 let src = "text [wrap: 'break'] 'hello howareyoudoing'";
225 let expected = r#"
226 ╔════════════════╗
227 ║hello howareyoud║
228 ║oing ║
229 ║ ║
230 ╚════════════════╝
231 "#;
232
233 TestRunner::new(src, (16, 3)).instance().render_assert(expected);
234 }
235
236 #[test]
237 fn char_wrap_layout_multiple_spans() {
238 let src = r#"
239 text 'one'
240 span 'two'
241 span ' averylongword'
242 span ' bunny'
243 "#;
244
245 let expected = r#"
246 ╔═══════════════════╗
247 ║onetwo ║
248 ║averylongword bunny║
249 ║ ║
250 ╚═══════════════════╝
251 "#;
252
253 TestRunner::new(src, (19, 3)).instance().render_assert(expected);
254 }
255
256 #[test]
257 fn multi_line_with_span() {
258 let src = r#"
259 border [width: 5 + 2]
260 text 'one'
261 span 'two'
262 "#;
263
264 let expected = r#"
265 ╔═════════╗
266 ║┌─────┐ ║
267 ║│onetw│ ║
268 ║│o │ ║
269 ║└─────┘ ║
270 ╚═════════╝
271 "#;
272
273 TestRunner::new(src, (9, 4)).instance().render_assert(expected);
274 }
275
276 #[test]
277 fn right_alignment() {
278 let src = "text [text_align: 'right'] 'a one xxxxxxxxxxxxxxxxxx'";
279 let expected = r#"
280 ╔══════════════════╗
281 ║ a one ║
282 ║xxxxxxxxxxxxxxxxxx║
283 ║ ║
284 ╚══════════════════╝
285 "#;
286
287 TestRunner::new(src, (18, 3)).instance().render_assert(expected);
288 }
289
290 #[test]
291 fn centre_alignment() {
292 let src = "text [text_align: 'centre'] 'a one xxxxxxxxxxxxxxxxxx'";
293 let expected = r#"
294 ╔══════════════════╗
295 ║ a one ║
296 ║xxxxxxxxxxxxxxxxxx║
297 ║ ║
298 ╚══════════════════╝
299 "#;
300
301 TestRunner::new(src, (18, 3)).instance().render_assert(expected);
302 }
303
304 #[test]
305 fn line_break() {
306 let src = "text 'What have you'";
307 let expected = r#"
308 ╔═════════╗
309 ║What have║
310 ║you ║
311 ║ ║
312 ╚═════════╝
313 "#;
314
315 TestRunner::new(src, (9, 3)).instance().render_assert(expected);
316 }
317}