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