Skip to main content

epaint/text/
text_layout_types.rs

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