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