duat_core/text/
builder.rs

1//! [`Text`] building macros
2//!
3//! This module contains 4 macros for text building: [`text!`],
4//! [`err!`], [`ok!`] and [`hint!`]. These are supposed to be used in
5//! various contexts, and they have differences on what the `Default`
6//! and `Accent` form.
7use std::{
8    fmt::{Alignment, Display, Write},
9    marker::PhantomData,
10    path::PathBuf,
11};
12
13use super::{Change, Key, Tag, Text};
14use crate::{data::RwData, form::FormId};
15
16/// Builds and modifies a [`Text`], based on replacements applied
17/// to it.
18///
19/// This struct is meant to be used alongside the [`text!`] family of
20/// macros. You pass it as the first argument, and the [`Text`] will
21/// be extended by the macro. This lets you write a [`Text`] with
22/// multiple macro invocations:
23///
24/// ```rust
25/// # use duat_core::text::{Text, hint};
26/// fn is_more_than_two(num: usize) -> Text {
27///     let mut builder = Text::builder();
28///     hint!(builder, "The number " [*a] num [] " is ");
29///     if num > 2 {
30///         hint!(builder, [*a] "more" [] " than 2.");
31///     } else {
32///         hint!(builder, [*a] "not more" [] " than 2.");
33///     }
34///     builder.finish()
35/// }
36/// ```
37///
38/// [`impl Display`]: std::fmt::Display
39/// [tag]: AlignCenter
40/// [`Form`]: crate::form::Form
41pub struct Builder {
42    text: Text,
43    last_form: Option<(usize, FormId)>,
44    last_align: Option<(usize, Alignment)>,
45    buffer: String,
46    last_was_empty: bool,
47}
48
49impl Builder {
50    /// Returns a new instance of [`Builder`]
51    ///
52    /// Use [`Text::builder`] if you don't want to bring [`Builder`]
53    /// into scope.
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// Finish construction and returns the [`Text`]
59    ///
60    /// Will also finish the last [`Form`] tag, pushing a [`PopForm`]
61    /// at the very end.
62    ///
63    /// [`Form`]: crate::form::Form
64    /// [`PopForm`]: Tag::PopForm
65    pub fn finish(mut self) -> Text {
66        if (self.text.buffers(..).next_back()).is_none_or(|b| b != b'\n') {
67            self.push_str("\n");
68            self.text.0.forced_new_line = true;
69        }
70
71        let end = self.text.len().byte();
72        if let Some((b, id)) = self.last_form {
73            self.text.insert_tag(Key::basic(), Tag::Form(b..end, id, 0));
74        }
75        match self.last_align {
76            Some((b, Alignment::Center)) => {
77                self.text.insert_tag(Key::basic(), Tag::AlignCenter(b..end));
78            }
79            Some((b, Alignment::Right)) => {
80                self.text.insert_tag(Key::basic(), Tag::AlignRight(b..end));
81            }
82            Some(_) | None => {}
83        }
84
85        self.text
86    }
87
88    /// Pushes a part of the text
89    ///
90    /// This can be an [`impl Display`] type, a [`RwData`] holding
91    /// an [`impl Display`] or a [tag surrogate].
92    ///
93    /// [`impl Display`]: std::fmt::Display
94    /// [tag surrogate]: Ghost
95    pub fn push<D: Display, _T>(&mut self, part: impl Into<BuilderPart<D, _T>>) {
96        let part = part.into();
97        let end = self.text.len().byte();
98        match part {
99            BuilderPart::Text(text) => self.push_text(text),
100            BuilderPart::ToString(display) => self.push_str(display),
101            BuilderPart::Form(id) => {
102                let last_form = match id == crate::form::DEFAULT_ID {
103                    true => self.last_form.take(),
104                    false => self.last_form.replace((end, id)),
105                };
106
107                if let Some((b, id)) = last_form {
108                    self.text.insert_tag(Key::basic(), Tag::Form(b..end, id, 0));
109                }
110            }
111            BuilderPart::AlignLeft => match self.last_align.take() {
112                Some((b, Alignment::Center)) => {
113                    self.text.insert_tag(Key::basic(), Tag::AlignCenter(b..end));
114                }
115                Some((b, Alignment::Right)) => {
116                    self.text.insert_tag(Key::basic(), Tag::AlignRight(b..end));
117                }
118                _ => {}
119            },
120            BuilderPart::AlignCenter => match self.last_align.take() {
121                Some((b, Alignment::Center)) => self.last_align = Some((b, Alignment::Center)),
122                Some((b, Alignment::Right)) => {
123                    self.text.insert_tag(Key::basic(), Tag::AlignRight(b..end));
124                }
125                None => self.last_align = Some((end, Alignment::Center)),
126                Some(_) => {}
127            },
128            BuilderPart::AlignRight => match self.last_align.take() {
129                Some((b, Alignment::Right)) => self.last_align = Some((b, Alignment::Right)),
130                Some((b, Alignment::Center)) => {
131                    self.text.insert_tag(Key::basic(), Tag::AlignCenter(b..end));
132                }
133                None => self.last_align = Some((end, Alignment::Right)),
134                Some(_) => {}
135            },
136            BuilderPart::Spacer(_) => self.text.insert_tag(Key::basic(), Tag::Spacer(end)),
137            BuilderPart::Ghost(text) => self.text.insert_tag(Key::basic(), Tag::Ghost(end, text)),
138        }
139    }
140
141    /// Whether or not the last added piece was empty
142    ///
143    /// This happens when an empty [`String`] or an empty [`Text`] is
144    /// pushed.
145    pub fn last_was_empty(&self) -> bool {
146        self.last_was_empty
147    }
148
149    /// Pushes an [`impl Display`] to the [`Text`]
150    ///
151    /// [`impl Display`]: std::fmt::Display
152    pub(crate) fn push_str<D: Display>(&mut self, d: D) {
153        self.buffer.clear();
154        write!(self.buffer, "{d}").unwrap();
155        if self.buffer.is_empty() {
156            self.last_was_empty = true;
157        } else {
158            self.last_was_empty = false;
159            let end = self.text.len();
160            self.text
161                .apply_change_inner(0, Change::str_insert(&self.buffer, end));
162        }
163    }
164
165    /// Pushes [`Text`] directly
166    pub(crate) fn push_text(&mut self, mut text: Text) {
167        if text.0.forced_new_line {
168            let change = Change::remove_nl(text.last_point().unwrap());
169            text.apply_change_inner(0, change);
170            text.0.forced_new_line = false;
171        }
172        self.last_was_empty = text.is_empty();
173
174        if let Some((b, id)) = self.last_form.take() {
175            self.text
176                .insert_tag(Key::basic(), Tag::Form(b..self.text.len().byte(), id, 0));
177        }
178
179        self.text.0.bytes.extend(text.0.bytes);
180        self.text.0.tags.extend(text.0.tags);
181    }
182}
183
184impl Default for Builder {
185    fn default() -> Self {
186        Builder {
187            text: Text::empty(),
188            last_form: None,
189            last_align: None,
190            buffer: String::with_capacity(50),
191            last_was_empty: false,
192        }
193    }
194}
195
196/// [`Builder`] part: Aligns the line centrally
197#[derive(Clone)]
198pub struct AlignCenter;
199/// [`Builder`] part: Aligns the line on the left
200#[derive(Clone)]
201pub struct AlignLeft;
202/// [`Builder`] part: Aligns the line on the right, which is the
203/// default
204#[derive(Clone)]
205pub struct AlignRight;
206/// [`Builder`] part: A spacer for more advanced alignment
207///
208/// When printing this screen line (one row on screen, i.e. until
209/// it wraps), Instead of following the current alignment, will
210/// put spacing between the next and previous characters. The
211/// length of the space will be roughly equal to the available
212/// space on this line divided by the number of [`Spacer`]s on it.
213///
214/// # Example
215///
216/// Let's say that this is the line being printed:
217///
218/// ```text
219/// This is my line,please,pretend it has tags
220/// ```
221///
222/// If we were to print it with `{Spacer}`s like this:
223///
224/// ```text
225/// This is my line,{Spacer}please,{Spacer}pretend it has tags
226/// ```
227///
228/// In a screen with a width of 50, it would come out like:
229///
230/// ```text
231/// This is my line,    please,    pretend it has tags
232/// ```
233#[derive(Clone)]
234pub struct Spacer;
235/// [`Builder`] part: Places ghost text
236///
237/// This is useful when, for example, creating command line prompts,
238/// since the text is non interactable.
239#[derive(Clone)]
240pub struct Ghost(pub Text);
241
242/// A part to be pushed to a [`Builder`] by a macro
243#[derive(Clone)]
244pub enum BuilderPart<D: Display, _T = String> {
245    Text(Text),
246    ToString(D),
247    Form(FormId),
248    AlignLeft,
249    AlignCenter,
250    AlignRight,
251    Spacer(PhantomData<_T>),
252    Ghost(Text),
253}
254
255impl From<FormId> for BuilderPart<String, FormId> {
256    fn from(value: FormId) -> Self {
257        Self::Form(value)
258    }
259}
260
261impl From<AlignLeft> for BuilderPart<String, AlignLeft> {
262    fn from(_: AlignLeft) -> Self {
263        BuilderPart::AlignLeft
264    }
265}
266
267impl From<AlignCenter> for BuilderPart<String, AlignCenter> {
268    fn from(_: AlignCenter) -> Self {
269        BuilderPart::AlignCenter
270    }
271}
272
273impl From<AlignRight> for BuilderPart<String, AlignRight> {
274    fn from(_: AlignRight) -> Self {
275        BuilderPart::AlignRight
276    }
277}
278
279impl From<Spacer> for BuilderPart<String, Spacer> {
280    fn from(_: Spacer) -> Self {
281        BuilderPart::Spacer(PhantomData)
282    }
283}
284
285impl From<Ghost> for BuilderPart<String, Ghost> {
286    fn from(value: Ghost) -> Self {
287        BuilderPart::Ghost(value.0)
288    }
289}
290
291impl From<Text> for BuilderPart<String, Text> {
292    fn from(value: Text) -> Self {
293        BuilderPart::Text(value)
294    }
295}
296
297impl<D: Display> From<&RwData<D>> for BuilderPart<String, D> {
298    fn from(value: &RwData<D>) -> Self {
299        BuilderPart::ToString(value.read().to_string())
300    }
301}
302
303impl<D: Display> From<D> for BuilderPart<D, D> {
304    fn from(value: D) -> Self {
305        BuilderPart::ToString(value)
306    }
307}
308
309impl From<PathBuf> for BuilderPart<String, PathBuf> {
310    fn from(value: PathBuf) -> Self {
311        BuilderPart::Text(Text::from(&value))
312    }
313}
314
315impl From<&PathBuf> for BuilderPart<String, PathBuf> {
316    fn from(value: &PathBuf) -> Self {
317        BuilderPart::Text(Text::from(value))
318    }
319}
320
321impl From<RwData<PathBuf>> for BuilderPart<String, PathBuf> {
322    fn from(value: RwData<PathBuf>) -> Self {
323        BuilderPart::Text(Text::from(&*value.read()))
324    }
325}
326
327/// The standard [`Text`] construction macro
328///
329/// TODO: Docs
330pub macro text {
331    // Forms
332    (@push $builder:expr, []) => {
333        let id = crate::form::id_of!("Default");
334        $builder.push(crate::text::Tag::PushForm(id, 0))
335    },
336    (@push $builder:expr, [*a]) => {
337        let id = crate::form::id_of!("Accent");
338        $builder.push(crate::text::Tag::PushForm(id, 0))
339    },
340    (@push $builder:expr, [$form:ident $(.$suffix:ident)*]) => {
341        let id = crate::form::id_of!(concat!(
342            stringify!($form) $(, stringify!(.), stringify!($suffix))*
343        ));
344        $builder.push(id)
345    },
346    (@push $builder:expr, [$($other:tt)+]) => {
347        compile_error!(concat!(
348            "Forms should be a list of identifiers separated by '.'s, received \"",
349             stringify!($($other)+),
350             "\" instead"
351        ))
352    },
353
354    // Plain text
355    (@push $builder:expr, $part:expr) => {
356        $builder.push($part)
357    },
358
359    (@parse $builder:expr, $part:tt $($parts:tt)*) => {{
360        text!(@push $builder, $part);
361        text!(@parse $builder, $($parts)*);
362    }},
363    (@parse $builder:expr,) => {},
364
365    ($builder:expr, $($parts:tt)+) => {{
366        let builder: &mut Builder = &mut $builder;
367        text!(@parse builder, $($parts)+);
368    }},
369    ($($parts:tt)+) => {{
370        let mut builder = Builder::new();
371        text!(builder, $($parts)+);
372        builder.finish()
373    }},
374}
375
376/// The standard [`Text`] construction macro
377///
378/// TODO: Docs
379pub macro ok {
380    // Forms
381    (@push $builder:expr, []) => {
382        $builder.push(crate::form::id_of!("DefaultOk"))
383    },
384    (@push $builder:expr, [*a]) => {
385        $builder.push(crate::form::id_of!("AccentOk"))
386    },
387    (@push $builder:expr, [$form:ident $(.$suffix:ident)*]) => {
388        $builder.push(crate::form::id_of!(concat!(
389            stringify!($form) $(, stringify!(.), stringify!($suffix))*
390        )));
391    },
392    (@push $builder:expr, [$($other:tt)+]) => {
393        compile_error!(concat!(
394            "Forms should be a list of identifiers separated by '.'s, received \"",
395             stringify!($($other)+),
396             "\" instead"
397        ))
398    },
399
400    // Plain text
401    (@push $builder:expr, $part:expr) => {
402        $builder.push($part)
403    },
404
405    (@parse $builder:expr, $part:tt $($parts:tt)*) => {{
406        ok!(@push $builder, $part);
407        ok!(@parse $builder, $($parts)*);
408    }},
409    (@parse $builder:expr,) => {},
410
411    ($builder:expr, $($parts:tt)+) => {{
412        let builder: &mut Builder = &mut $builder;
413        ok!(@parse builder, $($parts)+);
414    }},
415    ($($parts:tt)+) => {{
416        let mut builder = Builder::new();
417        ok!(builder, [DefaultOk] $($parts)+);
418        builder.finish()
419    }},
420}
421
422/// The standard [`Text`] construction macro
423///
424/// TODO: Docs
425pub macro err {
426    // Forms
427    (@push $builder:expr, []) => {
428        $builder.push(crate::form::id_of!("DefaultErr"))
429    },
430    (@push $builder:expr, [*a]) => {
431        $builder.push(crate::form::id_of!("AccentErr"))
432    },
433    (@push $builder:expr, [$form:ident $(.$suffix:ident)*]) => {
434        $builder.push(crate::form::id_of!(concat!(
435            stringify!($form) $(, stringify!(.), stringify!($suffix))*
436        )));
437    },
438    (@push $builder:expr, [$($other:tt)+]) => {
439        compile_error!(concat!(
440            "Forms should be a list of identifiers separated by '.'s, received \"",
441             stringify!($($other)+),
442             "\" instead"
443        ))
444    },
445
446    // Plain text
447    (@push $builder:expr, $part:expr) => {
448        $builder.push($part)
449    },
450
451    (@parse $builder:expr, $part:tt $($parts:tt)*) => {{
452        err!(@push $builder, $part);
453        err!(@parse $builder, $($parts)*);
454    }},
455    (@parse $builder:expr,) => {},
456
457    ($builder:expr, $($parts:tt)+) => {{
458        let builder: &mut Builder = &mut $builder;
459        err!(@parse builder, $($parts)+);
460    }},
461    ($($parts:tt)+) => {{
462        let mut builder = Builder::new();
463        err!(builder, [DefaultErr] $($parts)+);
464        builder.finish()
465    }},
466}
467
468/// The standard [`Text`] construction macro
469///
470/// TODO: Docs
471pub macro hint {
472    // Forms
473    (@push $builder:expr, []) => {
474        $builder.push(crate::form::id_of!("DefaultHint"))
475    },
476    (@push $builder:expr, [*a]) => {
477        $builder.push(crate::form::id_of!("AccentHint"))
478    },
479    (@push $builder:expr, [$form:ident $(.$suffix:ident)*]) => {
480        $builder.push(crate::form::id_of!(concat!(
481            stringify!($form) $(, stringify!(.), stringify!($suffix))*
482        )))
483    },
484    (@push $builder:expr, [$($other:tt)+]) => {
485        compile_error!(concat!(
486            "Forms should be a list of identifiers separated by '.'s, received \"",
487             stringify!($($other)+),
488             "\" instead"
489        ))
490    },
491
492    // Plain text
493    (@push $builder:expr, $part:expr) => {
494        $builder.push($part)
495    },
496
497    (@parse $builder:expr, $part:tt $($parts:tt)*) => {{
498        hint!(@push $builder, $part);
499        hint!(@parse $builder, $($parts)*);
500    }},
501    (@parse $builder:expr,) => {},
502
503    ($builder:expr, $($parts:tt)+) => {{
504        let builder: &mut Builder = &mut $builder;
505        hint!(@parse builder, $($parts)+);
506    }},
507    ($($parts:tt)+) => {{
508        let mut builder = Builder::new();
509        hint!(builder, [DefaultHint] $($parts)+);
510        builder.finish()
511    }},
512}