Skip to main content

duat_core/text/
iter.rs

1//! The functions for iteration of [`Text`]s.
2//!
3//! These functions will iterate over the text, reverse or forwards,
4//! keeping track of characters and [`Tag`]s, sending them in order,
5//! while also hiding the existance of certain "meta" tags, namely the
6//! [ghost] and [concealment] tags. This allows for a seemless
7//! iteration which is especially useful for printing, as the printer
8//! only needs to care about [`char`]s and `Tag`s, most of which are
9//! just [`Form`] changing `Tag`s
10//!
11//! [`Tag`]: super::Tag
12//! [ghost]: super::Inlay
13//! [concealment]: super::Conceal
14//! [`Form`]: crate::form::Form
15use std::{
16    iter::{Chain, Rev},
17    str::Chars,
18};
19
20use super::{
21    Point, SpawnId, Text,
22    tags::{self, RawTag},
23};
24use crate::{
25    form::MaskId,
26    text::{TwoPoints, tags::InnerTags},
27};
28
29/// An [`Iterator`] over the [`TextPart`]s of the [`Text`].
30///
31/// This is useful for both printing and measurement of `Text`, and
32/// can incorporate string replacements as part of its design.
33#[derive(Clone)]
34pub struct FwdIter<'t> {
35    text: &'t Text,
36    point: Point,
37    init_point: Point,
38    chars: FwdChars<'t>,
39    tags: tags::FwdTags<'t>,
40    conceals: u32,
41
42    // Things to deal with ghost text.
43    main_iter: Option<MainIter<FwdChars<'t>, tags::FwdTags<'t>>>,
44    ghost: Option<(Point, usize)>,
45}
46
47impl<'t> FwdIter<'t> {
48    /// Returns a new forward [`Iterator`] over the [`TextPlace`]s in
49    /// the [`Text`].
50    #[track_caller]
51    pub(super) fn new_at(text: &'t Text, points: TwoPoints, is_ghost: bool) -> Self {
52        let TwoPoints { real, ghost } = points;
53        let point = real.min(if is_ghost {
54            text.last_point()
55        } else {
56            text.end_point()
57        });
58
59        // The second usize argument of ghost is the "distance traversed".
60        // When iterating over this starting point, the Tags iterator will
61        // still iterate over prior Inlays in the same byte, even if they were
62        // supposed to be skipped.
63        // The "distance traversed" serves the purpose of skipping those
64        // ghosts until the correct one is reached, hence why it starts at 0.
65        let ghost = if let Some(offset) = ghost {
66            let points = text.ghost_max_points_at(real.byte());
67            points.ghost.map(|max| (max.min(offset), 0))
68        } else {
69            let points = text.ghost_max_points_at(real.byte());
70            points.ghost.zip(Some(0))
71        };
72
73        Self {
74            text,
75            point,
76            init_point: point,
77            chars: buf_chars_fwd(text, point.byte(), is_ghost),
78            tags: text.tags_fwd(point.byte(), None),
79            conceals: 0,
80
81            main_iter: None,
82            ghost,
83        }
84    }
85
86    ////////// Querying functions
87
88    /// Wether the [`Iterator`] is on a [`Inlay`].
89    ///
90    /// [`Inlay`]: super::Inlay
91    #[inline(always)]
92    pub fn is_on_ghost(&self) -> bool {
93        self.main_iter.is_some()
94    }
95
96    /// Returns the current real and ghost [`Point`]s of the
97    /// [`Iterator`].
98    #[inline(always)]
99    pub fn points(&self) -> TwoPoints {
100        if let Some(MainIter { point, .. }) = self.main_iter.as_ref() {
101            TwoPoints::new(*point, self.ghost.map(|(tg, _)| tg).unwrap())
102        } else {
103            TwoPoints::new_after_ghost(self.point)
104        }
105    }
106
107    /// The [`Text`] that's being iterated over.
108    pub fn text(&self) -> &Text {
109        self.text
110    }
111
112    /// Handles meta [`Tag`]s.
113    ///
114    /// [`Tag`]: super::Tag
115    #[inline(always)]
116    fn handle_meta_tag(&mut self, tag: &RawTag, b: usize) -> bool {
117        match tag {
118            RawTag::Inlay(_, id) => {
119                if b < self.point.byte() || self.conceals > 0 {
120                    return true;
121                }
122                let text = self.text.get_ghost(*id).unwrap();
123
124                let (this_ghost, total_ghost) = if let Some((ghost, dist)) = &mut self.ghost {
125                    if ghost.byte() >= *dist + text.last_point().byte() {
126                        *dist += text.last_point().byte();
127                        return true;
128                    }
129                    (text.point_at_byte(ghost.byte() - *dist), *ghost)
130                } else {
131                    (Point::default(), Point::default())
132                };
133
134                let iter = FwdIter::new_at(text, this_ghost.to_two_points_before(), true);
135                let point = std::mem::replace(&mut self.point, this_ghost);
136                let init_point = std::mem::replace(&mut self.init_point, this_ghost);
137                let chars = std::mem::replace(&mut self.chars, iter.chars);
138                let tags = std::mem::replace(&mut self.tags, iter.tags);
139
140                self.ghost = Some((total_ghost, total_ghost.byte()));
141                self.main_iter = Some(MainIter { point, init_point, chars, tags });
142            }
143            RawTag::StartConceal(_) => self.conceals += 1,
144            RawTag::EndConceal(_) => {
145                self.conceals = self.conceals.saturating_sub(1);
146                if self.conceals == 0 {
147                    // If we have moved forward and were in a ghost, that ghost is no
148                    // longer valid.
149                    self.ghost.take_if(|_| self.point.byte() < b);
150                    self.point = self.point.max(self.text.point_at_byte(b));
151                    self.chars = buf_chars_fwd(self.text, self.point.byte(), false);
152                }
153            }
154            RawTag::ConcealUntil(b) => {
155                let point = self.text.point_at_byte(*b as usize);
156                *self = FwdIter::new_at(self.text, point.to_two_points_before(), false);
157                return false;
158            }
159            RawTag::Spacer(_) | RawTag::SpawnedWidget(..) | RawTag::Overlay(..)
160                if b < self.init_point.byte() => {}
161            RawTag::StartToggle(..) | RawTag::EndToggle(..) => {}
162            _ => return false,
163        }
164
165        true
166    }
167}
168
169impl<'t> Iterator for FwdIter<'t> {
170    type Item = TextPlace<'t>;
171
172    #[inline]
173    fn next(&mut self) -> Option<Self::Item> {
174        if let Some((b, tag)) = self
175            .tags
176            .next_if(|(b, _)| *b <= self.point.byte() || self.conceals > 0)
177        {
178            if self.handle_meta_tag(&tag, b) {
179                self.next()
180            } else {
181                let tags = &self.text.0.tags;
182                Some(TextPlace::new(self.points(), TextPart::from_raw(tags, tag)))
183            }
184        } else if let Some(char) = self.chars.next() {
185            let points = self.points();
186            self.point = self.point.fwd(char);
187
188            self.ghost = match self.main_iter {
189                Some(..) => self.ghost.map(|(g, d)| (g.fwd(char), d + 1)),
190                None => None,
191            };
192
193            Some(TextPlace::new(points, TextPart::Char(char)))
194        } else if let Some(main_iter) = self.main_iter.take() {
195            self.point = main_iter.point;
196            self.init_point = main_iter.init_point;
197            self.chars = main_iter.chars;
198            self.tags = main_iter.tags;
199
200            self.next()
201        } else {
202            None
203        }
204    }
205}
206
207/// An [`Iterator`] over the [`TextPart`]s of the [`Text`].
208///
209/// This is useful for both printing and measurement of [`Text`], and
210/// can incorporate string replacements as part of its design.
211#[derive(Clone)]
212pub struct RevIter<'t> {
213    text: &'t Text,
214    point: Point,
215    init_point: Point,
216    chars: RevChars<'t>,
217    tags: tags::RevTags<'t>,
218    conceals: usize,
219
220    main_iter: Option<MainIter<RevChars<'t>, tags::RevTags<'t>>>,
221    ghost: Option<(Point, usize)>,
222}
223
224impl<'t> RevIter<'t> {
225    /// Returns a new reverse [`Iterator`] over the [`TextPlace`]s in
226    /// the [`Text`].
227    #[track_caller]
228    pub(super) fn new_at(text: &'t Text, points: TwoPoints) -> Self {
229        let TwoPoints { real, ghost } = points;
230        let point = real.min(text.end_point());
231
232        let ghost = ghost.and_then(|offset| {
233            let points = text.ghost_max_points_at(real.byte());
234            points.ghost.map(|max| (max.min(offset), max.byte()))
235        });
236
237        Self {
238            text,
239            point,
240            init_point: point,
241            chars: buf_chars_rev(text, point.byte()),
242            tags: text.tags_rev(point.byte(), None),
243            conceals: 0,
244
245            main_iter: None,
246            ghost,
247        }
248    }
249
250    ////////// Querying functions
251
252    /// Returns the current real and ghost [`Point`]s.
253    pub fn points(&self) -> TwoPoints {
254        if let Some(MainIter { point, .. }) = self.main_iter.as_ref() {
255            TwoPoints::new(*point, self.point)
256        } else if let Some((ghost, _)) = self.ghost {
257            TwoPoints::new(self.point, ghost)
258        } else {
259            TwoPoints::new_after_ghost(self.point)
260        }
261    }
262
263    /// The [`Text`] that's being iterated over.
264    pub fn text(&self) -> &'t Text {
265        self.text
266    }
267
268    /// Wether the [`Iterator`] is on a [`Inlay`].
269    ///
270    /// [`Inlay`]: super::Inlay
271    pub fn is_on_ghost(&self) -> bool {
272        self.main_iter.is_some()
273    }
274
275    /// Handles meta [`Tag`]s.
276    ///
277    /// [`Tag`]: super::Tag
278    #[inline]
279    fn handled_meta_tag(&mut self, tag: &RawTag, b: usize) -> bool {
280        match tag {
281            RawTag::Inlay(_, id) => {
282                if b > self.point.byte() || self.conceals > 0 {
283                    return true;
284                }
285                let text = self.text.get_ghost(*id).unwrap();
286
287                let (ghost_b, this_ghost) = if let Some((offset, dist)) = &mut self.ghost {
288                    if *dist - text.last_point().byte() >= offset.byte() {
289                        *dist -= text.last_point().byte();
290                        return true;
291                    }
292                    (
293                        text.point_at_byte(offset.byte() + text.last_point().byte() - *dist),
294                        *offset,
295                    )
296                } else {
297                    let this = text.last_point();
298                    let points = self.text.ghost_max_points_at(b);
299                    (this, points.ghost.unwrap())
300                };
301
302                let iter = text.iter_rev(ghost_b.to_two_points_before());
303                let point = std::mem::replace(&mut self.point, this_ghost);
304                let init_point = std::mem::replace(&mut self.init_point, this_ghost);
305                let chars = std::mem::replace(&mut self.chars, iter.chars);
306                let tags = std::mem::replace(&mut self.tags, iter.tags);
307
308                self.ghost = Some((this_ghost, this_ghost.byte()));
309                self.main_iter = Some(MainIter { point, init_point, chars, tags });
310            }
311
312            RawTag::StartConceal(_) => {
313                self.conceals = self.conceals.saturating_sub(1);
314                if self.conceals == 0 {
315                    self.ghost.take_if(|_| b < self.point.byte());
316                    self.point = self.point.min(self.text.point_at_byte(b));
317                    self.chars = buf_chars_rev(self.text, self.point.byte());
318                }
319            }
320            RawTag::EndConceal(_) => self.conceals += 1,
321            RawTag::ConcealUntil(b) => {
322                let point = self.text.point_at_byte(*b as usize);
323                *self = RevIter::new_at(self.text, point.to_two_points_before());
324                return false;
325            }
326            RawTag::Spacer(_) | RawTag::SpawnedWidget(..) | RawTag::Overlay(..)
327                if b > self.init_point.byte() => {}
328            RawTag::StartToggle(..) | RawTag::EndToggle(..) => {}
329            _ => return false,
330        }
331
332        true
333    }
334}
335
336impl<'t> Iterator for RevIter<'t> {
337    type Item = TextPlace<'t>;
338
339    #[inline]
340    fn next(&mut self) -> Option<Self::Item> {
341        if let Some((b, tag)) = self
342            .tags
343            .next_if(|(b, _)| *b >= self.point.byte() || self.conceals > 0)
344        {
345            if self.handled_meta_tag(&tag, b) {
346                self.next()
347            } else {
348                let tags = &self.text.0.tags;
349                Some(TextPlace::new(self.points(), TextPart::from_raw(tags, tag)))
350            }
351        } else if let Some(char) = self.chars.next() {
352            self.point = self.point.rev(char);
353
354            self.ghost = match self.main_iter {
355                Some(..) => self.ghost.map(|(g, d)| (g.rev(char), d - 1)),
356                None => None,
357            };
358
359            Some(TextPlace::new(self.points(), TextPart::Char(char)))
360        } else if let Some(main_iter) = self.main_iter.take() {
361            self.point = main_iter.point;
362            self.init_point = main_iter.init_point;
363            self.chars = main_iter.chars;
364            self.tags = main_iter.tags;
365
366            self.next()
367        } else {
368            None
369        }
370    }
371}
372
373fn buf_chars_fwd(text: &Text, b: usize, minus_last_nl: bool) -> FwdChars<'_> {
374    let [s0, s1] = text
375        .slices(b..text.len() - minus_last_nl as usize)
376        .map(|s| unsafe { std::str::from_utf8_unchecked(s) });
377    s0.chars().chain(s1.chars())
378}
379
380fn buf_chars_rev(text: &Text, b: usize) -> RevChars<'_> {
381    let [s0, s1] = text
382        .slices(..b)
383        .map(|s| unsafe { std::str::from_utf8_unchecked(s) });
384    s1.chars().rev().chain(s0.chars().rev())
385}
386
387/// An element of a [`Text`].
388///
389/// This struct is comprised of three parts:
390///
391/// - A real [`Point`], representing a position on the real `Text`;
392/// - A ghost `Point`, which is a position in a [`Inlay`], [`None`] if
393///   not in a `Inlay`;
394/// - A [`TextPart`], which will either be a `char` or a [`Tag`];
395///
396/// [`Inlay`]: super::Inlay
397/// [`Tag`]: super::Tag
398#[derive(Debug, Clone, Copy)]
399pub struct TextPlace<'t> {
400    /// The real [`Point`].
401    pub real: Point,
402    /// The [`Point`] in a [`Inlay`].
403    ///
404    /// If there are multiple `Inlay`s in the same character, this
405    /// `Point` will point to a sum of the previous [`Text`]'s
406    /// [lengths] plus the position on this specific `Inlay`, so
407    /// every `Point` should point to a specific position in a char.
408    ///
409    /// [`Inlay`]: super::Inlay
410    /// [lengths]: super::Strs::len
411    pub ghost: Option<Point>,
412    /// A [`TextPart`], which will either be a `char` or a [`Tag`];
413    ///
414    /// [`Tag`]: super::Tag
415    pub part: TextPart<'t>,
416}
417
418impl<'t> TextPlace<'t> {
419    /// Returns a new [`TextPlace`].
420    #[inline]
421    const fn new(points: TwoPoints, part: TextPart<'t>) -> Self {
422        let TwoPoints { real, ghost } = points;
423        Self { real, ghost, part }
424    }
425
426    /// Whether this [`TextPlace`] is in a [`Inlay`].
427    ///
428    /// [`Inlay`]: super::Inlay
429    pub const fn is_real(&self) -> bool {
430        self.ghost.is_none()
431    }
432
433    /// Returns the real position, if not on a [`Inlay`].
434    ///
435    /// [`Inlay`]: super::Inlay
436    pub const fn as_real_char(self) -> Option<(Point, char)> {
437        let Some(char) = self.part.as_char() else {
438            return None;
439        };
440        if self.ghost.is_none() {
441            Some((self.real, char))
442        } else {
443            None
444        }
445    }
446
447    /// The real [byte](Point::byte).
448    pub const fn byte(&self) -> usize {
449        self.real.byte()
450    }
451
452    /// The real [char](Point::char).
453    pub const fn char(&self) -> usize {
454        self.real.char()
455    }
456
457    /// The real [line](Point::line).
458    pub const fn line(&self) -> usize {
459        self.real.line()
460    }
461
462    /// The real and ghost [`Point`]s, can be used as [`TwoPoints`].
463    pub const fn points(&self) -> TwoPoints {
464        if let Some(ghost) = self.ghost {
465            TwoPoints::new(self.real, ghost)
466        } else {
467            TwoPoints::new_after_ghost(self.real)
468        }
469    }
470}
471
472type FwdChars<'t> = Chain<Chars<'t>, Chars<'t>>;
473type RevChars<'t> = Chain<Rev<Chars<'t>>, Rev<Chars<'t>>>;
474
475use crate::form::FormId;
476
477/// A part of the [`Text`], can be a [`char`] or a [`Tag`].
478///
479/// This type is used in iteration by [Ui]s in order to
480/// correctly print Duat's content. Additionally, you may be
481/// able to tell that there is no ghost text or concealment
482/// tags, and there is a [`ResetState`].
483///
484/// That is because the [`Text`]'s iteration process automatically
485/// gets rid of these tags, since, from the point of view of the
486/// ui, ghost text is just regular text, while conceals are
487/// simply the lack of text. And if the ui can handle printing
488/// regular text, printing ghost text should be a breeze.
489///
490/// [`Text`]: super::Text
491/// [`Tag`]: super::Tag
492/// [Ui]: crate::ui::traits::RawUi
493/// [`ResetState`]: Part::ResetState
494#[derive(Debug, Clone, Copy, PartialEq, Eq)]
495#[doc(hidden)]
496pub enum TextPart<'t> {
497    /// A printed `char`, can be real or a [`Inlay`].
498    ///
499    /// [`Inlay`]: super::Inlay
500    Char(char),
501    /// Push a [`Form`] to the [`Painter`].
502    ///
503    /// [`Form`]: crate::form::Form
504    /// [`Painter`]: crate::form::Painter
505    PushForm(FormId, u8),
506    /// Pop a [`Form`] from the [`Painter`].
507    ///
508    /// [`Form`]: crate::form::Form
509    /// [`Painter`]: crate::form::Painter
510    PopForm(FormId),
511    /// Add a [`Spacer`].
512    ///
513    /// [`Spacer`]: super::Spacer
514    Spacer,
515    /// An inlay [`Text`].
516    Overlay(&'t Text),
517    /// A spawned [`Widget`].
518    ///
519    /// [`Widget`]: crate::ui::Widget
520    SpawnedWidget(SpawnId),
521    /// Push a mask to the [`Painter`], which can map [`Form`]s given
522    /// a suffix.
523    ///
524    /// [`Form`]: crate::form::Form
525    /// [`Painter`]: crate::form::Painter
526    PushMask(MaskId),
527    /// Pop a mask from the [`Painter`].
528    PopMask(MaskId),
529    /// Resets all [`FormId`]s, [`ToggleId`]s and alignments.
530    ///
531    /// Used when a [`Conceal`] covers a large region, which Duat
532    /// optimizes by just not iterating over the [`TextPart`]s within.
533    /// This could skip some [`Tag`]s, so this variant serves the
534    /// purpose of terminating or initiating in place of skipped
535    /// [`Tag`]s.
536    ///
537    /// This variant does not actually represent any [`Tag`].
538    ///
539    /// [`Conceal`]: super::Conceal
540    /// [`Tag`]: super::Tag
541    ResetState,
542}
543
544impl<'t> TextPart<'t> {
545    /// Returns a new [`TextPart`] from a [`RawTag`].
546    #[inline]
547    pub(super) fn from_raw(tags: &'t InnerTags, value: RawTag) -> Self {
548        match value {
549            RawTag::PushForm(_, id, prio) => Self::PushForm(id, prio),
550            RawTag::PopForm(_, id) => Self::PopForm(id),
551            RawTag::Spacer(_) => Self::Spacer,
552            RawTag::ConcealUntil(_) => Self::ResetState,
553            RawTag::SpawnedWidget(_, id) => Self::SpawnedWidget(id),
554            RawTag::Overlay(_, id) => Self::Overlay(tags.get_ghost(id).unwrap()),
555            RawTag::PushMask(_, id) => Self::PushMask(id),
556            RawTag::PopMask(_, id) => Self::PopMask(id),
557            RawTag::StartConceal(_)
558            | RawTag::EndConceal(_)
559            | RawTag::Inlay(..)
560            | RawTag::StartToggle(..)
561            | RawTag::EndToggle(..) => {
562                unreachable!("These tags are automatically processed elsewhere.")
563            }
564        }
565    }
566
567    /// Returns `true` if the part is [`Char`].
568    ///
569    /// [`Char`]: TextPart::Char
570    #[must_use]
571    #[inline]
572    pub const fn is_char(&self) -> bool {
573        matches!(self, TextPart::Char(_))
574    }
575
576    /// Returns `true` if the part is not [`Char`].
577    ///
578    /// [`Char`]: TextPart::Char
579    #[inline]
580    pub const fn is_tag(&self) -> bool {
581        !self.is_char()
582    }
583
584    /// Returns [`Some`] if the part is [`Char`].
585    ///
586    /// [`Char`]: TextPart::Char
587    #[inline]
588    pub const fn as_char(&self) -> Option<char> {
589        if let Self::Char(v) = self {
590            Some(*v)
591        } else {
592            None
593        }
594    }
595}
596
597#[derive(Debug, Clone)]
598struct MainIter<Chars, Tags> {
599    point: Point,
600    init_point: Point,
601    chars: Chars,
602    tags: Tags,
603}