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