duat_core/text/
builder.rs

1//! [`Text`] building macros
2//!
3//! This module defines the [`txt!`] macro, alongside its [`Builder`]
4//! companion struct. This macro is very convenient for the creation
5//! of [`Text`]s, since its syntax is just a superset of the natural
6//! syntax of [`format_args!`], also allowing for the addition of
7//! [`Form`]s through specialized arguments.
8//!
9//! [`Form`]: crate::form::Form
10use std::{
11    fmt::{Alignment, Display, Write},
12    marker::PhantomData,
13    path::Path,
14};
15
16use super::{Change, Tagger, Text};
17use crate::{
18    buffer::PathKind,
19    form::FormId,
20    text::{AlignCenter, AlignLeft, AlignRight, FormTag, Ghost, Spacer},
21};
22
23/// Builds and modifies a [`Text`], based on replacements applied
24/// to it.
25///
26/// This struct is meant to be used alongside the [`txt!`] macro, as
27/// you can just push more [`Text`] to the [`Builder`] py pushing
28/// another [`Builder`], which can be returned by the [`txt!`] macro:
29///
30/// ```rust
31/// # use duat_core::text::{Text, txt};
32/// fn is_more_than_two(num: usize) -> Text {
33///     let mut builder = Text::builder();
34///     builder.push(txt!("The number [a]{num}[] is"));
35///     if num > 2 {
36///         builder.push(txt!("[a]more[] than 2."));
37///     } else {
38///         builder.push(txt!("[a]not more[] than 2."));
39///     }
40///     builder.build()
41/// }
42/// ```
43///
44/// In the above call, you can see that `num` was interpolated, just
45/// like with [`println!`], there are also [`Form`]s being applied to
46/// the [`Text`]. Each `[]` pair denotes a [`Form`]. These pairs
47/// follow the following rule:
48///
49/// - `[]`: Will push the `"default"` [`Form`], which is actually just
50///   removing prior [`Form`]s.
51/// - `[a]`: Will push the `"accent"` [`Form`].
52/// - `[{form}]`: Will push the `"{form}"`, where `{form}` can be any
53///   sequence of valid identifiers, separated by `"."`s.
54///
55/// [`impl Display`]: std::fmt::Display
56/// [tag]: AlignCenter
57/// [`Form`]: crate::form::Form
58#[derive(Clone)]
59pub struct Builder {
60    text: Text,
61    last_form: Option<(usize, FormTag)>,
62    last_align: Option<(usize, Alignment)>,
63    buffer: String,
64    last_was_empty: bool,
65    /// Wether to collapse `" "`s after an empty element is pushed
66    pub no_space_after_empty: bool,
67}
68
69impl Builder {
70    /// Returns a new instance of [`Builder`]
71    ///
72    /// Use [`Text::builder`] if you don't want to bring [`Builder`]
73    /// into scope.
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Finish construction and returns the [`Text`]
79    ///
80    /// Will also finish the last [`Form`] and alignments pushed to
81    /// the [builder], as well as pushing a `'\n'` at the end, much
82    /// like with regular [`Text`] construction.
83    ///
84    /// [`Form`]: crate::form::Form
85    /// [builder]: Builder
86    /// [`Builder::into::<Text>`]: Into::into
87    /// [`Widget`]: crate::ui::Widget
88    /// [`StatusLine`]: https://docs.rs/duat/latest/duat/widgets/struct.StatusLine.html
89    pub fn build(mut self) -> Text {
90        if let Some((b, id)) = self.last_form
91            && b < self.text.last_point().byte()
92        {
93            self.text.insert_tag(Tagger::basic(), b.., id);
94        }
95        if let Some((b, align)) = self.last_align
96            && b < self.text.last_point().byte()
97        {
98            match align {
99                Alignment::Center => {
100                    self.text.insert_tag(Tagger::basic(), b.., AlignCenter);
101                }
102                Alignment::Right => {
103                    self.text.insert_tag(Tagger::basic(), b.., AlignRight);
104                }
105                _ => {}
106            }
107        }
108
109        self.text
110    }
111
112    /// Builds the [`Text`], removing any double `\n`s at the end
113    ///
114    /// When building multi-line `Text`, it is common to just insert
115    /// every piece of `Text` with a `\n` at the end. This ends up
116    /// resulting in a `Text` that has two `\n`s at the end, since
117    /// there is always at least one final `\n`.
118    ///
119    /// This function lets you construct the [`Text`] taking that
120    /// effect into account.
121    pub fn build_no_double_nl(self) -> Text {
122        let mut text = self.build();
123        if let Some(last_last_byte) = text.len().byte().checked_sub(2)
124            && let Some(strs) = text.strs(last_last_byte..)
125            && strs == "\n\n"
126        {
127            text.replace_range(last_last_byte..last_last_byte + 1, "");
128        }
129
130        text
131    }
132
133    /// Pushes a part of the text
134    ///
135    /// This can be an [`impl Display`] type, an [`impl Tag`], a
136    /// [`FormId`], a [`PathBuf`], or even [`std::process::Output`].
137    ///
138    /// [`impl Display`]: std::fmt::Display
139    /// [`impl Tag`]: super::Tag
140    pub fn push<D: Display, _T>(&mut self, part: impl AsBuilderPart<D, _T>) {
141        self.push_builder_part(part.as_builder_part());
142    }
143
144    #[doc(hidden)]
145    pub fn push_builder_part<_T>(&mut self, part: BuilderPart<impl Display, _T>) {
146        fn push_simple(builder: &mut Builder, part: BuilderPart) {
147            use Alignment::*;
148            use BuilderPart as BP;
149
150            let end = builder.text.last_point().byte();
151            let tagger = Tagger::basic();
152
153            match part {
154                BP::Text(text) => builder.push_text(text),
155                BP::Builder(other) => builder.push_builder(other),
156                BP::Path(path) => builder.push_str(path.to_string_lossy()),
157                BP::PathKind(text) => builder.push_text(&text),
158                BP::Form(tag) => {
159                    let last_form = if tag == crate::form::DEFAULT_ID.to_tag(0) {
160                        builder.last_form.take()
161                    } else {
162                        builder.last_form.replace((end, tag))
163                    };
164
165                    if let Some((b, tag)) = last_form
166                        && b < end
167                    {
168                        builder.text.insert_tag(tagger, b..end, tag);
169                    }
170                }
171                BP::AlignLeft => match builder.last_align.take() {
172                    Some((b, Center)) if b < end => {
173                        builder.text.insert_tag(tagger, b..end, AlignCenter);
174                    }
175                    Some((b, Right)) if b < end => {
176                        builder.text.insert_tag(tagger, b..end, AlignRight);
177                    }
178                    _ => {}
179                },
180                BP::AlignCenter => match builder.last_align.take() {
181                    Some((b, Center)) => builder.last_align = Some((b, Center)),
182                    Some((b, Right)) if b < end => {
183                        builder.text.insert_tag(tagger, b..end, AlignRight);
184                        builder.last_align = Some((end, Center));
185                    }
186                    None => builder.last_align = Some((end, Center)),
187                    Some(_) => {}
188                },
189                BP::AlignRight => match builder.last_align.take() {
190                    Some((b, Right)) => builder.last_align = Some((b, Right)),
191                    Some((b, Center)) if b < end => {
192                        builder.text.insert_tag(tagger, b..end, AlignCenter);
193                        builder.last_align = Some((end, Right));
194                    }
195                    None => builder.last_align = Some((end, Right)),
196                    Some(_) => {}
197                },
198                BP::Spacer(_) => _ = builder.text.insert_tag(tagger, end, Spacer),
199                BP::Ghost(ghost) => _ = builder.text.insert_tag(tagger, end, ghost.clone()),
200                BP::ToString(_) => unsafe { std::hint::unreachable_unchecked() },
201            }
202        }
203
204        match part.try_to_basic() {
205            Ok(part_ref) => push_simple(self, part_ref),
206            Err(BuilderPart::ToString(display)) => self.push_str(display),
207            Err(_) => unsafe { std::hint::unreachable_unchecked() },
208        }
209    }
210
211    /// Whether or not the last added piece was empty
212    ///
213    /// This happens when an empty [`String`] or an empty [`Text`] is
214    /// pushed.
215    pub fn last_was_empty(&self) -> bool {
216        self.last_was_empty
217    }
218
219    /// Pushes an [`impl Display`] to the [`Text`]
220    ///
221    /// If `builder.no_space_after_empty` is set to `true` and
222    /// the argument is equal to `" "`, then it won't be added if
223    /// the previous argument was empty. This is useful especially
224    /// in situations where you expect to be constructing a `Text`
225    /// with spaces in between the elements (like in a status line),
226    /// but you don't want an empty element to just leave to space
227    /// wide gap in between two non empty elements.
228    ///
229    /// [`impl Display`]: std::fmt::Display
230    pub fn push_str<D: Display>(&mut self, d: D) {
231        self.buffer.clear();
232        write!(self.buffer, "{d}").unwrap();
233        if self.buffer.is_empty()
234            || (self.no_space_after_empty && self.buffer == " " && self.last_was_empty)
235        {
236            self.last_was_empty = true;
237        } else {
238            self.last_was_empty = false;
239            let end = self.text.last_point();
240            self.text
241                .apply_change_inner(0, Change::str_insert(&self.buffer, end));
242        }
243    }
244
245    /// Resets the [`Form`]
246    ///
247    /// This is equivalent to pushing the `default` `Form`.
248    ///
249    /// [`Form`]: crate::form::Form
250    pub fn reset_form(&mut self) {
251        let end = self.text.last_point().byte();
252        if let Some((b, last_form)) = self.last_form.take() {
253            self.text.insert_tag(Tagger::basic(), b..end, last_form);
254        }
255    }
256
257    /// Resets the [`Form`]
258    ///
259    /// This is equivalent to pushing the `default` `Form`.
260    ///
261    /// [`Form`]: crate::form::Form
262    pub fn reset_alignment(&mut self) {
263        let end = self.text.last_point().byte();
264        match self.last_align.take() {
265            Some((b, Alignment::Center)) if b < end => {
266                self.text.insert_tag(Tagger::basic(), b..end, AlignCenter);
267            }
268            Some((b, Alignment::Right)) if b < end => {
269                self.text.insert_tag(Tagger::basic(), b..end, AlignRight);
270            }
271            _ => {}
272        }
273    }
274
275    /// Pushes [`Text`] directly
276    fn push_text(&mut self, text: &Text) {
277        self.last_was_empty = text.is_empty();
278        self.text.insert_text(self.text.last_point(), text);
279    }
280
281    /// Pushes [`Text`] directly
282    fn push_builder(&mut self, other: &Builder) {
283        self.last_was_empty = other.text.is_empty();
284
285        let offset = self.text.last_point().byte();
286        self.text.insert_text(offset, &other.text);
287        let end = self.text.last_point().byte();
288
289        if let Some((b, id)) = other.last_form
290            && b < other.text.last_point().byte()
291        {
292            self.text.insert_tag(Tagger::basic(), offset + b..end, id);
293        }
294        if let Some((b, align)) = other.last_align
295            && b < other.text.last_point().byte()
296        {
297            let tagger = Tagger::basic();
298            if let Alignment::Center = align {
299                self.text.insert_tag(tagger, offset + b..end, AlignCenter);
300            } else {
301                self.text.insert_tag(tagger, offset + b..end, AlignRight);
302            }
303        }
304    }
305}
306
307impl Default for Builder {
308    fn default() -> Self {
309        Builder {
310            text: Text::new(),
311            last_form: None,
312            last_align: None,
313            buffer: String::with_capacity(50),
314            last_was_empty: false,
315            no_space_after_empty: false,
316        }
317    }
318}
319
320impl std::fmt::Debug for Builder {
321    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322        f.debug_struct("Builder")
323            .field("text", &self.text)
324            .finish_non_exhaustive()
325    }
326}
327
328impl From<Builder> for Text {
329    fn from(value: Builder) -> Self {
330        value.build()
331    }
332}
333
334/// A part to be pushed to a [`Builder`] by a macro
335#[derive(Clone)]
336pub enum BuilderPart<'a, D: Display = String, _T = ()> {
337    /// Text to be pushed
338    ///
339    /// # Note
340    ///
341    /// Every [`Text`] struct has a `\n` attached at the end,
342    /// but when pushing it to a [`Builder`], said `\n` is
343    /// automatically removed. If you want to keep a `\n` at the
344    /// end, push an additional one.
345    Text(&'a Text),
346    /// A Text Builder
347    ///
348    /// Much like the [`Text`], normally, the [`Builder`] finishes
349    /// with a `\n`, but when pushed to another [`Builder`], that `\n`
350    /// is removed as well.
351    Builder(&'a Builder),
352    /// An [`impl Display`](std::fmt::Display) type
353    ToString(&'a D),
354    /// An [`Path`] reference
355    Path(&'a std::path::Path),
356    /// A [`PathKind`]
357    PathKind(Text),
358    /// A [`FormId`]
359    Form(FormTag),
360    /// Sets the alignment to the left, i.e. resets it
361    AlignLeft,
362    /// Sets the alignment to the center
363    AlignCenter,
364    /// Sets the alignment to the right
365    AlignRight,
366    /// A spacer for more advanced alignment
367    Spacer(PhantomData<_T>),
368    /// Ghost [`Text`] that is separate from the real thing
369    Ghost(&'a Ghost),
370}
371
372impl<'a, D: Display, _T> BuilderPart<'a, D, _T> {
373    fn try_to_basic(self) -> Result<BuilderPart<'a>, Self> {
374        match self {
375            BuilderPart::Text(text) => Ok(BuilderPart::Text(text)),
376            BuilderPart::Builder(builder) => Ok(BuilderPart::Builder(builder)),
377            BuilderPart::ToString(_) => Err(self),
378            BuilderPart::Path(path) => Ok(BuilderPart::Path(path)),
379            BuilderPart::PathKind(text) => Ok(BuilderPart::PathKind(text)),
380            BuilderPart::Form(form_id) => Ok(BuilderPart::Form(form_id)),
381            BuilderPart::AlignLeft => Ok(BuilderPart::AlignLeft),
382            BuilderPart::AlignCenter => Ok(BuilderPart::AlignCenter),
383            BuilderPart::AlignRight => Ok(BuilderPart::AlignRight),
384            BuilderPart::Spacer(_) => Ok(BuilderPart::Spacer(PhantomData)),
385            BuilderPart::Ghost(ghost) => Ok(BuilderPart::Ghost(ghost)),
386        }
387    }
388}
389
390/// Defines which types can be pushed onto a [`Builder`]
391///
392/// Every [`Tag`], as well as any [`Display`] and [`AsRef<Path>`]
393/// types can be pushed to a `Builder`.
394///
395/// [`Tag`]: super::Tag
396pub trait AsBuilderPart<D: Display = String, _T = ()> {
397    /// Gets a [`BuilderPart`] fro this value
398    fn as_builder_part(&self) -> BuilderPart<'_, D, _T>;
399}
400
401macro_rules! implAsBuilderPart {
402    ($type:ident, $value:ident, $result:expr) => {
403        impl AsBuilderPart for $type {
404            fn as_builder_part(&self) -> BuilderPart<'_> {
405                let $value = self;
406                $result
407            }
408        }
409    };
410}
411
412implAsBuilderPart!(Builder, builder, BuilderPart::Builder(builder));
413implAsBuilderPart!(FormId, form_id, BuilderPart::Form(form_id.to_tag(0)));
414implAsBuilderPart!(FormTag, form_tag, BuilderPart::Form(*form_tag));
415implAsBuilderPart!(AlignLeft, _align, BuilderPart::AlignLeft);
416implAsBuilderPart!(AlignCenter, _align, BuilderPart::AlignCenter);
417implAsBuilderPart!(AlignRight, _align, BuilderPart::AlignRight);
418implAsBuilderPart!(Spacer, _spacer, BuilderPart::Spacer(PhantomData));
419implAsBuilderPart!(Ghost, ghost, BuilderPart::Ghost(ghost));
420implAsBuilderPart!(Text, text, BuilderPart::Text(text));
421implAsBuilderPart!(Path, path, BuilderPart::Path(path));
422implAsBuilderPart!(PathKind, path, BuilderPart::PathKind(path.name_txt()));
423
424impl<D: Display> AsBuilderPart<D, D> for D {
425    fn as_builder_part(&self) -> BuilderPart<'_, D, D> {
426        BuilderPart::ToString(self)
427    }
428}
429
430/// The standard [`Text`] construction macro
431///
432/// TODO: Docs
433///
434/// [`Text`]: super::Text
435#[macro_export]
436#[doc(hidden)]
437macro_rules! __txt__ {
438    ($($parts:tt)+) => {{
439        #[allow(unused_imports)]
440        use $crate::{
441            __parse_arg__, __parse_form__, __parse_str__, private_exports::format_like
442        };
443
444        let mut builder = $crate::text::Builder::new();
445        let _ = format_like!(
446            __parse_str__,
447            [('{', __parse_arg__, false), ('[', __parse_form__, true)],
448            &mut builder,
449            $($parts)*
450        );
451
452        builder.build()
453    }};
454}
455
456#[macro_export]
457#[doc(hidden)]
458macro_rules! __log__ {
459    ($lvl:expr, $($arg:tt)*) => {{
460        #[allow(unused_must_use)]
461        let text = $crate::text::txt!($($arg)*);
462
463        $crate::context::logs().push_record($crate::context::Record::new(
464            text,
465            $lvl,
466        ));
467    }}
468}
469
470#[macro_export]
471#[doc(hidden)]
472macro_rules! __parse_str__ {
473    ($builder:expr, $str:literal) => {{
474        let builder = $builder;
475        builder.push_str($str);
476        builder
477    }};
478}
479
480#[macro_export]
481#[doc(hidden)]
482macro_rules! __parse_arg__ {
483    ($builder:expr,"", $arg:expr) => {{
484        use $crate::text::AsBuilderPart;
485        let builder = $builder;
486        builder.push_builder_part($arg.as_builder_part());
487        builder
488    }};
489    ($builder:expr, $modif:literal, $arg:expr) => {{
490        let builder = $builder;
491        builder.push_str(format!(concat!("{:", $modif, "}"), &$arg));
492        builder
493    }};
494}
495
496#[macro_export]
497#[doc(hidden)]
498macro_rules! __parse_form__ {
499    ($builder:expr, $priority:literal,) => {{
500        use $crate::text::AsBuilderPart;
501        const PRIORITY: u8 = $crate::priority($priority);
502        let builder = $builder;
503        let id = $crate::form::DEFAULT_ID;
504        builder.push_builder_part(id.to_tag(PRIORITY).as_builder_part());
505        builder
506    }};
507    ($builder:expr, $priority:literal, a) => {{
508        use $crate::text::AsBuilderPart;
509        const PRIORITY: u8 = $crate::priority($priority);
510        let builder = $builder;
511        let id = $crate::form::ACCENT_ID;
512        builder.push_builder_part(id.to_tag(PRIORITY).as_builder_part());
513        builder
514    }};
515    ($builder:expr, $priority:literal, $($form:tt)*) => {{
516        use $crate::text::AsBuilderPart;
517        const PRIORITY: u8 = $crate::priority($priority);
518        let builder = $builder;
519        let id = $crate::form::id_of!(concat!($(stringify!($form)),*));
520        builder.push_builder_part(id.to_tag(PRIORITY).as_builder_part());
521        builder
522    }};
523}