epaint/text/
text_layout_types.rs

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