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, 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, FormId)>,
61    last_align: Option<(usize, Alignment)>,
62    buffer: String,
63    last_was_empty: bool,
64}
65
66impl Builder {
67    /// Returns a new instance of [`Builder`]
68    ///
69    /// Use [`Text::builder`] if you don't want to bring [`Builder`]
70    /// into scope.
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// Finish construction and returns the [`Text`]
76    ///
77    /// Will also finish the last [`Form`] and alignments pushed to
78    /// the [builder], as well as pushing a `'\n'` at the end, much
79    /// like with regular [`Text`] construction.
80    ///
81    /// [`Form`]: crate::form::Form
82    /// [builder]: Builder
83    /// [`Builder::into::<Text>`]: Into::into
84    /// [`Widget`]: crate::ui::Widget
85    /// [`StatusLine`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.StatusLine.html
86    pub fn build(mut self) -> Text {
87        self.push_str("\n");
88        self.build_no_nl()
89    }
90
91    /// Builds the [`Text`] without adding a `'\n'` at the end
92    ///
93    /// This should only be used when adding [`Text`] to another
94    /// [`Builder`], as a `'\n'` should only be added at the end of
95    /// [`Text`]s, or when creating a [ghosts], which don't end in a
96    /// `'\n'`, since they are placed in the middle of another
97    /// [`Text`], much like [`Text`]s added to a [`Builder`]
98    ///
99    /// [ghosts]: super::Ghost
100    fn build_no_nl(mut self) -> Text {
101        if let Some((b, id)) = self.last_form
102            && b < self.text.len().byte()
103        {
104            self.text.insert_tag(Tagger::basic(), b.., id.to_tag(0));
105        }
106        if let Some((b, align)) = self.last_align
107            && b < self.text.len().byte()
108        {
109            match align {
110                Alignment::Center => self.text.insert_tag(Tagger::basic(), b.., AlignCenter),
111                Alignment::Right => self.text.insert_tag(Tagger::basic(), b.., AlignRight),
112                _ => {}
113            }
114        }
115
116        self.text
117    }
118
119    /// Pushes a part of the text
120    ///
121    /// This can be an [`impl Display`] type, an [`impl Tag`], a
122    /// [`FormId`], a [`PathBuf`], or even [`std::process::Output`].
123    ///
124    /// [`impl Display`]: std::fmt::Display
125    /// [`impl Tag`]: super::Tag
126    pub fn push<D: Display, _T>(&mut self, part: impl Into<BuilderPart<D, _T>>) {
127        fn push_basic(builder: &mut Builder, part: BuilderPart) {
128            use Alignment::*;
129            use BuilderPart as BP;
130
131            let end = builder.text.len().byte();
132            match part {
133                BP::Text(text) => builder.push_text(text),
134                BP::Form(id) => {
135                    let last_form = if id == crate::form::DEFAULT_ID {
136                        builder.last_form.take()
137                    } else {
138                        builder.last_form.replace((end, id))
139                    };
140
141                    if let Some((b, id)) = last_form
142                        && b < end
143                    {
144                        builder.text.insert_tag(Tagger::basic(), b.., id.to_tag(0));
145                    }
146                }
147                BP::AlignLeft => match builder.last_align.take() {
148                    Some((b, Center)) if b < end => {
149                        builder.text.insert_tag(Tagger::basic(), b.., AlignCenter);
150                    }
151                    Some((b, Right)) if b < end => {
152                        builder.text.insert_tag(Tagger::basic(), b.., 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::basic(), b.., 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::basic(), b.., AlignCenter);
169                        builder.last_align = Some((end, Right));
170                    }
171                    None => builder.last_align = Some((end, Right)),
172                    Some(_) => {}
173                },
174                BP::Spacer(_) => builder.text.insert_tag(Tagger::basic(), end, Spacer),
175                BP::Ghost(text) => builder.text.insert_tag(Tagger::basic(), end, Ghost(text)),
176                BP::ToString(_) => unsafe { std::hint::unreachable_unchecked() },
177            }
178        }
179
180        match Into::<BuilderPart<D, _T>>::into(part).try_to_basic() {
181            Ok(basic_part) => push_basic(self, basic_part),
182            Err(BuilderPart::ToString(display)) => self.push_str(display),
183            Err(_) => unsafe { std::hint::unreachable_unchecked() },
184        }
185    }
186
187    /// Whether or not the last added piece was empty
188    ///
189    /// This happens when an empty [`String`] or an empty [`Text`] is
190    /// pushed.
191    pub fn last_was_empty(&self) -> bool {
192        self.last_was_empty
193    }
194
195    /// Pushes an [`impl Display`] to the [`Text`]
196    ///
197    /// [`impl Display`]: std::fmt::Display
198    pub fn push_str<D: Display>(&mut self, d: D) {
199        self.buffer.clear();
200        write!(self.buffer, "{d}").unwrap();
201        if self.buffer.is_empty() {
202            self.last_was_empty = true;
203        } else {
204            self.last_was_empty = false;
205            let end = self.text.len();
206            self.text
207                .apply_change_inner(0, Change::str_insert(&self.buffer, end));
208        }
209    }
210
211    /// Pushes [`Text`] directly
212    fn push_text(&mut self, text: Text) {
213        self.last_was_empty = text.is_empty();
214
215        if let Some((b, id)) = self.last_form.take() {
216            self.text.insert_tag(Tagger::basic(), b.., id.to_tag(0));
217        }
218
219        self.text.0.bytes.extend(text.0.bytes);
220        self.text.0.tags.extend(text.0.tags);
221    }
222}
223
224impl Default for Builder {
225    fn default() -> Self {
226        Builder {
227            text: Text::empty(),
228            last_form: None,
229            last_align: None,
230            buffer: String::with_capacity(50),
231            last_was_empty: false,
232        }
233    }
234}
235
236impl std::fmt::Debug for Builder {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        f.debug_struct("Builder")
239            .field("text", &self.text)
240            .finish_non_exhaustive()
241    }
242}
243
244impl From<Builder> for Text {
245    fn from(value: Builder) -> Self {
246        value.build()
247    }
248}
249
250/// A part to be pushed to a [`Builder`] by a macro
251#[derive(Clone)]
252pub enum BuilderPart<D: Display = String, _T = ()> {
253    /// Text to be pushed
254    Text(Text),
255    /// An [`impl Display`](std::fmt::Display) type
256    ToString(D),
257    /// A [`FormId`]
258    Form(FormId),
259    /// Sets the alignment to the left, i.e. resets it
260    AlignLeft,
261    /// Sets the alignment to the center
262    AlignCenter,
263    /// Sets the alignment to the right
264    AlignRight,
265    /// A spacer for more advanced alignment
266    Spacer(PhantomData<_T>),
267    /// Ghost [`Text`] that is separate from the real thing
268    Ghost(Text),
269}
270
271impl<D: Display, _T> BuilderPart<D, _T> {
272    fn try_to_basic(self) -> Result<BuilderPart, Self> {
273        match self {
274            BuilderPart::Text(text) => Ok(BuilderPart::Text(text)),
275            BuilderPart::ToString(d) => Err(BuilderPart::ToString(d)),
276            BuilderPart::Form(form_id) => Ok(BuilderPart::Form(form_id)),
277            BuilderPart::AlignLeft => Ok(BuilderPart::AlignLeft),
278            BuilderPart::AlignCenter => Ok(BuilderPart::AlignCenter),
279            BuilderPart::AlignRight => Ok(BuilderPart::AlignRight),
280            BuilderPart::Spacer(_) => Ok(BuilderPart::Spacer(PhantomData)),
281            BuilderPart::Ghost(text) => Ok(BuilderPart::Ghost(text)),
282        }
283    }
284}
285
286impl From<Builder> for BuilderPart {
287    fn from(value: Builder) -> Self {
288        Self::Text(value.build_no_nl())
289    }
290}
291
292impl From<FormId> for BuilderPart {
293    fn from(value: FormId) -> Self {
294        Self::Form(value)
295    }
296}
297
298impl From<AlignLeft> for BuilderPart {
299    fn from(_: AlignLeft) -> Self {
300        BuilderPart::AlignLeft
301    }
302}
303
304impl From<AlignCenter> for BuilderPart {
305    fn from(_: AlignCenter) -> Self {
306        BuilderPart::AlignCenter
307    }
308}
309
310impl From<AlignRight> for BuilderPart {
311    fn from(_: AlignRight) -> Self {
312        BuilderPart::AlignRight
313    }
314}
315
316impl From<Spacer> for BuilderPart {
317    fn from(_: Spacer) -> Self {
318        BuilderPart::Spacer(PhantomData)
319    }
320}
321
322impl<T: Into<Text>> From<Ghost<T>> for BuilderPart {
323    fn from(value: Ghost<T>) -> Self {
324        BuilderPart::Ghost(value.0.into())
325    }
326}
327
328impl From<Text> for BuilderPart {
329    fn from(value: Text) -> Self {
330        BuilderPart::Text(value.without_last_nl())
331    }
332}
333
334impl<D: Display> From<D> for BuilderPart<D, D> {
335    fn from(value: D) -> Self {
336        BuilderPart::ToString(value)
337    }
338}
339
340impl From<PathBuf> for BuilderPart {
341    fn from(value: PathBuf) -> Self {
342        BuilderPart::ToString(value.to_string_lossy().to_string())
343    }
344}
345
346impl From<&PathBuf> for BuilderPart {
347    fn from(value: &PathBuf) -> Self {
348        BuilderPart::ToString(value.to_string_lossy().to_string())
349    }
350}
351
352impl From<std::process::Output> for BuilderPart {
353    fn from(value: std::process::Output) -> Self {
354        BuilderPart::ToString(String::from_utf8_lossy(&value.stdout).into_owned())
355    }
356}
357
358/// The standard [`Text`] construction macro
359///
360/// TODO: Docs
361///
362/// [`Text`]: super::Text
363pub macro txt($($parts:tt)+) {{
364    #[allow(unused_imports)]
365    use $crate::private_exports::{format_like, parse_arg, parse_form, parse_str};
366
367    let mut builder = $crate::text::Builder::new();
368
369    format_like!(
370        parse_str,
371        [('{', parse_arg, false), ('[', parse_form, true)],
372        &mut builder,
373        $($parts)*
374    );
375
376    builder
377}}