Skip to main content

epaint/text/
text_layout_types.rs

1use core::{ops::Range, str::FromStr as _};
2use std::sync::Arc;
3
4use super::{
5    cursor::{CCursor, LayoutCursor},
6    font::UvRect,
7    index::{ByteIndex, ByteRange, ByteRangeExt as _, CharIndex},
8};
9use crate::{Color32, FontId, Mesh, Stroke, text::FontsView};
10use emath::{Align, GuiRounding as _, NumExt as _, OrderedFloat, Pos2, Rect, Vec2, pos2, vec2};
11pub use font_types::Tag;
12use smallvec::SmallVec;
13
14/// Describes the task of laying out text.
15///
16/// This supports mixing different fonts, color and formats (underline etc).
17///
18/// Pass this to [`crate::FontsView::layout_job`] or [`crate::text::layout`].
19///
20/// ## Example:
21/// ```
22/// use epaint::{Color32, text::{LayoutJob, TextFormat}, FontFamily, FontId};
23///
24/// let mut job = LayoutJob::default();
25/// job.append(
26///     "Hello ",
27///     0.0,
28///     TextFormat {
29///         font_id: FontId::new(14.0, FontFamily::Proportional),
30///         color: Color32::WHITE,
31///         ..Default::default()
32///     },
33/// );
34/// job.append(
35///     "World!",
36///     0.0,
37///     TextFormat {
38///         font_id: FontId::new(14.0, FontFamily::Monospace),
39///         color: Color32::BLACK,
40///         ..Default::default()
41///     },
42/// );
43/// ```
44///
45/// As you can see, constructing a [`LayoutJob`] is currently a lot of work.
46/// It would be nice to have a helper macro for it!
47#[derive(Clone, Debug, PartialEq)]
48#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
49pub struct LayoutJob {
50    /// The complete text of this job, referenced by [`LayoutSection`].
51    pub text: String,
52
53    /// The different section, which can have different fonts, colors, etc.
54    ///
55    /// Invariant: the sections are ordered by their `byte_range`,
56    /// and together cover the whole of [`Self::text`] with no gaps and no overlaps.
57    /// That is: the first section starts at byte 0, the last section ends at `text.len()`,
58    /// and each section starts exactly where the previous one ended.
59    /// This is checked by [`Self::debug_sanity_check`].
60    pub sections: Vec<LayoutSection>,
61
62    /// Controls the text wrapping and elision.
63    pub wrap: TextWrapping,
64
65    /// The first row must be at least this high.
66    /// This is in case we lay out text that is the continuation
67    /// of some earlier text (sharing the same row),
68    /// in which case this will be the height of the earlier text.
69    /// In other cases, set this to `0.0`.
70    pub first_row_min_height: f32,
71
72    /// If `true`, all `\n` characters will result in a new _paragraph_,
73    /// starting on a new row.
74    ///
75    /// If `false`, all `\n` characters will be ignored
76    /// and show up as the replacement character.
77    ///
78    /// Default: `true`.
79    pub break_on_newline: bool,
80
81    /// How to horizontally align the text (`Align::LEFT`, `Align::Center`, `Align::RIGHT`).
82    pub halign: Align,
83
84    /// Justify text so that word-wrapped rows fill the whole [`TextWrapping::max_width`].
85    pub justify: bool,
86
87    /// Round output sizes using [`emath::GuiRounding`], to avoid rounding errors in layout code.
88    pub round_output_to_gui: bool,
89
90    /// If `false` (default), trailing whitespace is ignored when computing
91    /// horizontal alignment ([`Self::halign`]).
92    /// This is desirable for labels so that e.g. "Hello " centers the same as "Hello".
93    ///
94    /// If `true`, trailing whitespace is included in the row width used for alignment.
95    /// This is desirable for text editors where the user expects to see their spaces.
96    pub keep_trailing_whitespace: bool,
97}
98
99impl Default for LayoutJob {
100    #[inline]
101    fn default() -> Self {
102        Self {
103            text: Default::default(),
104            sections: Default::default(),
105            wrap: Default::default(),
106            first_row_min_height: 0.0,
107            break_on_newline: true,
108            halign: Align::LEFT,
109            justify: false,
110            round_output_to_gui: true,
111            keep_trailing_whitespace: false,
112        }
113    }
114}
115
116impl LayoutJob {
117    /// Clear the text and sections while preserving the layout settings.
118    #[inline]
119    pub fn clear(&mut self) {
120        self.text.clear();
121        self.sections.clear();
122    }
123
124    /// Break on `\n` and at the given wrap width.
125    #[inline]
126    pub fn simple(text: String, font_id: FontId, color: Color32, wrap_width: f32) -> Self {
127        Self {
128            sections: vec![LayoutSection {
129                leading_space: 0.0,
130                byte_range: ByteRange::full(&text),
131                format: TextFormat::simple(font_id, color),
132            }],
133            text,
134            wrap: TextWrapping {
135                max_width: wrap_width,
136                ..Default::default()
137            },
138            break_on_newline: true,
139            ..Default::default()
140        }
141    }
142
143    /// Break on `\n`
144    #[inline]
145    pub fn simple_format(text: String, format: TextFormat) -> Self {
146        Self {
147            sections: vec![LayoutSection {
148                leading_space: 0.0,
149                byte_range: ByteRange::full(&text),
150                format,
151            }],
152            text,
153            break_on_newline: true,
154            ..Default::default()
155        }
156    }
157
158    /// Does not break on `\n`, but shows the replacement character instead.
159    #[inline]
160    pub fn simple_singleline(text: String, font_id: FontId, color: Color32) -> Self {
161        Self {
162            sections: vec![LayoutSection {
163                leading_space: 0.0,
164                byte_range: ByteRange::full(&text),
165                format: TextFormat::simple(font_id, color),
166            }],
167            text,
168            wrap: Default::default(),
169            break_on_newline: false,
170            ..Default::default()
171        }
172    }
173
174    #[inline]
175    pub fn single_section(text: String, format: TextFormat) -> Self {
176        Self {
177            sections: vec![LayoutSection {
178                leading_space: 0.0,
179                byte_range: ByteRange::full(&text),
180                format,
181            }],
182            text,
183            wrap: Default::default(),
184            break_on_newline: true,
185            ..Default::default()
186        }
187    }
188
189    #[inline]
190    pub fn is_empty(&self) -> bool {
191        self.sections.is_empty()
192    }
193
194    /// Helper for adding a new section when building a [`LayoutJob`].
195    ///
196    /// If the appended text has the same [`TextFormat`] as the last section and no
197    /// `leading_space`, it is merged into that section instead of adding a new one.
198    /// This keeps the section count down and lets text shaping (e.g. kerning) work
199    /// across the appended text, since shaping is done per [`LayoutSection`].
200    pub fn append(&mut self, text: &str, leading_space: f32, format: TextFormat) {
201        let start = self.text.len();
202        self.text += text;
203        let byte_range = ByteIndex(start)..ByteIndex(self.text.len());
204
205        // Optimization: merge into the previous section if it has the same format
206        // and this one adds no leading space.
207        if leading_space == 0.0
208            && let Some(last) = self.sections.last_mut()
209            && last.format == format
210        {
211            last.byte_range.end = byte_range.end;
212            return;
213        }
214
215        self.sections.push(LayoutSection {
216            leading_space,
217            byte_range,
218            format,
219        });
220    }
221
222    /// The [`TextFormat`] of the section containing the character starting at the given byte index.
223    ///
224    /// If the index is past the end, the format of the last section is returned.
225    ///
226    /// Panics if the job has no sections.
227    /// Assumes [`LayoutJob::sections`] are ordered by increasing `byte_range` (as produced by [`Self::append`]).
228    pub fn format_at_byte(&self, byte_idx: ByteIndex) -> &TextFormat {
229        self.debug_sanity_check();
230        let last = self.sections.last().expect("LayoutJob has no sections");
231        let idx = self
232            .sections
233            .partition_point(|section| section.byte_range.end <= byte_idx);
234        let section = self.sections.get(idx).unwrap_or(last);
235        &section.format
236    }
237
238    /// Check the [`Self::sections`] invariant: the sections are ordered and together
239    /// cover the whole of [`Self::text`] with no gaps and no overlaps.
240    ///
241    /// Only does anything in debug builds.
242    #[cfg_attr(not(debug_assertions), expect(clippy::unused_self))]
243    pub fn debug_sanity_check(&self) {
244        #[cfg(debug_assertions)]
245        {
246            if self.sections.is_empty() {
247                assert!(
248                    self.text.is_empty(),
249                    "LayoutJob has text but no sections: {:?}",
250                    self.text
251                );
252                return;
253            }
254
255            assert_eq!(
256                self.sections
257                    .first()
258                    .expect("checked above")
259                    .byte_range
260                    .start,
261                ByteIndex::ZERO,
262                "First LayoutSection must start at byte 0"
263            );
264            assert_eq!(
265                self.sections.last().expect("checked above").byte_range.end,
266                ByteIndex(self.text.len()),
267                "Last LayoutSection must end at the end of the text"
268            );
269
270            for section in &self.sections {
271                let Range { start, end } = section.byte_range;
272                assert!(start <= end, "LayoutSection has a reversed byte_range");
273            }
274            for (prev, next) in core::iter::zip(&self.sections, self.sections.iter().skip(1)) {
275                assert_eq!(
276                    prev.byte_range.end, next.byte_range.start,
277                    "LayoutSections must be ordered with no gaps and no overlaps"
278                );
279            }
280        }
281    }
282
283    /// The height of the tallest font used in the job.
284    ///
285    /// Returns a value rounded to [`emath::GUI_ROUNDING`].
286    pub fn font_height(&self, fonts: &mut FontsView<'_>) -> f32 {
287        let mut max_height = 0.0_f32;
288        for section in &self.sections {
289            max_height = max_height.max(fonts.row_height(&section.format.font_id));
290        }
291        max_height
292    }
293
294    /// The wrap with, with a small margin in some cases.
295    pub fn effective_wrap_width(&self) -> f32 {
296        if self.round_output_to_gui {
297            // On a previous pass we may have rounded down by at most 0.5 and reported that as a width.
298            // egui may then set that width as the max width for subsequent frames, and it is important
299            // that we then don't wrap earlier.
300            self.wrap.max_width + 0.5
301        } else {
302            self.wrap.max_width
303        }
304    }
305}
306
307impl core::hash::Hash for LayoutJob {
308    #[inline]
309    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
310        let Self {
311            text,
312            sections,
313            wrap,
314            first_row_min_height,
315            break_on_newline,
316            halign,
317            justify,
318            round_output_to_gui,
319            keep_trailing_whitespace,
320        } = self;
321
322        text.hash(state);
323        sections.hash(state);
324        wrap.hash(state);
325        emath::OrderedFloat(*first_row_min_height).hash(state);
326        break_on_newline.hash(state);
327        halign.hash(state);
328        justify.hash(state);
329        round_output_to_gui.hash(state);
330        keep_trailing_whitespace.hash(state);
331    }
332}
333
334// ----------------------------------------------------------------------------
335
336/// A contiguous range of [`LayoutJob::text`] that shares the same [`TextFormat`].
337///
338/// The sections of a [`LayoutJob`] are ordered and together cover the whole text
339/// with no gaps and no overlaps. See [`LayoutJob::sections`] for the full invariant.
340///
341/// Text is shaped on a per-section basis: each section is an independent shaping run.
342/// This means kerning (and ligatures) are only correct _within_ a single section,
343/// and not across the boundary between two adjacent sections.
344/// For this reason [`LayoutJob::append`] merges consecutive sections when possible.
345#[derive(Clone, Debug, PartialEq)]
346#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
347pub struct LayoutSection {
348    /// Can be used for first row indentation.
349    pub leading_space: f32,
350
351    /// Range into [`LayoutJob::text`].
352    pub byte_range: ByteRange,
353
354    /// How to format the text in this section (font, color, etc).
355    pub format: TextFormat,
356}
357
358impl core::hash::Hash for LayoutSection {
359    #[inline]
360    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
361        let Self {
362            leading_space,
363            byte_range,
364            format,
365        } = self;
366        OrderedFloat(*leading_space).hash(state);
367        byte_range.hash(state);
368        format.hash(state);
369    }
370}
371
372// ----------------------------------------------------------------------------
373
374/// Helper trait for all types that can be parsed as a [`font_types::Tag`].
375pub trait IntoTag {
376    fn into_tag(self) -> font_types::Tag;
377}
378
379impl IntoTag for font_types::Tag {
380    #[inline(always)]
381    fn into_tag(self) -> font_types::Tag {
382        self
383    }
384}
385
386impl IntoTag for u32 {
387    #[inline(always)]
388    fn into_tag(self) -> font_types::Tag {
389        font_types::Tag::from_u32(self)
390    }
391}
392
393impl IntoTag for [u8; 4] {
394    #[inline(always)]
395    fn into_tag(self) -> font_types::Tag {
396        font_types::Tag::new_checked(&self).expect("Invalid variation axis tag")
397    }
398}
399
400impl IntoTag for &[u8; 4] {
401    #[inline(always)]
402    fn into_tag(self) -> font_types::Tag {
403        font_types::Tag::new_checked(self).expect("Invalid variation axis tag")
404    }
405}
406
407impl IntoTag for &str {
408    #[inline(always)]
409    fn into_tag(self) -> font_types::Tag {
410        font_types::Tag::from_str(self).expect("Invalid variation axis tag")
411    }
412}
413
414/// List of font variation coordinates by axis tag. If more than one coordinate for a given axis is provided, the last
415/// one added is used.
416#[derive(Clone, Debug, PartialEq, Default)]
417#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
418pub struct VariationCoords(SmallVec<[(font_types::Tag, f32); 2]>);
419
420impl VariationCoords {
421    /// Create a list of variation coordinates from a sequence of (tag, value) pairs.
422    ///
423    /// ## Example:
424    /// ```
425    /// use epaint::text::VariationCoords;
426    ///
427    /// let coords = VariationCoords::new([
428    ///     (b"wght", 500.0),
429    ///     (b"wdth", 75.0),
430    /// ]);
431    /// ```
432    pub fn new<T: IntoTag>(values: impl IntoIterator<Item = (T, f32)>) -> Self {
433        Self(values.into_iter().map(|(t, c)| (t.into_tag(), c)).collect())
434    }
435
436    /// Add a variation coordinate to the list.
437    #[inline(always)]
438    pub fn push(&mut self, tag: impl IntoTag, coord: f32) {
439        self.0.push((tag.into_tag(), coord));
440    }
441
442    /// Remove the coordinate at the given index.
443    pub fn remove(&mut self, index: usize) {
444        self.0.remove(index);
445    }
446
447    pub fn clear(&mut self) {
448        self.0.clear();
449    }
450}
451
452impl AsRef<[(font_types::Tag, f32)]> for VariationCoords {
453    #[inline(always)]
454    fn as_ref(&self) -> &[(font_types::Tag, f32)] {
455        &self.0
456    }
457}
458
459impl AsMut<[(font_types::Tag, f32)]> for VariationCoords {
460    fn as_mut(&mut self) -> &mut [(font_types::Tag, f32)] {
461        &mut self.0
462    }
463}
464
465impl core::hash::Hash for VariationCoords {
466    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
467        self.0.len().hash(state);
468        for (tag, coord) in &self.0 {
469            tag.hash(state);
470            OrderedFloat(*coord).hash(state);
471        }
472    }
473}
474
475/// Formatting option for a section of text.
476#[derive(Clone, Debug, PartialEq)]
477#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
478pub struct TextFormat {
479    pub font_id: FontId,
480
481    /// Extra spacing between letters, in points.
482    ///
483    /// Default: 0.0.
484    pub extra_letter_spacing: f32,
485
486    /// Explicit line height of the text in points.
487    ///
488    /// This is the distance between the bottom row of two subsequent lines of text.
489    ///
490    /// If `None` (the default), the line height is determined by the font.
491    ///
492    /// For even text it is recommended you round this to an even number of _pixels_.
493    pub line_height: Option<f32>,
494
495    /// Text color
496    pub color: Color32,
497
498    pub background: Color32,
499
500    /// Amount to expand background fill by.
501    ///
502    /// Default: 1.0
503    pub expand_bg: f32,
504
505    pub coords: VariationCoords,
506
507    pub italics: bool,
508
509    pub underline: Stroke,
510
511    pub strikethrough: Stroke,
512
513    /// If you use a small font and [`Align::TOP`] you
514    /// can get the effect of raised text.
515    ///
516    /// If you use a small font and [`Align::BOTTOM`]
517    /// you get the effect of a subscript.
518    ///
519    /// If you use [`Align::Center`], you get text that is centered
520    /// around a common center-line, which is nice when mixining emojis
521    /// and normal text in e.g. a button.
522    pub valign: Align,
523}
524
525impl Default for TextFormat {
526    #[inline]
527    fn default() -> Self {
528        Self {
529            font_id: FontId::default(),
530            extra_letter_spacing: 0.0,
531            line_height: None,
532            color: Color32::GRAY,
533            background: Color32::TRANSPARENT,
534            expand_bg: 1.0,
535            coords: VariationCoords::default(),
536            italics: false,
537            underline: Stroke::NONE,
538            strikethrough: Stroke::NONE,
539            valign: Align::BOTTOM,
540        }
541    }
542}
543
544impl core::hash::Hash for TextFormat {
545    #[inline]
546    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
547        let Self {
548            font_id,
549            extra_letter_spacing,
550            line_height,
551            color,
552            background,
553            expand_bg,
554            coords,
555            italics,
556            underline,
557            strikethrough,
558            valign,
559        } = self;
560        font_id.hash(state);
561        emath::OrderedFloat(*extra_letter_spacing).hash(state);
562        if let Some(line_height) = *line_height {
563            emath::OrderedFloat(line_height).hash(state);
564        }
565        color.hash(state);
566        background.hash(state);
567        emath::OrderedFloat(*expand_bg).hash(state);
568        coords.hash(state);
569        italics.hash(state);
570        underline.hash(state);
571        strikethrough.hash(state);
572        valign.hash(state);
573    }
574}
575
576impl TextFormat {
577    #[inline]
578    pub fn simple(font_id: FontId, color: Color32) -> Self {
579        Self {
580            font_id,
581            color,
582            ..Default::default()
583        }
584    }
585}
586
587// ----------------------------------------------------------------------------
588
589/// How to wrap and elide text.
590///
591/// This enum is used in high-level APIs where providing a [`TextWrapping`] is too verbose.
592#[derive(Clone, Copy, Debug, PartialEq, Eq)]
593#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
594pub enum TextWrapMode {
595    /// The text should expand the `Ui` size when reaching its boundary.
596    Extend,
597
598    /// The text should wrap to the next line when reaching the `Ui` boundary.
599    Wrap,
600
601    /// The text should be elided using "…" when reaching the `Ui` boundary.
602    ///
603    /// Note that using [`TextWrapping`] and [`LayoutJob`] offers more control over the elision.
604    Truncate,
605}
606
607/// Controls the text wrapping and elision of a [`LayoutJob`].
608#[derive(Clone, Debug, PartialEq)]
609#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
610pub struct TextWrapping {
611    /// Wrap text so that no row is wider than this.
612    ///
613    /// If you would rather truncate text that doesn't fit, set [`Self::max_rows`] to `1`.
614    ///
615    /// Set `max_width` to [`f32::INFINITY`] to turn off wrapping and elision.
616    ///
617    /// Note that `\n` always produces a new row
618    /// if [`LayoutJob::break_on_newline`] is `true`.
619    pub max_width: f32,
620
621    /// Maximum amount of rows the text galley should have.
622    ///
623    /// If this limit is reached, text will be truncated
624    /// and [`Self::overflow_character`] appended to the final row.
625    /// You can detect this by checking [`Galley::elided`].
626    ///
627    /// If set to `0`, no text will be outputted.
628    ///
629    /// If set to `1`, a single row will be outputted,
630    /// eliding the text after [`Self::max_width`] is reached.
631    /// When you set `max_rows = 1`, it is recommended you also set [`Self::break_anywhere`] to `true`.
632    ///
633    /// Default value: `usize::MAX`.
634    pub max_rows: usize,
635
636    /// If `true`: Allow breaking between any characters.
637    /// If `false` (default): prefer breaking between words, etc.
638    ///
639    /// NOTE: Due to limitations in the current implementation,
640    /// when truncating text using [`Self::max_rows`] the text may be truncated
641    /// in the middle of a word even if [`Self::break_anywhere`] is `false`.
642    /// Therefore it is recommended to set [`Self::break_anywhere`] to `true`
643    /// whenever [`Self::max_rows`] is set to `1`.
644    pub break_anywhere: bool,
645
646    /// Character to use to represent elided text.
647    ///
648    /// The default is `…`.
649    ///
650    /// If not set, no character will be used (but the text will still be elided).
651    pub overflow_character: Option<char>,
652}
653
654impl core::hash::Hash for TextWrapping {
655    #[inline]
656    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
657        let Self {
658            max_width,
659            max_rows,
660            break_anywhere,
661            overflow_character,
662        } = self;
663        emath::OrderedFloat(*max_width).hash(state);
664        max_rows.hash(state);
665        break_anywhere.hash(state);
666        overflow_character.hash(state);
667    }
668}
669
670impl Default for TextWrapping {
671    fn default() -> Self {
672        Self {
673            max_width: f32::INFINITY,
674            max_rows: usize::MAX,
675            break_anywhere: false,
676            overflow_character: Some('…'),
677        }
678    }
679}
680
681impl TextWrapping {
682    /// Create a [`TextWrapping`] from a [`TextWrapMode`] and an available width.
683    pub fn from_wrap_mode_and_width(mode: TextWrapMode, max_width: f32) -> Self {
684        match mode {
685            TextWrapMode::Extend => Self::no_max_width(),
686            TextWrapMode::Wrap => Self::wrap_at_width(max_width),
687            TextWrapMode::Truncate => Self::truncate_at_width(max_width),
688        }
689    }
690
691    /// A row can be as long as it need to be.
692    pub fn no_max_width() -> Self {
693        Self {
694            max_width: f32::INFINITY,
695            ..Default::default()
696        }
697    }
698
699    /// A row can be at most `max_width` wide but can wrap in any number of lines.
700    pub fn wrap_at_width(max_width: f32) -> Self {
701        Self {
702            max_width,
703            ..Default::default()
704        }
705    }
706
707    /// Elide text that doesn't fit within the given width, replaced with `…`.
708    pub fn truncate_at_width(max_width: f32) -> Self {
709        Self {
710            max_width,
711            max_rows: 1,
712            break_anywhere: true,
713            ..Default::default()
714        }
715    }
716}
717
718// ----------------------------------------------------------------------------
719
720/// Text that has been laid out, ready for painting.
721///
722/// You can create a [`Galley`] using [`crate::FontsView::layout_job`];
723///
724/// Needs to be recreated if the underlying font atlas texture changes, which
725/// happens under the following conditions:
726/// - `pixels_per_point` or `max_texture_size` change. These parameters are set
727///   in [`crate::text::Fonts::begin_pass`]. When using `egui` they are set
728///   from `egui::InputState` and can change at any time.
729/// - The atlas has become full. This can happen any time a new glyph is added
730///   to the atlas, which in turn can happen any time new text is laid out.
731///
732/// The name comes from typography, where a "galley" is a metal tray
733/// containing a column of set type, usually the size of a page of text.
734#[derive(Clone, Debug, PartialEq)]
735#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
736pub struct Galley {
737    /// The job that this galley is the result of.
738    /// Contains the original string and style sections.
739    pub job: Arc<LayoutJob>,
740
741    /// Rows of text, from top to bottom, and their offsets.
742    ///
743    /// The number of characters in all rows sum up to `job.text.chars().count()`
744    /// unless [`Self::elided`] is `true`.
745    ///
746    /// Note that a paragraph (a piece of text separated with `\n`)
747    /// can be split up into multiple rows.
748    pub rows: Vec<PlacedRow>,
749
750    /// Set to true the text was truncated due to [`TextWrapping::max_rows`].
751    pub elided: bool,
752
753    /// Bounding rect.
754    ///
755    /// `rect.top()` is always 0.0.
756    ///
757    /// With [`LayoutJob::halign`]:
758    /// * [`Align::LEFT`]: `rect.left() == 0.0`
759    /// * [`Align::Center`]: `rect.center() == 0.0`
760    /// * [`Align::RIGHT`]: `rect.right() == 0.0`
761    pub rect: Rect,
762
763    /// Tight bounding box around all the meshes in all the rows.
764    /// Can be used for culling.
765    pub mesh_bounds: Rect,
766
767    /// Total number of vertices in all the row meshes.
768    pub num_vertices: usize,
769
770    /// Total number of indices in all the row meshes.
771    pub num_indices: usize,
772
773    /// The number of physical pixels for each logical point.
774    /// Since this affects the layout, we keep track of it
775    /// so that we can warn if this has changed once we get to
776    /// tessellation.
777    pub pixels_per_point: f32,
778
779    pub(crate) intrinsic_size: Vec2,
780}
781
782#[derive(Clone, Debug, PartialEq)]
783#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
784pub struct PlacedRow {
785    /// The position of this [`Row`] relative to the galley.
786    ///
787    /// This is rounded to the closest _pixel_ in order to produce crisp, pixel-perfect text.
788    pub pos: Pos2,
789
790    /// The underlying unpositioned [`Row`].
791    pub row: Arc<Row>,
792
793    /// If true, this [`PlacedRow`] came from a paragraph ending with a `\n`.
794    /// The `\n` itself is omitted from row's [`Row::glyphs`].
795    /// A `\n` in the input text always creates a new [`PlacedRow`] below it,
796    /// so that text that ends with `\n` has an empty [`PlacedRow`] last.
797    /// This also implies that the last [`PlacedRow`] in a [`Galley`] always has `ends_with_newline == false`.
798    pub ends_with_newline: bool,
799}
800
801impl PlacedRow {
802    /// Logical bounding rectangle on font heights etc.
803    ///
804    /// This ignores / includes the `LayoutSection::leading_space`.
805    pub fn rect(&self) -> Rect {
806        Rect::from_min_size(self.pos, self.row.size)
807    }
808
809    /// Same as [`Self::rect`] but excluding the `LayoutSection::leading_space`.
810    pub fn rect_without_leading_space(&self) -> Rect {
811        let x = self.pos.x + self.glyphs.first().map_or(0.0, |g| g.pos.x);
812        let right = self.pos.x + self.size.x;
813        Rect::from_min_max(
814            Pos2::new(x, self.pos.y),
815            Pos2::new(right, self.pos.y + self.size.y),
816        )
817    }
818}
819
820impl core::ops::Deref for PlacedRow {
821    type Target = Row;
822
823    fn deref(&self) -> &Self::Target {
824        &self.row
825    }
826}
827
828#[derive(Clone, Debug, PartialEq)]
829#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
830pub struct Row {
831    /// This is included in case there are no glyphs.
832    ///
833    /// Only used during layout, then set to an invalid value in order to
834    /// enable the paragraph-concat optimization path without having to
835    /// adjust `section_index` when concatting.
836    pub(crate) section_index_at_start: u32,
837
838    /// One for each `char`.
839    pub glyphs: Vec<Glyph>,
840
841    /// Logical size based on font heights etc.
842    /// Includes leading and trailing whitespace.
843    pub size: Vec2,
844
845    /// The mesh, ready to be rendered.
846    pub visuals: RowVisuals,
847}
848
849/// The tessellated output of a row.
850#[derive(Clone, Debug, PartialEq, Eq)]
851#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
852pub struct RowVisuals {
853    /// The tessellated text, using non-normalized (texel) UV coordinates.
854    /// That is, you need to divide the uv coordinates by the texture size.
855    pub mesh: Mesh,
856
857    /// Bounds of the mesh, and can be used for culling.
858    /// Does NOT include leading or trailing whitespace glyphs!!
859    pub mesh_bounds: Rect,
860
861    /// The number of triangle indices added before the first glyph triangle.
862    ///
863    /// This can be used to insert more triangles after the background but before the glyphs,
864    /// i.e. for text selection visualization.
865    pub glyph_index_start: usize,
866
867    /// The range of vertices in the mesh that contain glyphs (as opposed to background, underlines, strikethorugh, etc).
868    ///
869    /// The glyph vertices comes after backgrounds (if any), but before any underlines and strikethrough.
870    pub glyph_vertex_range: Range<usize>,
871}
872
873impl Default for RowVisuals {
874    fn default() -> Self {
875        Self {
876            mesh: Default::default(),
877            mesh_bounds: Rect::NOTHING,
878            glyph_index_start: 0,
879            glyph_vertex_range: 0..0,
880        }
881    }
882}
883
884#[derive(Copy, Clone, Debug, PartialEq)]
885#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
886pub struct Glyph {
887    /// The character this glyph represents.
888    pub chr: char,
889
890    /// Baseline position, relative to the row.
891    /// Logical position: pos.y is the same for all chars of the same [`TextFormat`].
892    pub pos: Pos2,
893
894    /// Logical width of the glyph.
895    pub advance_width: f32,
896
897    /// Height of this row of text.
898    ///
899    /// Usually same as [`Self::font_height`],
900    /// unless explicitly overridden by [`TextFormat::line_height`].
901    pub line_height: f32,
902
903    /// The ascent of this font.
904    pub font_ascent: f32,
905
906    /// The row/line height of this font.
907    pub font_height: f32,
908
909    /// The ascent of the sub-font within the font (`FontFace`).
910    pub font_face_ascent: f32,
911
912    /// The row/line height of the sub-font within the font (`FontFace`).
913    pub font_face_height: f32,
914
915    /// Position and size of the glyph in the font texture, in texels.
916    pub uv_rect: UvRect,
917
918    /// Index into [`LayoutJob::sections`]. Decides color etc.
919    ///
920    /// Only used during layout, then set to an invalid value in order to
921    /// enable the paragraph-concat optimization path without having to
922    /// adjust `section_index` when concatting.
923    pub(crate) section_index: u32,
924
925    /// Which is our first vertex in [`RowVisuals::mesh`].
926    pub first_vertex: u32,
927}
928
929impl Glyph {
930    #[inline]
931    pub fn size(&self) -> Vec2 {
932        Vec2::new(self.advance_width, self.line_height)
933    }
934
935    #[inline]
936    pub fn max_x(&self) -> f32 {
937        self.pos.x + self.advance_width
938    }
939
940    /// Same y range for all characters with the same [`TextFormat`].
941    #[inline]
942    pub fn logical_rect(&self) -> Rect {
943        Rect::from_min_size(self.pos - vec2(0.0, self.font_ascent), self.size())
944    }
945}
946
947// ----------------------------------------------------------------------------
948
949impl Row {
950    /// The text on this row, excluding the implicit `\n` if any.
951    pub fn text(&self) -> String {
952        self.glyphs.iter().map(|g| g.chr).collect()
953    }
954
955    /// Excludes the implicit `\n` after the [`Row`], if any.
956    #[inline]
957    pub fn char_count_excluding_newline(&self) -> CharIndex {
958        CharIndex(self.glyphs.len())
959    }
960
961    /// Closest char at the desired x coordinate in row-relative coordinates.
962    /// Returns something in the range `[0, char_count_excluding_newline()]`.
963    pub fn char_at(&self, desired_x: f32) -> CharIndex {
964        for (i, glyph) in self.glyphs.iter().enumerate() {
965            if desired_x < glyph.logical_rect().center().x {
966                return CharIndex(i);
967            }
968        }
969        self.char_count_excluding_newline()
970    }
971
972    pub fn x_offset(&self, column: CharIndex) -> f32 {
973        if let Some(glyph) = self.glyphs.get(column.0) {
974            glyph.pos.x
975        } else {
976            self.size.x
977        }
978    }
979
980    #[inline]
981    pub fn height(&self) -> f32 {
982        self.size.y
983    }
984}
985
986impl PlacedRow {
987    #[inline]
988    pub fn min_y(&self) -> f32 {
989        self.rect().top()
990    }
991
992    #[inline]
993    pub fn max_y(&self) -> f32 {
994        self.rect().bottom()
995    }
996
997    /// Includes the implicit `\n` after the [`PlacedRow`], if any.
998    #[inline]
999    pub fn char_count_including_newline(&self) -> CharIndex {
1000        CharIndex(self.row.glyphs.len() + (self.ends_with_newline as usize))
1001    }
1002}
1003
1004impl Galley {
1005    #[inline]
1006    pub fn is_empty(&self) -> bool {
1007        self.job.is_empty()
1008    }
1009
1010    /// The full, non-elided text of the input job.
1011    #[inline]
1012    pub fn text(&self) -> &str {
1013        &self.job.text
1014    }
1015
1016    #[inline]
1017    pub fn size(&self) -> Vec2 {
1018        self.rect.size()
1019    }
1020
1021    /// This is the size that a non-wrapped, non-truncated, non-justified version of the text
1022    /// would have.
1023    ///
1024    /// Useful for advanced layouting.
1025    #[inline]
1026    pub fn intrinsic_size(&self) -> Vec2 {
1027        // We do the rounding here instead of in `round_output_to_gui` so that rounding
1028        // errors don't accumulate when concatenating multiple galleys.
1029        if self.job.round_output_to_gui {
1030            self.intrinsic_size.round_ui()
1031        } else {
1032            self.intrinsic_size
1033        }
1034    }
1035
1036    pub(crate) fn round_output_to_gui(&mut self) {
1037        for placed_row in &mut self.rows {
1038            // Optimization: only call `make_mut` if necessary (can cause a deep clone)
1039            let rounded_size = placed_row.row.size.round_ui();
1040            if placed_row.row.size != rounded_size {
1041                Arc::make_mut(&mut placed_row.row).size = rounded_size;
1042            }
1043        }
1044
1045        let rect = &mut self.rect;
1046
1047        let did_exceed_wrap_width_by_a_lot = rect.width() > self.job.wrap.max_width + 1.0;
1048
1049        *rect = rect.round_ui();
1050
1051        if did_exceed_wrap_width_by_a_lot {
1052            // If the user picked a too aggressive wrap width (e.g. more narrow than any individual glyph),
1053            // we should let the user know by reporting that our width is wider than the wrap width.
1054        } else {
1055            // Make sure we don't report being wider than the wrap width the user picked:
1056            rect.max.x = rect
1057                .max
1058                .x
1059                .at_most(rect.min.x + self.job.wrap.max_width)
1060                .floor_ui();
1061        }
1062    }
1063
1064    /// Append each galley under the previous one.
1065    pub fn concat(job: Arc<LayoutJob>, galleys: &[Arc<Self>], pixels_per_point: f32) -> Self {
1066        profiling::function_scope!();
1067
1068        let mut merged_galley = Self {
1069            job,
1070            rows: Vec::new(),
1071            elided: false,
1072            rect: Rect::ZERO,
1073            mesh_bounds: Rect::NOTHING,
1074            num_vertices: 0,
1075            num_indices: 0,
1076            pixels_per_point,
1077            intrinsic_size: Vec2::ZERO,
1078        };
1079
1080        for (i, galley) in galleys.iter().enumerate() {
1081            let current_y_offset = merged_galley.rect.height();
1082            let is_last_galley = i + 1 == galleys.len();
1083
1084            merged_galley
1085                .rows
1086                .extend(galley.rows.iter().enumerate().map(|(row_idx, placed_row)| {
1087                    let new_pos = placed_row.pos + current_y_offset * Vec2::Y;
1088                    let new_pos = new_pos.round_to_pixels(pixels_per_point);
1089                    merged_galley.mesh_bounds |=
1090                        placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2());
1091                    merged_galley.rect |= Rect::from_min_size(new_pos, placed_row.size);
1092
1093                    let mut ends_with_newline = placed_row.ends_with_newline;
1094                    let is_last_row_in_galley = row_idx + 1 == galley.rows.len();
1095                    // Since we remove the `\n` when splitting rows, we need to add it back here
1096                    ends_with_newline |= !is_last_galley && is_last_row_in_galley;
1097                    super::PlacedRow {
1098                        pos: new_pos,
1099                        row: Arc::clone(&placed_row.row),
1100                        ends_with_newline,
1101                    }
1102                }));
1103
1104            merged_galley.num_vertices += galley.num_vertices;
1105            merged_galley.num_indices += galley.num_indices;
1106            // Note that if `galley.elided` is true this will be the last `Galley` in
1107            // the vector and the loop will end.
1108            merged_galley.elided |= galley.elided;
1109            merged_galley.intrinsic_size.x =
1110                f32::max(merged_galley.intrinsic_size.x, galley.intrinsic_size.x);
1111            merged_galley.intrinsic_size.y += galley.intrinsic_size.y;
1112        }
1113
1114        if merged_galley.job.round_output_to_gui {
1115            merged_galley.round_output_to_gui();
1116        }
1117
1118        merged_galley
1119    }
1120}
1121
1122impl AsRef<str> for Galley {
1123    #[inline]
1124    fn as_ref(&self) -> &str {
1125        self.text()
1126    }
1127}
1128
1129impl core::borrow::Borrow<str> for Galley {
1130    #[inline]
1131    fn borrow(&self) -> &str {
1132        self.text()
1133    }
1134}
1135
1136impl core::ops::Deref for Galley {
1137    type Target = str;
1138    #[inline]
1139    fn deref(&self) -> &str {
1140        self.text()
1141    }
1142}
1143
1144// ----------------------------------------------------------------------------
1145
1146/// ## Physical positions
1147impl Galley {
1148    /// Zero-width rect past the last character.
1149    fn end_pos(&self) -> Rect {
1150        if let Some(row) = self.rows.last() {
1151            let x = row.rect().right();
1152            Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
1153        } else {
1154            // Empty galley
1155            Rect::from_min_max(pos2(0.0, 0.0), pos2(0.0, 0.0))
1156        }
1157    }
1158
1159    /// Returns a 0-width Rect.
1160    pub fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect {
1161        let Some(row) = self.rows.get(layout_cursor.row) else {
1162            return self.end_pos();
1163        };
1164
1165        let x = row.x_offset(layout_cursor.column) + row.pos.x;
1166        Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
1167    }
1168
1169    /// Returns a 0-width Rect.
1170    pub fn pos_from_cursor(&self, cursor: CCursor) -> Rect {
1171        self.pos_from_layout_cursor(&self.layout_from_cursor(cursor))
1172    }
1173
1174    /// Cursor at the given position within the galley.
1175    ///
1176    /// A cursor above the galley is considered
1177    /// same as a cursor at the start,
1178    /// and a cursor below the galley is considered
1179    /// same as a cursor at the end.
1180    /// This allows implementing text-selection by dragging above/below the galley.
1181    pub fn cursor_from_pos(&self, pos: Vec2) -> CCursor {
1182        // Vertical margin around galley improves text selection UX
1183        const VMARGIN: f32 = 5.0;
1184
1185        if let Some(first_row) = self.rows.first()
1186            && pos.y < first_row.min_y() - VMARGIN
1187        {
1188            return self.begin();
1189        }
1190        if let Some(last_row) = self.rows.last()
1191            && last_row.max_y() + VMARGIN < pos.y
1192        {
1193            return self.end();
1194        }
1195
1196        let mut best_y_dist = f32::INFINITY;
1197        let mut cursor = CCursor::default();
1198
1199        let mut ccursor_index = CharIndex::ZERO;
1200
1201        for row in &self.rows {
1202            let min_y = row.min_y();
1203            let max_y = row.max_y();
1204
1205            let is_pos_within_row = min_y <= pos.y && pos.y <= max_y;
1206            let y_dist = (min_y - pos.y).abs().min((max_y - pos.y).abs());
1207            if is_pos_within_row || y_dist < best_y_dist {
1208                best_y_dist = y_dist;
1209                // char_at is `Row` not `PlacedRow` relative which means we have to subtract the pos.
1210                let column = row.char_at(pos.x - row.pos.x);
1211                let prefer_next_row = column < row.char_count_excluding_newline();
1212                cursor = CCursor {
1213                    index: ccursor_index + column,
1214                    prefer_next_row,
1215                };
1216
1217                if is_pos_within_row {
1218                    return cursor;
1219                }
1220            }
1221            ccursor_index += row.char_count_including_newline();
1222        }
1223
1224        cursor
1225    }
1226}
1227
1228/// ## Cursor positions
1229impl Galley {
1230    /// Cursor to the first character.
1231    ///
1232    /// This is the same as [`CCursor::default`].
1233    #[inline]
1234    #[expect(clippy::unused_self)]
1235    pub fn begin(&self) -> CCursor {
1236        CCursor::default()
1237    }
1238
1239    /// Cursor to one-past last character.
1240    pub fn end(&self) -> CCursor {
1241        if self.rows.is_empty() {
1242            return Default::default();
1243        }
1244        let mut ccursor = CCursor {
1245            index: CharIndex::ZERO,
1246            prefer_next_row: true,
1247        };
1248        for row in &self.rows {
1249            let row_char_count = row.char_count_including_newline();
1250            ccursor.index += row_char_count;
1251        }
1252        ccursor
1253    }
1254}
1255
1256/// ## Cursor conversions
1257impl Galley {
1258    // The returned cursor is clamped.
1259    pub fn layout_from_cursor(&self, cursor: CCursor) -> LayoutCursor {
1260        let prefer_next_row = cursor.prefer_next_row;
1261        let mut ccursor_it = CCursor {
1262            index: CharIndex::ZERO,
1263            prefer_next_row,
1264        };
1265
1266        for (row_nr, row) in self.rows.iter().enumerate() {
1267            let row_char_count = row.char_count_excluding_newline();
1268
1269            if ccursor_it.index <= cursor.index && cursor.index <= ccursor_it.index + row_char_count
1270            {
1271                let column = cursor.index - ccursor_it.index;
1272
1273                let select_next_row_instead = prefer_next_row
1274                    && !row.ends_with_newline
1275                    && column >= row.char_count_excluding_newline();
1276                if !select_next_row_instead {
1277                    return LayoutCursor {
1278                        row: row_nr,
1279                        column,
1280                    };
1281                }
1282            }
1283            ccursor_it.index += row.char_count_including_newline();
1284        }
1285        debug_assert!(ccursor_it == self.end(), "Cursor out of bounds");
1286
1287        if let Some(last_row) = self.rows.last() {
1288            LayoutCursor {
1289                row: self.rows.len() - 1,
1290                column: last_row.char_count_including_newline(),
1291            }
1292        } else {
1293            Default::default()
1294        }
1295    }
1296
1297    fn cursor_from_layout(&self, layout_cursor: LayoutCursor) -> CCursor {
1298        if layout_cursor.row >= self.rows.len() {
1299            return self.end();
1300        }
1301
1302        let prefer_next_row =
1303            layout_cursor.column < self.rows[layout_cursor.row].char_count_excluding_newline();
1304        let mut cursor_it = CCursor {
1305            index: CharIndex::ZERO,
1306            prefer_next_row,
1307        };
1308
1309        for (row_nr, row) in self.rows.iter().enumerate() {
1310            if row_nr == layout_cursor.row {
1311                cursor_it.index += layout_cursor.column.min(row.char_count_excluding_newline());
1312
1313                return cursor_it;
1314            }
1315            cursor_it.index += row.char_count_including_newline();
1316        }
1317        cursor_it
1318    }
1319}
1320
1321/// ## Cursor positions
1322impl Galley {
1323    #[expect(clippy::unused_self)]
1324    pub fn cursor_left_one_character(&self, cursor: &CCursor) -> CCursor {
1325        if cursor.index == CharIndex::ZERO {
1326            Default::default()
1327        } else {
1328            CCursor {
1329                index: cursor.index - 1,
1330                prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the beginning of a row than at the end.
1331            }
1332        }
1333    }
1334
1335    pub fn cursor_right_one_character(&self, cursor: &CCursor) -> CCursor {
1336        CCursor {
1337            index: (cursor.index + 1).min(self.end().index),
1338            prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the beginning of a row than at the end.
1339        }
1340    }
1341
1342    pub fn clamp_cursor(&self, cursor: &CCursor) -> CCursor {
1343        self.cursor_from_layout(self.layout_from_cursor(*cursor))
1344    }
1345
1346    pub fn cursor_up_one_row(
1347        &self,
1348        cursor: &CCursor,
1349        h_pos: Option<f32>,
1350    ) -> (CCursor, Option<f32>) {
1351        let layout_cursor = self.layout_from_cursor(*cursor);
1352        let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
1353        if layout_cursor.row == 0 {
1354            (CCursor::default(), None)
1355        } else {
1356            let new_row = layout_cursor.row - 1;
1357
1358            let new_layout_cursor = {
1359                // keep same X coord
1360                // char_at is Row-relative, so subtract the row's position
1361                let column = self.rows[new_row].char_at(h_pos - self.rows[new_row].pos.x);
1362                LayoutCursor {
1363                    row: new_row,
1364                    column,
1365                }
1366            };
1367            (self.cursor_from_layout(new_layout_cursor), Some(h_pos))
1368        }
1369    }
1370
1371    pub fn cursor_down_one_row(
1372        &self,
1373        cursor: &CCursor,
1374        h_pos: Option<f32>,
1375    ) -> (CCursor, Option<f32>) {
1376        let layout_cursor = self.layout_from_cursor(*cursor);
1377        let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
1378        if layout_cursor.row + 1 < self.rows.len() {
1379            let new_row = layout_cursor.row + 1;
1380
1381            let new_layout_cursor = {
1382                // keep same X coord
1383                // char_at is Row-relative, so subtract the row's position
1384                let column = self.rows[new_row].char_at(h_pos - self.rows[new_row].pos.x);
1385                LayoutCursor {
1386                    row: new_row,
1387                    column,
1388                }
1389            };
1390
1391            (self.cursor_from_layout(new_layout_cursor), Some(h_pos))
1392        } else {
1393            (self.end(), None)
1394        }
1395    }
1396
1397    pub fn cursor_begin_of_row(&self, cursor: &CCursor) -> CCursor {
1398        let layout_cursor = self.layout_from_cursor(*cursor);
1399        self.cursor_from_layout(LayoutCursor {
1400            row: layout_cursor.row,
1401            column: CharIndex::ZERO,
1402        })
1403    }
1404
1405    pub fn cursor_end_of_row(&self, cursor: &CCursor) -> CCursor {
1406        let layout_cursor = self.layout_from_cursor(*cursor);
1407        self.cursor_from_layout(LayoutCursor {
1408            row: layout_cursor.row,
1409            column: self.rows[layout_cursor.row].char_count_excluding_newline(),
1410        })
1411    }
1412
1413    pub fn cursor_begin_of_paragraph(&self, cursor: &CCursor) -> CCursor {
1414        let mut layout_cursor = self.layout_from_cursor(*cursor);
1415        layout_cursor.column = CharIndex::ZERO;
1416
1417        loop {
1418            let prev_row = layout_cursor
1419                .row
1420                .checked_sub(1)
1421                .and_then(|row| self.rows.get(row));
1422
1423            let Some(prev_row) = prev_row else {
1424                // This is the first row
1425                break;
1426            };
1427
1428            if prev_row.ends_with_newline {
1429                break;
1430            }
1431
1432            layout_cursor.row -= 1;
1433        }
1434
1435        self.cursor_from_layout(layout_cursor)
1436    }
1437
1438    pub fn cursor_end_of_paragraph(&self, cursor: &CCursor) -> CCursor {
1439        let mut layout_cursor = self.layout_from_cursor(*cursor);
1440        loop {
1441            let row = &self.rows[layout_cursor.row];
1442            if row.ends_with_newline || layout_cursor.row == self.rows.len() - 1 {
1443                layout_cursor.column = row.char_count_excluding_newline();
1444                break;
1445            }
1446
1447            layout_cursor.row += 1;
1448        }
1449
1450        self.cursor_from_layout(layout_cursor)
1451    }
1452}