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
706impl Glyph {
707    #[inline]
708    pub fn size(&self) -> Vec2 {
709        Vec2::new(self.advance_width, self.line_height)
710    }
711
712    #[inline]
713    pub fn max_x(&self) -> f32 {
714        self.pos.x + self.advance_width
715    }
716
717    /// Same y range for all characters with the same [`TextFormat`].
718    #[inline]
719    pub fn logical_rect(&self) -> Rect {
720        Rect::from_min_size(self.pos - vec2(0.0, self.font_ascent), self.size())
721    }
722}
723
724// ----------------------------------------------------------------------------
725
726impl Row {
727    /// The text on this row, excluding the implicit `\n` if any.
728    pub fn text(&self) -> String {
729        self.glyphs.iter().map(|g| g.chr).collect()
730    }
731
732    /// Excludes the implicit `\n` after the [`Row`], if any.
733    #[inline]
734    pub fn char_count_excluding_newline(&self) -> usize {
735        self.glyphs.len()
736    }
737
738    /// Includes the implicit `\n` after the [`Row`], if any.
739    #[inline]
740    pub fn char_count_including_newline(&self) -> usize {
741        self.glyphs.len() + (self.ends_with_newline as usize)
742    }
743
744    /// Closest char at the desired x coordinate in row-relative coordinates.
745    /// Returns something in the range `[0, char_count_excluding_newline()]`.
746    pub fn char_at(&self, desired_x: f32) -> usize {
747        for (i, glyph) in self.glyphs.iter().enumerate() {
748            if desired_x < glyph.logical_rect().center().x {
749                return i;
750            }
751        }
752        self.char_count_excluding_newline()
753    }
754
755    pub fn x_offset(&self, column: usize) -> f32 {
756        if let Some(glyph) = self.glyphs.get(column) {
757            glyph.pos.x
758        } else {
759            self.size.x
760        }
761    }
762
763    #[inline]
764    pub fn height(&self) -> f32 {
765        self.size.y
766    }
767}
768
769impl PlacedRow {
770    #[inline]
771    pub fn min_y(&self) -> f32 {
772        self.rect().top()
773    }
774
775    #[inline]
776    pub fn max_y(&self) -> f32 {
777        self.rect().bottom()
778    }
779}
780
781impl Galley {
782    #[inline]
783    pub fn is_empty(&self) -> bool {
784        self.job.is_empty()
785    }
786
787    /// The full, non-elided text of the input job.
788    #[inline]
789    pub fn text(&self) -> &str {
790        &self.job.text
791    }
792
793    #[inline]
794    pub fn size(&self) -> Vec2 {
795        self.rect.size()
796    }
797
798    /// This is the size that a non-wrapped, non-truncated, non-justified version of the text
799    /// would have.
800    ///
801    /// Useful for advanced layouting.
802    #[inline]
803    pub fn intrinsic_size(&self) -> Vec2 {
804        // We do the rounding here instead of in `round_output_to_gui` so that rounding
805        // errors don't accumulate when concatenating multiple galleys.
806        if self.job.round_output_to_gui {
807            self.intrinsic_size.round_ui()
808        } else {
809            self.intrinsic_size
810        }
811    }
812
813    pub(crate) fn round_output_to_gui(&mut self) {
814        for placed_row in &mut self.rows {
815            // Optimization: only call `make_mut` if necessary (can cause a deep clone)
816            let rounded_size = placed_row.row.size.round_ui();
817            if placed_row.row.size != rounded_size {
818                Arc::make_mut(&mut placed_row.row).size = rounded_size;
819            }
820        }
821
822        let rect = &mut self.rect;
823
824        let did_exceed_wrap_width_by_a_lot = rect.width() > self.job.wrap.max_width + 1.0;
825
826        *rect = rect.round_ui();
827
828        if did_exceed_wrap_width_by_a_lot {
829            // If the user picked a too aggressive wrap width (e.g. more narrow than any individual glyph),
830            // we should let the user know by reporting that our width is wider than the wrap width.
831        } else {
832            // Make sure we don't report being wider than the wrap width the user picked:
833            rect.max.x = rect
834                .max
835                .x
836                .at_most(rect.min.x + self.job.wrap.max_width)
837                .floor_ui();
838        }
839    }
840
841    /// Append each galley under the previous one.
842    pub fn concat(job: Arc<LayoutJob>, galleys: &[Arc<Self>], pixels_per_point: f32) -> Self {
843        profiling::function_scope!();
844
845        let mut merged_galley = Self {
846            job,
847            rows: Vec::new(),
848            elided: false,
849            rect: Rect::ZERO,
850            mesh_bounds: Rect::NOTHING,
851            num_vertices: 0,
852            num_indices: 0,
853            pixels_per_point,
854            intrinsic_size: Vec2::ZERO,
855        };
856
857        for (i, galley) in galleys.iter().enumerate() {
858            let current_y_offset = merged_galley.rect.height();
859            let is_last_galley = i + 1 == galleys.len();
860
861            merged_galley
862                .rows
863                .extend(galley.rows.iter().enumerate().map(|(row_idx, placed_row)| {
864                    let new_pos = placed_row.pos + current_y_offset * Vec2::Y;
865                    let new_pos = new_pos.round_to_pixels(pixels_per_point);
866                    merged_galley.mesh_bounds |=
867                        placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2());
868                    merged_galley.rect |= Rect::from_min_size(new_pos, placed_row.size);
869
870                    let mut row = placed_row.row.clone();
871                    let is_last_row_in_galley = row_idx + 1 == galley.rows.len();
872                    if !is_last_galley && is_last_row_in_galley {
873                        // Since we remove the `\n` when splitting rows, we need to add it back here
874                        Arc::make_mut(&mut row).ends_with_newline = true;
875                    }
876                    super::PlacedRow { pos: new_pos, row }
877                }));
878
879            merged_galley.num_vertices += galley.num_vertices;
880            merged_galley.num_indices += galley.num_indices;
881            // Note that if `galley.elided` is true this will be the last `Galley` in
882            // the vector and the loop will end.
883            merged_galley.elided |= galley.elided;
884            merged_galley.intrinsic_size.x =
885                f32::max(merged_galley.intrinsic_size.x, galley.intrinsic_size.x);
886            merged_galley.intrinsic_size.y += galley.intrinsic_size.y;
887        }
888
889        if merged_galley.job.round_output_to_gui {
890            merged_galley.round_output_to_gui();
891        }
892
893        merged_galley
894    }
895}
896
897impl AsRef<str> for Galley {
898    #[inline]
899    fn as_ref(&self) -> &str {
900        self.text()
901    }
902}
903
904impl std::borrow::Borrow<str> for Galley {
905    #[inline]
906    fn borrow(&self) -> &str {
907        self.text()
908    }
909}
910
911impl std::ops::Deref for Galley {
912    type Target = str;
913    #[inline]
914    fn deref(&self) -> &str {
915        self.text()
916    }
917}
918
919// ----------------------------------------------------------------------------
920
921/// ## Physical positions
922impl Galley {
923    /// Zero-width rect past the last character.
924    fn end_pos(&self) -> Rect {
925        if let Some(row) = self.rows.last() {
926            let x = row.rect().right();
927            Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
928        } else {
929            // Empty galley
930            Rect::from_min_max(pos2(0.0, 0.0), pos2(0.0, 0.0))
931        }
932    }
933
934    /// Returns a 0-width Rect.
935    fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect {
936        let Some(row) = self.rows.get(layout_cursor.row) else {
937            return self.end_pos();
938        };
939
940        let x = row.x_offset(layout_cursor.column);
941        Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
942    }
943
944    /// Returns a 0-width Rect.
945    pub fn pos_from_cursor(&self, cursor: CCursor) -> Rect {
946        self.pos_from_layout_cursor(&self.layout_from_cursor(cursor))
947    }
948
949    /// Cursor at the given position within the galley.
950    ///
951    /// A cursor above the galley is considered
952    /// same as a cursor at the start,
953    /// and a cursor below the galley is considered
954    /// same as a cursor at the end.
955    /// This allows implementing text-selection by dragging above/below the galley.
956    pub fn cursor_from_pos(&self, pos: Vec2) -> CCursor {
957        // Vertical margin around galley improves text selection UX
958        const VMARGIN: f32 = 5.0;
959
960        if let Some(first_row) = self.rows.first()
961            && pos.y < first_row.min_y() - VMARGIN
962        {
963            return self.begin();
964        }
965        if let Some(last_row) = self.rows.last()
966            && last_row.max_y() + VMARGIN < pos.y
967        {
968            return self.end();
969        }
970
971        let mut best_y_dist = f32::INFINITY;
972        let mut cursor = CCursor::default();
973
974        let mut ccursor_index = 0;
975
976        for row in &self.rows {
977            let min_y = row.min_y();
978            let max_y = row.max_y();
979
980            let is_pos_within_row = min_y <= pos.y && pos.y <= max_y;
981            let y_dist = (min_y - pos.y).abs().min((max_y - pos.y).abs());
982            if is_pos_within_row || y_dist < best_y_dist {
983                best_y_dist = y_dist;
984                // char_at is `Row` not `PlacedRow` relative which means we have to subtract the pos.
985                let column = row.char_at(pos.x - row.pos.x);
986                let prefer_next_row = column < row.char_count_excluding_newline();
987                cursor = CCursor {
988                    index: ccursor_index + column,
989                    prefer_next_row,
990                };
991
992                if is_pos_within_row {
993                    return cursor;
994                }
995            }
996            ccursor_index += row.char_count_including_newline();
997        }
998
999        cursor
1000    }
1001}
1002
1003/// ## Cursor positions
1004impl Galley {
1005    /// Cursor to the first character.
1006    ///
1007    /// This is the same as [`CCursor::default`].
1008    #[inline]
1009    #[expect(clippy::unused_self)]
1010    pub fn begin(&self) -> CCursor {
1011        CCursor::default()
1012    }
1013
1014    /// Cursor to one-past last character.
1015    pub fn end(&self) -> CCursor {
1016        if self.rows.is_empty() {
1017            return Default::default();
1018        }
1019        let mut ccursor = CCursor {
1020            index: 0,
1021            prefer_next_row: true,
1022        };
1023        for row in &self.rows {
1024            let row_char_count = row.char_count_including_newline();
1025            ccursor.index += row_char_count;
1026        }
1027        ccursor
1028    }
1029}
1030
1031/// ## Cursor conversions
1032impl Galley {
1033    // The returned cursor is clamped.
1034    pub fn layout_from_cursor(&self, cursor: CCursor) -> LayoutCursor {
1035        let prefer_next_row = cursor.prefer_next_row;
1036        let mut ccursor_it = CCursor {
1037            index: 0,
1038            prefer_next_row,
1039        };
1040
1041        for (row_nr, row) in self.rows.iter().enumerate() {
1042            let row_char_count = row.char_count_excluding_newline();
1043
1044            if ccursor_it.index <= cursor.index && cursor.index <= ccursor_it.index + row_char_count
1045            {
1046                let column = cursor.index - ccursor_it.index;
1047
1048                let select_next_row_instead = prefer_next_row
1049                    && !row.ends_with_newline
1050                    && column >= row.char_count_excluding_newline();
1051                if !select_next_row_instead {
1052                    return LayoutCursor {
1053                        row: row_nr,
1054                        column,
1055                    };
1056                }
1057            }
1058            ccursor_it.index += row.char_count_including_newline();
1059        }
1060        debug_assert!(ccursor_it == self.end(), "Cursor out of bounds");
1061
1062        if let Some(last_row) = self.rows.last() {
1063            LayoutCursor {
1064                row: self.rows.len() - 1,
1065                column: last_row.char_count_including_newline(),
1066            }
1067        } else {
1068            Default::default()
1069        }
1070    }
1071
1072    fn cursor_from_layout(&self, layout_cursor: LayoutCursor) -> CCursor {
1073        if layout_cursor.row >= self.rows.len() {
1074            return self.end();
1075        }
1076
1077        let prefer_next_row =
1078            layout_cursor.column < self.rows[layout_cursor.row].char_count_excluding_newline();
1079        let mut cursor_it = CCursor {
1080            index: 0,
1081            prefer_next_row,
1082        };
1083
1084        for (row_nr, row) in self.rows.iter().enumerate() {
1085            if row_nr == layout_cursor.row {
1086                cursor_it.index += layout_cursor
1087                    .column
1088                    .at_most(row.char_count_excluding_newline());
1089
1090                return cursor_it;
1091            }
1092            cursor_it.index += row.char_count_including_newline();
1093        }
1094        cursor_it
1095    }
1096}
1097
1098/// ## Cursor positions
1099impl Galley {
1100    #[expect(clippy::unused_self)]
1101    pub fn cursor_left_one_character(&self, cursor: &CCursor) -> CCursor {
1102        if cursor.index == 0 {
1103            Default::default()
1104        } else {
1105            CCursor {
1106                index: cursor.index - 1,
1107                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.
1108            }
1109        }
1110    }
1111
1112    pub fn cursor_right_one_character(&self, cursor: &CCursor) -> CCursor {
1113        CCursor {
1114            index: (cursor.index + 1).min(self.end().index),
1115            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.
1116        }
1117    }
1118
1119    pub fn clamp_cursor(&self, cursor: &CCursor) -> CCursor {
1120        self.cursor_from_layout(self.layout_from_cursor(*cursor))
1121    }
1122
1123    pub fn cursor_up_one_row(
1124        &self,
1125        cursor: &CCursor,
1126        h_pos: Option<f32>,
1127    ) -> (CCursor, Option<f32>) {
1128        let layout_cursor = self.layout_from_cursor(*cursor);
1129        let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
1130        if layout_cursor.row == 0 {
1131            (CCursor::default(), None)
1132        } else {
1133            let new_row = layout_cursor.row - 1;
1134
1135            let new_layout_cursor = {
1136                // keep same X coord
1137                let column = self.rows[new_row].char_at(h_pos);
1138                LayoutCursor {
1139                    row: new_row,
1140                    column,
1141                }
1142            };
1143            (self.cursor_from_layout(new_layout_cursor), Some(h_pos))
1144        }
1145    }
1146
1147    pub fn cursor_down_one_row(
1148        &self,
1149        cursor: &CCursor,
1150        h_pos: Option<f32>,
1151    ) -> (CCursor, Option<f32>) {
1152        let layout_cursor = self.layout_from_cursor(*cursor);
1153        let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
1154        if layout_cursor.row + 1 < self.rows.len() {
1155            let new_row = layout_cursor.row + 1;
1156
1157            let new_layout_cursor = {
1158                // keep same X coord
1159                let column = self.rows[new_row].char_at(h_pos);
1160                LayoutCursor {
1161                    row: new_row,
1162                    column,
1163                }
1164            };
1165
1166            (self.cursor_from_layout(new_layout_cursor), Some(h_pos))
1167        } else {
1168            (self.end(), None)
1169        }
1170    }
1171
1172    pub fn cursor_begin_of_row(&self, cursor: &CCursor) -> CCursor {
1173        let layout_cursor = self.layout_from_cursor(*cursor);
1174        self.cursor_from_layout(LayoutCursor {
1175            row: layout_cursor.row,
1176            column: 0,
1177        })
1178    }
1179
1180    pub fn cursor_end_of_row(&self, cursor: &CCursor) -> CCursor {
1181        let layout_cursor = self.layout_from_cursor(*cursor);
1182        self.cursor_from_layout(LayoutCursor {
1183            row: layout_cursor.row,
1184            column: self.rows[layout_cursor.row].char_count_excluding_newline(),
1185        })
1186    }
1187
1188    pub fn cursor_begin_of_paragraph(&self, cursor: &CCursor) -> CCursor {
1189        let mut layout_cursor = self.layout_from_cursor(*cursor);
1190        layout_cursor.column = 0;
1191
1192        loop {
1193            let prev_row = layout_cursor
1194                .row
1195                .checked_sub(1)
1196                .and_then(|row| self.rows.get(row));
1197
1198            let Some(prev_row) = prev_row else {
1199                // This is the first row
1200                break;
1201            };
1202
1203            if prev_row.ends_with_newline {
1204                break;
1205            }
1206
1207            layout_cursor.row -= 1;
1208        }
1209
1210        self.cursor_from_layout(layout_cursor)
1211    }
1212
1213    pub fn cursor_end_of_paragraph(&self, cursor: &CCursor) -> CCursor {
1214        let mut layout_cursor = self.layout_from_cursor(*cursor);
1215        loop {
1216            let row = &self.rows[layout_cursor.row];
1217            if row.ends_with_newline || layout_cursor.row == self.rows.len() - 1 {
1218                layout_cursor.column = row.char_count_excluding_newline();
1219                break;
1220            }
1221
1222            layout_cursor.row += 1;
1223        }
1224
1225        self.cursor_from_layout(layout_cursor)
1226    }
1227}