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