Skip to main content

incular_text/
spans.rs

1//! Plain and rich text descriptions.
2
3use crate::{
4    TextAlign, TextEngine, TextLayout, TextLayoutOptions, TextOverflow, TextScaler, TextStyle,
5};
6use incular_core::Size;
7use std::{fmt, ops::Range, sync::Arc};
8
9/// A widget-independent inline child. The text crate stores a stable identity
10/// and measured size rather than depending on `incular-widgets`, which keeps
11/// the crate graph acyclic. Widgets can resolve the identity during layout.
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct WidgetSpan {
14    pub id: u64,
15    pub size: Size,
16    pub alignment: WidgetSpanAlignment,
17}
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
20pub enum WidgetSpanAlignment {
21    #[default]
22    Baseline,
23    AboveBaseline,
24    BelowBaseline,
25    Middle,
26    TextTop,
27    TextBottom,
28}
29
30impl WidgetSpan {
31    #[must_use]
32    pub const fn new(id: u64) -> Self {
33        Self {
34            id,
35            size: Size::ZERO,
36            alignment: WidgetSpanAlignment::Baseline,
37        }
38    }
39
40    #[must_use]
41    pub const fn sized(id: u64, size: Size) -> Self {
42        Self {
43            id,
44            size,
45            alignment: WidgetSpanAlignment::Baseline,
46        }
47    }
48
49    #[must_use]
50    pub const fn with_size(mut self, size: Size) -> Self {
51        self.size = size;
52        self
53    }
54
55    #[must_use]
56    pub const fn alignment(mut self, alignment: WidgetSpanAlignment) -> Self {
57        self.alignment = alignment;
58        self
59    }
60}
61
62/// A node in a rich text tree.
63#[derive(Clone, Debug, PartialEq)]
64#[allow(clippy::large_enum_variant)]
65pub enum InlineSpan {
66    Text(TextSpan),
67    Widget(WidgetSpan),
68}
69
70impl From<TextSpan> for InlineSpan {
71    fn from(value: TextSpan) -> Self {
72        Self::Text(value)
73    }
74}
75
76impl From<WidgetSpan> for InlineSpan {
77    fn from(value: WidgetSpan) -> Self {
78        Self::Widget(value)
79    }
80}
81
82/// A recursively nestable text node. Styles are inherited by descendants;
83/// inline widget spans occupy one object-replacement character in plain text.
84#[derive(Clone, Debug, PartialEq)]
85pub struct TextSpan {
86    pub text: String,
87    pub style: Option<TextStyle>,
88    pub children: Vec<InlineSpan>,
89}
90
91impl TextSpan {
92    #[must_use]
93    pub fn new(text: impl Into<String>) -> Self {
94        Self {
95            text: text.into(),
96            style: None,
97            children: Vec::new(),
98        }
99    }
100
101    #[must_use]
102    pub fn empty() -> Self {
103        Self::new("")
104    }
105
106    #[must_use]
107    pub fn style(mut self, style: TextStyle) -> Self {
108        self.style = Some(style);
109        self
110    }
111
112    #[must_use]
113    pub fn child(mut self, child: impl Into<InlineSpan>) -> Self {
114        self.children.push(child.into());
115        self
116    }
117
118    #[must_use]
119    pub fn children<I, S>(mut self, children: I) -> Self
120    where
121        I: IntoIterator<Item = S>,
122        S: Into<InlineSpan>,
123    {
124        self.children = children.into_iter().map(Into::into).collect();
125        self
126    }
127
128    #[must_use]
129    pub fn with_children<I, S>(text: impl Into<String>, children: I) -> Self
130    where
131        I: IntoIterator<Item = S>,
132        S: Into<InlineSpan>,
133    {
134        Self::new(text).children(children)
135    }
136
137    #[must_use]
138    pub fn plain_text(&self) -> String {
139        let mut output = String::new();
140        self.write_plain_text(&mut output);
141        output
142    }
143
144    #[must_use]
145    pub fn text_len(&self) -> usize {
146        self.plain_text().len()
147    }
148
149    /// Returns leaf text runs with inherited styles and byte ranges into the
150    /// flattened plain text. Widget spans are represented by `\u{fffc}`.
151    #[must_use]
152    pub fn flatten(&self, inherited: &TextStyle) -> Vec<StyledTextRun> {
153        let mut runs = Vec::new();
154        let mut offset = 0;
155        self.flatten_into(inherited, &mut offset, &mut runs);
156        runs
157    }
158
159    pub fn visit(&self, visitor: &mut impl TextSpanVisitor) {
160        self.visit_with(&TextStyle::default(), visitor);
161    }
162
163    fn write_plain_text(&self, output: &mut String) {
164        output.push_str(&self.text);
165        for child in &self.children {
166            match child {
167                InlineSpan::Text(span) => span.write_plain_text(output),
168                InlineSpan::Widget(_) => output.push('\u{fffc}'),
169            }
170        }
171    }
172
173    fn flatten_into(
174        &self,
175        inherited: &TextStyle,
176        offset: &mut usize,
177        runs: &mut Vec<StyledTextRun>,
178    ) {
179        let style = self
180            .style
181            .as_ref()
182            .map_or_else(|| inherited.clone(), |local| inherited.merge(local));
183        if !self.text.is_empty() {
184            let start = *offset;
185            *offset += self.text.len();
186            runs.push(StyledTextRun {
187                text: self.text.clone(),
188                style: style.clone(),
189                range: start..*offset,
190            });
191        }
192        for child in &self.children {
193            match child {
194                InlineSpan::Text(span) => span.flatten_into(&style, offset, runs),
195                InlineSpan::Widget(widget) => {
196                    let start = *offset;
197                    *offset += '\u{fffc}'.len_utf8();
198                    runs.push(StyledTextRun {
199                        text: '\u{fffc}'.to_string(),
200                        style: style.clone(),
201                        range: start..*offset,
202                    });
203                    visitor_placeholder(widget, &style, start..*offset, runs);
204                }
205            }
206        }
207    }
208
209    fn visit_with(&self, inherited: &TextStyle, visitor: &mut impl TextSpanVisitor) {
210        let style = self
211            .style
212            .as_ref()
213            .map_or_else(|| inherited.clone(), |local| inherited.merge(local));
214        if !self.text.is_empty() {
215            visitor.visit_text(&self.text, &style);
216        }
217        for child in &self.children {
218            match child {
219                InlineSpan::Text(span) => span.visit_with(&style, visitor),
220                InlineSpan::Widget(widget) => visitor.visit_widget(*widget, &style),
221            }
222        }
223    }
224}
225
226/// A flattened leaf with inherited style information.
227#[derive(Clone, Debug, PartialEq)]
228pub struct StyledTextRun {
229    pub text: String,
230    pub style: TextStyle,
231    pub range: Range<usize>,
232}
233
234/// Visitor for rich text leaves. Both methods have defaults so callers can
235/// collect only the kind of leaf they need.
236pub trait TextSpanVisitor {
237    fn visit_text(&mut self, _text: &str, _style: &TextStyle) {}
238    fn visit_widget(&mut self, _widget: WidgetSpan, _style: &TextStyle) {}
239}
240
241// A widget leaf is already represented by a StyledTextRun. This helper keeps
242// the flattening loop explicit without requiring a second public run variant.
243fn visitor_placeholder(
244    _widget: &WidgetSpan,
245    _style: &TextStyle,
246    _range: Range<usize>,
247    _runs: &mut Vec<StyledTextRun>,
248) {
249}
250
251/// A rich paragraph with paragraph-level alignment and scaling.
252#[derive(Clone, Debug, PartialEq)]
253pub struct RichText {
254    pub text: TextSpan,
255    pub text_align: TextAlign,
256    pub text_scaler: TextScaler,
257    pub max_lines: Option<usize>,
258    pub soft_wrap: bool,
259    pub overflow: TextOverflow,
260}
261
262impl RichText {
263    #[must_use]
264    pub fn new(text: TextSpan) -> Self {
265        Self {
266            text,
267            text_align: TextAlign::Start,
268            text_scaler: TextScaler::default(),
269            max_lines: None,
270            soft_wrap: true,
271            overflow: TextOverflow::Clip,
272        }
273    }
274
275    #[must_use]
276    pub fn text_align(mut self, text_align: TextAlign) -> Self {
277        self.text_align = text_align;
278        self
279    }
280
281    #[must_use]
282    pub fn align(self, text_align: TextAlign) -> Self {
283        self.text_align(text_align)
284    }
285
286    #[must_use]
287    pub fn text_scaler(mut self, text_scaler: TextScaler) -> Self {
288        self.text_scaler = text_scaler;
289        self
290    }
291
292    #[must_use]
293    pub fn max_lines(mut self, max_lines: Option<usize>) -> Self {
294        self.max_lines = max_lines;
295        self
296    }
297
298    #[must_use]
299    pub fn soft_wrap(mut self, soft_wrap: bool) -> Self {
300        self.soft_wrap = soft_wrap;
301        self
302    }
303
304    #[must_use]
305    pub fn overflow(mut self, overflow: TextOverflow) -> Self {
306        self.overflow = overflow;
307        self
308    }
309
310    #[must_use]
311    pub fn plain_text(&self) -> String {
312        self.text.plain_text()
313    }
314
315    #[must_use]
316    pub fn flatten(&self) -> Vec<StyledTextRun> {
317        self.text
318            .flatten(&TextStyle::default().scaled(self.text_scaler))
319    }
320
321    #[must_use]
322    pub fn layout(&self, engine: &mut TextEngine, max_width: Option<f32>) -> Arc<TextLayout> {
323        let style = self
324            .flatten()
325            .first()
326            .map_or_else(TextStyle::default, |run| run.style.clone());
327        engine.layout_with_options(
328            &self.plain_text(),
329            &style,
330            TextLayoutOptions::new(max_width, self.text_align)
331                .soft_wrap(self.soft_wrap)
332                .max_lines(self.max_lines)
333                .overflow(self.overflow),
334        )
335    }
336}
337
338/// A plain paragraph description. Rendering code can turn it into a
339/// [`TextSpan`] without losing the familiar `Text::new("...")` API.
340#[derive(Clone, Debug, PartialEq)]
341pub struct Text {
342    pub text: String,
343    pub style: TextStyle,
344    pub text_align: TextAlign,
345    pub text_scaler: TextScaler,
346    pub max_lines: Option<usize>,
347    pub soft_wrap: bool,
348    pub overflow: TextOverflow,
349}
350
351impl Text {
352    #[must_use]
353    pub fn new(text: impl Into<String>) -> Self {
354        Self {
355            text: text.into(),
356            style: TextStyle::default(),
357            text_align: TextAlign::Start,
358            text_scaler: TextScaler::default(),
359            max_lines: None,
360            soft_wrap: true,
361            overflow: TextOverflow::Clip,
362        }
363    }
364
365    #[must_use]
366    pub fn style(mut self, style: TextStyle) -> Self {
367        self.style = style;
368        self
369    }
370
371    #[must_use]
372    pub fn align(mut self, align: TextAlign) -> Self {
373        self.text_align = align;
374        self
375    }
376
377    #[must_use]
378    pub fn text_align(self, align: TextAlign) -> Self {
379        self.align(align)
380    }
381
382    #[must_use]
383    pub fn text_scaler(mut self, scaler: TextScaler) -> Self {
384        self.text_scaler = scaler;
385        self
386    }
387
388    #[must_use]
389    pub fn max_lines(mut self, max_lines: Option<usize>) -> Self {
390        self.max_lines = max_lines;
391        self
392    }
393
394    #[must_use]
395    pub fn soft_wrap(mut self, soft_wrap: bool) -> Self {
396        self.soft_wrap = soft_wrap;
397        self
398    }
399
400    #[must_use]
401    pub fn overflow(mut self, overflow: TextOverflow) -> Self {
402        self.overflow = overflow;
403        self
404    }
405
406    #[must_use]
407    pub fn span(&self) -> TextSpan {
408        TextSpan::new(self.text.clone()).style(self.style.clone())
409    }
410
411    #[must_use]
412    pub fn rich_text(&self) -> RichText {
413        RichText::new(self.span())
414            .align(self.text_align)
415            .text_scaler(self.text_scaler)
416            .max_lines(self.max_lines)
417            .soft_wrap(self.soft_wrap)
418            .overflow(self.overflow)
419    }
420
421    #[must_use]
422    pub fn layout(&self, engine: &mut TextEngine, max_width: Option<f32>) -> Arc<TextLayout> {
423        engine.layout_with_options(
424            &self.text,
425            &self.style.scaled(self.text_scaler),
426            TextLayoutOptions::new(max_width, self.text_align)
427                .soft_wrap(self.soft_wrap)
428                .max_lines(self.max_lines)
429                .overflow(self.overflow),
430        )
431    }
432}
433
434impl From<&str> for Text {
435    fn from(value: &str) -> Self {
436        Self::new(value)
437    }
438}
439
440impl From<String> for Text {
441    fn from(value: String) -> Self {
442        Self::new(value)
443    }
444}
445
446impl fmt::Display for Text {
447    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
448        formatter.write_str(&self.text)
449    }
450}