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.glyphs.first().map_or(self.pos.x, |g| g.pos.x);
700        let size_x = self.size.x - x;
701        Rect::from_min_size(Pos2::new(x, self.pos.y), Vec2::new(size_x, self.size.y))
702    }
703}
704
705impl std::ops::Deref for PlacedRow {
706    type Target = Row;
707
708    fn deref(&self) -> &Self::Target {
709        &self.row
710    }
711}
712
713#[derive(Clone, Debug, PartialEq)]
714#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
715pub struct Row {
716    /// This is included in case there are no glyphs.
717    ///
718    /// Only used during layout, then set to an invalid value in order to
719    /// enable the paragraph-concat optimization path without having to
720    /// adjust `section_index` when concatting.
721    pub(crate) section_index_at_start: u32,
722
723    /// One for each `char`.
724    pub glyphs: Vec<Glyph>,
725
726    /// Logical size based on font heights etc.
727    /// Includes leading and trailing whitespace.
728    pub size: Vec2,
729
730    /// The mesh, ready to be rendered.
731    pub visuals: RowVisuals,
732}
733
734/// The tessellated output of a row.
735#[derive(Clone, Debug, PartialEq, Eq)]
736#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
737pub struct RowVisuals {
738    /// The tessellated text, using non-normalized (texel) UV coordinates.
739    /// That is, you need to divide the uv coordinates by the texture size.
740    pub mesh: Mesh,
741
742    /// Bounds of the mesh, and can be used for culling.
743    /// Does NOT include leading or trailing whitespace glyphs!!
744    pub mesh_bounds: Rect,
745
746    /// The number of triangle indices added before the first glyph triangle.
747    ///
748    /// This can be used to insert more triangles after the background but before the glyphs,
749    /// i.e. for text selection visualization.
750    pub glyph_index_start: usize,
751
752    /// The range of vertices in the mesh that contain glyphs (as opposed to background, underlines, strikethorugh, etc).
753    ///
754    /// The glyph vertices comes after backgrounds (if any), but before any underlines and strikethrough.
755    pub glyph_vertex_range: Range<usize>,
756}
757
758impl Default for RowVisuals {
759    fn default() -> Self {
760        Self {
761            mesh: Default::default(),
762            mesh_bounds: Rect::NOTHING,
763            glyph_index_start: 0,
764            glyph_vertex_range: 0..0,
765        }
766    }
767}
768
769#[derive(Copy, Clone, Debug, PartialEq)]
770#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
771pub struct Glyph {
772    /// The character this glyph represents.
773    pub chr: char,
774
775    /// Baseline position, relative to the row.
776    /// Logical position: pos.y is the same for all chars of the same [`TextFormat`].
777    pub pos: Pos2,
778
779    /// Logical width of the glyph.
780    pub advance_width: f32,
781
782    /// Height of this row of text.
783    ///
784    /// Usually same as [`Self::font_height`],
785    /// unless explicitly overridden by [`TextFormat::line_height`].
786    pub line_height: f32,
787
788    /// The ascent of this font.
789    pub font_ascent: f32,
790
791    /// The row/line height of this font.
792    pub font_height: f32,
793
794    /// The ascent of the sub-font within the font (`FontFace`).
795    pub font_face_ascent: f32,
796
797    /// The row/line height of the sub-font within the font (`FontFace`).
798    pub font_face_height: f32,
799
800    /// Position and size of the glyph in the font texture, in texels.
801    pub uv_rect: UvRect,
802
803    /// Index into [`LayoutJob::sections`]. Decides color etc.
804    ///
805    /// Only used during layout, then set to an invalid value in order to
806    /// enable the paragraph-concat optimization path without having to
807    /// adjust `section_index` when concatting.
808    pub(crate) section_index: u32,
809
810    /// Which is our first vertex in [`RowVisuals::mesh`].
811    pub first_vertex: u32,
812}
813
814impl Glyph {
815    #[inline]
816    pub fn size(&self) -> Vec2 {
817        Vec2::new(self.advance_width, self.line_height)
818    }
819
820    #[inline]
821    pub fn max_x(&self) -> f32 {
822        self.pos.x + self.advance_width
823    }
824
825    /// Same y range for all characters with the same [`TextFormat`].
826    #[inline]
827    pub fn logical_rect(&self) -> Rect {
828        Rect::from_min_size(self.pos - vec2(0.0, self.font_ascent), self.size())
829    }
830}
831
832// ----------------------------------------------------------------------------
833
834impl Row {
835    /// The text on this row, excluding the implicit `\n` if any.
836    pub fn text(&self) -> String {
837        self.glyphs.iter().map(|g| g.chr).collect()
838    }
839
840    /// Excludes the implicit `\n` after the [`Row`], if any.
841    #[inline]
842    pub fn char_count_excluding_newline(&self) -> usize {
843        self.glyphs.len()
844    }
845
846    /// Closest char at the desired x coordinate in row-relative coordinates.
847    /// Returns something in the range `[0, char_count_excluding_newline()]`.
848    pub fn char_at(&self, desired_x: f32) -> usize {
849        for (i, glyph) in self.glyphs.iter().enumerate() {
850            if desired_x < glyph.logical_rect().center().x {
851                return i;
852            }
853        }
854        self.char_count_excluding_newline()
855    }
856
857    pub fn x_offset(&self, column: usize) -> f32 {
858        if let Some(glyph) = self.glyphs.get(column) {
859            glyph.pos.x
860        } else {
861            self.size.x
862        }
863    }
864
865    #[inline]
866    pub fn height(&self) -> f32 {
867        self.size.y
868    }
869}
870
871impl PlacedRow {
872    #[inline]
873    pub fn min_y(&self) -> f32 {
874        self.rect().top()
875    }
876
877    #[inline]
878    pub fn max_y(&self) -> f32 {
879        self.rect().bottom()
880    }
881
882    /// Includes the implicit `\n` after the [`PlacedRow`], if any.
883    #[inline]
884    pub fn char_count_including_newline(&self) -> usize {
885        self.row.glyphs.len() + (self.ends_with_newline as usize)
886    }
887}
888
889impl Galley {
890    #[inline]
891    pub fn is_empty(&self) -> bool {
892        self.job.is_empty()
893    }
894
895    /// The full, non-elided text of the input job.
896    #[inline]
897    pub fn text(&self) -> &str {
898        &self.job.text
899    }
900
901    #[inline]
902    pub fn size(&self) -> Vec2 {
903        self.rect.size()
904    }
905
906    /// This is the size that a non-wrapped, non-truncated, non-justified version of the text
907    /// would have.
908    ///
909    /// Useful for advanced layouting.
910    #[inline]
911    pub fn intrinsic_size(&self) -> Vec2 {
912        // We do the rounding here instead of in `round_output_to_gui` so that rounding
913        // errors don't accumulate when concatenating multiple galleys.
914        if self.job.round_output_to_gui {
915            self.intrinsic_size.round_ui()
916        } else {
917            self.intrinsic_size
918        }
919    }
920
921    pub(crate) fn round_output_to_gui(&mut self) {
922        for placed_row in &mut self.rows {
923            // Optimization: only call `make_mut` if necessary (can cause a deep clone)
924            let rounded_size = placed_row.row.size.round_ui();
925            if placed_row.row.size != rounded_size {
926                Arc::make_mut(&mut placed_row.row).size = rounded_size;
927            }
928        }
929
930        let rect = &mut self.rect;
931
932        let did_exceed_wrap_width_by_a_lot = rect.width() > self.job.wrap.max_width + 1.0;
933
934        *rect = rect.round_ui();
935
936        if did_exceed_wrap_width_by_a_lot {
937            // If the user picked a too aggressive wrap width (e.g. more narrow than any individual glyph),
938            // we should let the user know by reporting that our width is wider than the wrap width.
939        } else {
940            // Make sure we don't report being wider than the wrap width the user picked:
941            rect.max.x = rect
942                .max
943                .x
944                .at_most(rect.min.x + self.job.wrap.max_width)
945                .floor_ui();
946        }
947    }
948
949    /// Append each galley under the previous one.
950    pub fn concat(job: Arc<LayoutJob>, galleys: &[Arc<Self>], pixels_per_point: f32) -> Self {
951        profiling::function_scope!();
952
953        let mut merged_galley = Self {
954            job,
955            rows: Vec::new(),
956            elided: false,
957            rect: Rect::ZERO,
958            mesh_bounds: Rect::NOTHING,
959            num_vertices: 0,
960            num_indices: 0,
961            pixels_per_point,
962            intrinsic_size: Vec2::ZERO,
963        };
964
965        for (i, galley) in galleys.iter().enumerate() {
966            let current_y_offset = merged_galley.rect.height();
967            let is_last_galley = i + 1 == galleys.len();
968
969            merged_galley
970                .rows
971                .extend(galley.rows.iter().enumerate().map(|(row_idx, placed_row)| {
972                    let new_pos = placed_row.pos + current_y_offset * Vec2::Y;
973                    let new_pos = new_pos.round_to_pixels(pixels_per_point);
974                    merged_galley.mesh_bounds |=
975                        placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2());
976                    merged_galley.rect |= Rect::from_min_size(new_pos, placed_row.size);
977
978                    let mut ends_with_newline = placed_row.ends_with_newline;
979                    let is_last_row_in_galley = row_idx + 1 == galley.rows.len();
980                    // Since we remove the `\n` when splitting rows, we need to add it back here
981                    ends_with_newline |= !is_last_galley && is_last_row_in_galley;
982                    super::PlacedRow {
983                        pos: new_pos,
984                        row: Arc::clone(&placed_row.row),
985                        ends_with_newline,
986                    }
987                }));
988
989            merged_galley.num_vertices += galley.num_vertices;
990            merged_galley.num_indices += galley.num_indices;
991            // Note that if `galley.elided` is true this will be the last `Galley` in
992            // the vector and the loop will end.
993            merged_galley.elided |= galley.elided;
994            merged_galley.intrinsic_size.x =
995                f32::max(merged_galley.intrinsic_size.x, galley.intrinsic_size.x);
996            merged_galley.intrinsic_size.y += galley.intrinsic_size.y;
997        }
998
999        if merged_galley.job.round_output_to_gui {
1000            merged_galley.round_output_to_gui();
1001        }
1002
1003        merged_galley
1004    }
1005}
1006
1007impl AsRef<str> for Galley {
1008    #[inline]
1009    fn as_ref(&self) -> &str {
1010        self.text()
1011    }
1012}
1013
1014impl std::borrow::Borrow<str> for Galley {
1015    #[inline]
1016    fn borrow(&self) -> &str {
1017        self.text()
1018    }
1019}
1020
1021impl std::ops::Deref for Galley {
1022    type Target = str;
1023    #[inline]
1024    fn deref(&self) -> &str {
1025        self.text()
1026    }
1027}
1028
1029// ----------------------------------------------------------------------------
1030
1031/// ## Physical positions
1032impl Galley {
1033    /// Zero-width rect past the last character.
1034    fn end_pos(&self) -> Rect {
1035        if let Some(row) = self.rows.last() {
1036            let x = row.rect().right();
1037            Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
1038        } else {
1039            // Empty galley
1040            Rect::from_min_max(pos2(0.0, 0.0), pos2(0.0, 0.0))
1041        }
1042    }
1043
1044    /// Returns a 0-width Rect.
1045    pub fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect {
1046        let Some(row) = self.rows.get(layout_cursor.row) else {
1047            return self.end_pos();
1048        };
1049
1050        let x = row.x_offset(layout_cursor.column) + row.pos.x - self.rect.left();
1051        Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
1052    }
1053
1054    /// Returns a 0-width Rect.
1055    pub fn pos_from_cursor(&self, cursor: CCursor) -> Rect {
1056        self.pos_from_layout_cursor(&self.layout_from_cursor(cursor))
1057    }
1058
1059    /// Cursor at the given position within the galley.
1060    ///
1061    /// A cursor above the galley is considered
1062    /// same as a cursor at the start,
1063    /// and a cursor below the galley is considered
1064    /// same as a cursor at the end.
1065    /// This allows implementing text-selection by dragging above/below the galley.
1066    pub fn cursor_from_pos(&self, pos: Vec2) -> CCursor {
1067        // Vertical margin around galley improves text selection UX
1068        const VMARGIN: f32 = 5.0;
1069
1070        if let Some(first_row) = self.rows.first()
1071            && pos.y < first_row.min_y() - VMARGIN
1072        {
1073            return self.begin();
1074        }
1075        if let Some(last_row) = self.rows.last()
1076            && last_row.max_y() + VMARGIN < pos.y
1077        {
1078            return self.end();
1079        }
1080
1081        let mut best_y_dist = f32::INFINITY;
1082        let mut cursor = CCursor::default();
1083
1084        let mut ccursor_index = 0;
1085
1086        for row in &self.rows {
1087            let min_y = row.min_y();
1088            let max_y = row.max_y();
1089
1090            let is_pos_within_row = min_y <= pos.y && pos.y <= max_y;
1091            let y_dist = (min_y - pos.y).abs().min((max_y - pos.y).abs());
1092            if is_pos_within_row || y_dist < best_y_dist {
1093                best_y_dist = y_dist;
1094                // char_at is `Row` not `PlacedRow` relative which means we have to subtract the pos.
1095                let column = row.char_at(pos.x - row.pos.x + self.rect.left());
1096                let prefer_next_row = column < row.char_count_excluding_newline();
1097                cursor = CCursor {
1098                    index: ccursor_index + column,
1099                    prefer_next_row,
1100                };
1101
1102                if is_pos_within_row {
1103                    return cursor;
1104                }
1105            }
1106            ccursor_index += row.char_count_including_newline();
1107        }
1108
1109        cursor
1110    }
1111}
1112
1113/// ## Cursor positions
1114impl Galley {
1115    /// Cursor to the first character.
1116    ///
1117    /// This is the same as [`CCursor::default`].
1118    #[inline]
1119    #[expect(clippy::unused_self)]
1120    pub fn begin(&self) -> CCursor {
1121        CCursor::default()
1122    }
1123
1124    /// Cursor to one-past last character.
1125    pub fn end(&self) -> CCursor {
1126        if self.rows.is_empty() {
1127            return Default::default();
1128        }
1129        let mut ccursor = CCursor {
1130            index: 0,
1131            prefer_next_row: true,
1132        };
1133        for row in &self.rows {
1134            let row_char_count = row.char_count_including_newline();
1135            ccursor.index += row_char_count;
1136        }
1137        ccursor
1138    }
1139}
1140
1141/// ## Cursor conversions
1142impl Galley {
1143    // The returned cursor is clamped.
1144    pub fn layout_from_cursor(&self, cursor: CCursor) -> LayoutCursor {
1145        let prefer_next_row = cursor.prefer_next_row;
1146        let mut ccursor_it = CCursor {
1147            index: 0,
1148            prefer_next_row,
1149        };
1150
1151        for (row_nr, row) in self.rows.iter().enumerate() {
1152            let row_char_count = row.char_count_excluding_newline();
1153
1154            if ccursor_it.index <= cursor.index && cursor.index <= ccursor_it.index + row_char_count
1155            {
1156                let column = cursor.index - ccursor_it.index;
1157
1158                let select_next_row_instead = prefer_next_row
1159                    && !row.ends_with_newline
1160                    && column >= row.char_count_excluding_newline();
1161                if !select_next_row_instead {
1162                    return LayoutCursor {
1163                        row: row_nr,
1164                        column,
1165                    };
1166                }
1167            }
1168            ccursor_it.index += row.char_count_including_newline();
1169        }
1170        debug_assert!(ccursor_it == self.end(), "Cursor out of bounds");
1171
1172        if let Some(last_row) = self.rows.last() {
1173            LayoutCursor {
1174                row: self.rows.len() - 1,
1175                column: last_row.char_count_including_newline(),
1176            }
1177        } else {
1178            Default::default()
1179        }
1180    }
1181
1182    fn cursor_from_layout(&self, layout_cursor: LayoutCursor) -> CCursor {
1183        if layout_cursor.row >= self.rows.len() {
1184            return self.end();
1185        }
1186
1187        let prefer_next_row =
1188            layout_cursor.column < self.rows[layout_cursor.row].char_count_excluding_newline();
1189        let mut cursor_it = CCursor {
1190            index: 0,
1191            prefer_next_row,
1192        };
1193
1194        for (row_nr, row) in self.rows.iter().enumerate() {
1195            if row_nr == layout_cursor.row {
1196                cursor_it.index += layout_cursor
1197                    .column
1198                    .at_most(row.char_count_excluding_newline());
1199
1200                return cursor_it;
1201            }
1202            cursor_it.index += row.char_count_including_newline();
1203        }
1204        cursor_it
1205    }
1206}
1207
1208/// ## Cursor positions
1209impl Galley {
1210    #[expect(clippy::unused_self)]
1211    pub fn cursor_left_one_character(&self, cursor: &CCursor) -> CCursor {
1212        if cursor.index == 0 {
1213            Default::default()
1214        } else {
1215            CCursor {
1216                index: cursor.index - 1,
1217                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.
1218            }
1219        }
1220    }
1221
1222    pub fn cursor_right_one_character(&self, cursor: &CCursor) -> CCursor {
1223        CCursor {
1224            index: (cursor.index + 1).min(self.end().index),
1225            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.
1226        }
1227    }
1228
1229    pub fn clamp_cursor(&self, cursor: &CCursor) -> CCursor {
1230        self.cursor_from_layout(self.layout_from_cursor(*cursor))
1231    }
1232
1233    pub fn cursor_up_one_row(
1234        &self,
1235        cursor: &CCursor,
1236        h_pos: Option<f32>,
1237    ) -> (CCursor, Option<f32>) {
1238        let layout_cursor = self.layout_from_cursor(*cursor);
1239        let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
1240        if layout_cursor.row == 0 {
1241            (CCursor::default(), None)
1242        } else {
1243            let new_row = layout_cursor.row - 1;
1244
1245            let new_layout_cursor = {
1246                // keep same X coord
1247                let column = self.rows[new_row].char_at(h_pos);
1248                LayoutCursor {
1249                    row: new_row,
1250                    column,
1251                }
1252            };
1253            (self.cursor_from_layout(new_layout_cursor), Some(h_pos))
1254        }
1255    }
1256
1257    pub fn cursor_down_one_row(
1258        &self,
1259        cursor: &CCursor,
1260        h_pos: Option<f32>,
1261    ) -> (CCursor, Option<f32>) {
1262        let layout_cursor = self.layout_from_cursor(*cursor);
1263        let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
1264        if layout_cursor.row + 1 < self.rows.len() {
1265            let new_row = layout_cursor.row + 1;
1266
1267            let new_layout_cursor = {
1268                // keep same X coord
1269                let column = self.rows[new_row].char_at(h_pos);
1270                LayoutCursor {
1271                    row: new_row,
1272                    column,
1273                }
1274            };
1275
1276            (self.cursor_from_layout(new_layout_cursor), Some(h_pos))
1277        } else {
1278            (self.end(), None)
1279        }
1280    }
1281
1282    pub fn cursor_begin_of_row(&self, cursor: &CCursor) -> CCursor {
1283        let layout_cursor = self.layout_from_cursor(*cursor);
1284        self.cursor_from_layout(LayoutCursor {
1285            row: layout_cursor.row,
1286            column: 0,
1287        })
1288    }
1289
1290    pub fn cursor_end_of_row(&self, cursor: &CCursor) -> CCursor {
1291        let layout_cursor = self.layout_from_cursor(*cursor);
1292        self.cursor_from_layout(LayoutCursor {
1293            row: layout_cursor.row,
1294            column: self.rows[layout_cursor.row].char_count_excluding_newline(),
1295        })
1296    }
1297
1298    pub fn cursor_begin_of_paragraph(&self, cursor: &CCursor) -> CCursor {
1299        let mut layout_cursor = self.layout_from_cursor(*cursor);
1300        layout_cursor.column = 0;
1301
1302        loop {
1303            let prev_row = layout_cursor
1304                .row
1305                .checked_sub(1)
1306                .and_then(|row| self.rows.get(row));
1307
1308            let Some(prev_row) = prev_row else {
1309                // This is the first row
1310                break;
1311            };
1312
1313            if prev_row.ends_with_newline {
1314                break;
1315            }
1316
1317            layout_cursor.row -= 1;
1318        }
1319
1320        self.cursor_from_layout(layout_cursor)
1321    }
1322
1323    pub fn cursor_end_of_paragraph(&self, cursor: &CCursor) -> CCursor {
1324        let mut layout_cursor = self.layout_from_cursor(*cursor);
1325        loop {
1326            let row = &self.rows[layout_cursor.row];
1327            if row.ends_with_newline || layout_cursor.row == self.rows.len() - 1 {
1328                layout_cursor.column = row.char_count_excluding_newline();
1329                break;
1330            }
1331
1332            layout_cursor.row += 1;
1333        }
1334
1335        self.cursor_from_layout(layout_cursor)
1336    }
1337}