Skip to main content

anathema_default_widgets/
text.rs

1use 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/// Text alignment aligns the text inside its parent.
17///
18/// Given a border with a width of nine and text alignment set to [`TextAlignment::Right`]:
19/// ```text
20/// ┌───────┐
21/// │I would│
22/// │ like a│
23/// │ lovely│
24/// │ cup of│
25/// │    tea│
26/// │ please│
27/// └───────┘
28/// ```
29///
30/// The text will only align it self within the parent widget.
31#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)]
32pub enum TextAlignment {
33    /// Align the to the left inside the parent
34    #[default]
35    Left,
36    /// Align the text in the centre of the parent
37    Centre,
38    /// Align the to the right inside the parent
39    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/// Text widget
57/// ```ignore
58/// Attributes:
59/// * background
60/// * foreground
61/// * text-align
62/// * wrap
63/// ```
64///
65/// Note: Spans, unlike other widgets, does not require a widget id
66///
67/// A `Text` widget will be as wide as its text.
68#[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        // Layout text
88        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        // Layout text of all the sub-nodes
96        _ = 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                            // TODO:
145                            // In the future there should probably be a way to
146                            // provide both style and glyph at the same time.
147                            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        // NOTE
163        // No positioning is done in here, it's all done when painting
164    }
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        // Everything is handled by the parent text
179        Ok(Size::ZERO)
180    }
181
182    fn position<'bp>(&mut self, _: PositionChildren<'_, 'bp>, _: WidgetId, _: &AttributeStorage<'bp>, _: PositionCtx) {
183        // Everything is handled by the parent text
184        // panic!("this should never be called");
185    }
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}