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