1#![allow(clippy::derived_hash_with_manual_eq)] #![allow(clippy::wrong_self_convention)] use std::ops::Range;
5use std::sync::Arc;
6
7use super::{
8 cursor::{CCursor, LayoutCursor},
9 font::UvRect,
10};
11use crate::{Color32, FontId, Mesh, Stroke, text::FontsView};
12use emath::{Align, GuiRounding as _, NumExt as _, OrderedFloat, Pos2, Rect, Vec2, pos2, vec2};
13
14#[derive(Clone, Debug, PartialEq)]
48#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
49pub struct LayoutJob {
50 pub text: String,
52
53 pub sections: Vec<LayoutSection>,
55
56 pub wrap: TextWrapping,
58
59 pub first_row_min_height: f32,
65
66 pub break_on_newline: bool,
74
75 pub halign: Align,
77
78 pub justify: bool,
80
81 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 #[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 #[inline]
123 pub fn simple_format(text: String, format: TextFormat) -> Self {
124 Self {
125 sections: vec![LayoutSection {
126 leading_space: 0.0,
127 byte_range: 0..text.len(),
128 format,
129 }],
130 text,
131 break_on_newline: true,
132 ..Default::default()
133 }
134 }
135
136 #[inline]
138 pub fn simple_singleline(text: String, font_id: FontId, color: Color32) -> Self {
139 Self {
140 sections: vec![LayoutSection {
141 leading_space: 0.0,
142 byte_range: 0..text.len(),
143 format: TextFormat::simple(font_id, color),
144 }],
145 text,
146 wrap: Default::default(),
147 break_on_newline: false,
148 ..Default::default()
149 }
150 }
151
152 #[inline]
153 pub fn single_section(text: String, format: TextFormat) -> Self {
154 Self {
155 sections: vec![LayoutSection {
156 leading_space: 0.0,
157 byte_range: 0..text.len(),
158 format,
159 }],
160 text,
161 wrap: Default::default(),
162 break_on_newline: true,
163 ..Default::default()
164 }
165 }
166
167 #[inline]
168 pub fn is_empty(&self) -> bool {
169 self.sections.is_empty()
170 }
171
172 pub fn append(&mut self, text: &str, leading_space: f32, format: TextFormat) {
174 let start = self.text.len();
175 self.text += text;
176 let byte_range = start..self.text.len();
177 self.sections.push(LayoutSection {
178 leading_space,
179 byte_range,
180 format,
181 });
182 }
183
184 pub fn font_height(&self, fonts: &mut FontsView<'_>) -> f32 {
188 let mut max_height = 0.0_f32;
189 for section in &self.sections {
190 max_height = max_height.max(fonts.row_height(§ion.format.font_id));
191 }
192 max_height
193 }
194
195 pub fn effective_wrap_width(&self) -> f32 {
197 if self.round_output_to_gui {
198 self.wrap.max_width + 0.5
202 } else {
203 self.wrap.max_width
204 }
205 }
206}
207
208impl std::hash::Hash for LayoutJob {
209 #[inline]
210 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
211 let Self {
212 text,
213 sections,
214 wrap,
215 first_row_min_height,
216 break_on_newline,
217 halign,
218 justify,
219 round_output_to_gui,
220 } = self;
221
222 text.hash(state);
223 sections.hash(state);
224 wrap.hash(state);
225 emath::OrderedFloat(*first_row_min_height).hash(state);
226 break_on_newline.hash(state);
227 halign.hash(state);
228 justify.hash(state);
229 round_output_to_gui.hash(state);
230 }
231}
232
233#[derive(Clone, Debug, PartialEq)]
236#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
237pub struct LayoutSection {
238 pub leading_space: f32,
240
241 pub byte_range: Range<usize>,
243
244 pub format: TextFormat,
245}
246
247impl std::hash::Hash for LayoutSection {
248 #[inline]
249 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
250 let Self {
251 leading_space,
252 byte_range,
253 format,
254 } = self;
255 OrderedFloat(*leading_space).hash(state);
256 byte_range.hash(state);
257 format.hash(state);
258 }
259}
260
261#[derive(Clone, Debug, PartialEq)]
265#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
266pub struct TextFormat {
267 pub font_id: FontId,
268
269 pub extra_letter_spacing: f32,
273
274 pub line_height: Option<f32>,
282
283 pub color: Color32,
285
286 pub background: Color32,
287
288 pub expand_bg: f32,
292
293 pub italics: bool,
294
295 pub underline: Stroke,
296
297 pub strikethrough: Stroke,
298
299 pub valign: Align,
309}
310
311impl Default for TextFormat {
312 #[inline]
313 fn default() -> Self {
314 Self {
315 font_id: FontId::default(),
316 extra_letter_spacing: 0.0,
317 line_height: None,
318 color: Color32::GRAY,
319 background: Color32::TRANSPARENT,
320 expand_bg: 1.0,
321 italics: false,
322 underline: Stroke::NONE,
323 strikethrough: Stroke::NONE,
324 valign: Align::BOTTOM,
325 }
326 }
327}
328
329impl std::hash::Hash for TextFormat {
330 #[inline]
331 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
332 let Self {
333 font_id,
334 extra_letter_spacing,
335 line_height,
336 color,
337 background,
338 expand_bg,
339 italics,
340 underline,
341 strikethrough,
342 valign,
343 } = self;
344 font_id.hash(state);
345 emath::OrderedFloat(*extra_letter_spacing).hash(state);
346 if let Some(line_height) = *line_height {
347 emath::OrderedFloat(line_height).hash(state);
348 }
349 color.hash(state);
350 background.hash(state);
351 emath::OrderedFloat(*expand_bg).hash(state);
352 italics.hash(state);
353 underline.hash(state);
354 strikethrough.hash(state);
355 valign.hash(state);
356 }
357}
358
359impl TextFormat {
360 #[inline]
361 pub fn simple(font_id: FontId, color: Color32) -> Self {
362 Self {
363 font_id,
364 color,
365 ..Default::default()
366 }
367 }
368}
369
370#[derive(Clone, Copy, Debug, PartialEq, Eq)]
376#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
377pub enum TextWrapMode {
378 Extend,
380
381 Wrap,
383
384 Truncate,
388}
389
390#[derive(Clone, Debug, PartialEq)]
392#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
393pub struct TextWrapping {
394 pub max_width: f32,
403
404 pub max_rows: usize,
418
419 pub break_anywhere: bool,
428
429 pub overflow_character: Option<char>,
435}
436
437impl std::hash::Hash for TextWrapping {
438 #[inline]
439 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
440 let Self {
441 max_width,
442 max_rows,
443 break_anywhere,
444 overflow_character,
445 } = self;
446 emath::OrderedFloat(*max_width).hash(state);
447 max_rows.hash(state);
448 break_anywhere.hash(state);
449 overflow_character.hash(state);
450 }
451}
452
453impl Default for TextWrapping {
454 fn default() -> Self {
455 Self {
456 max_width: f32::INFINITY,
457 max_rows: usize::MAX,
458 break_anywhere: false,
459 overflow_character: Some('…'),
460 }
461 }
462}
463
464impl TextWrapping {
465 pub fn from_wrap_mode_and_width(mode: TextWrapMode, max_width: f32) -> Self {
467 match mode {
468 TextWrapMode::Extend => Self::no_max_width(),
469 TextWrapMode::Wrap => Self::wrap_at_width(max_width),
470 TextWrapMode::Truncate => Self::truncate_at_width(max_width),
471 }
472 }
473
474 pub fn no_max_width() -> Self {
476 Self {
477 max_width: f32::INFINITY,
478 ..Default::default()
479 }
480 }
481
482 pub fn wrap_at_width(max_width: f32) -> Self {
484 Self {
485 max_width,
486 ..Default::default()
487 }
488 }
489
490 pub fn truncate_at_width(max_width: f32) -> Self {
492 Self {
493 max_width,
494 max_rows: 1,
495 break_anywhere: true,
496 ..Default::default()
497 }
498 }
499}
500
501#[derive(Clone, Debug, PartialEq)]
518#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
519pub struct Galley {
520 pub job: Arc<LayoutJob>,
523
524 pub rows: Vec<PlacedRow>,
532
533 pub elided: bool,
535
536 pub rect: Rect,
545
546 pub mesh_bounds: Rect,
549
550 pub num_vertices: usize,
552
553 pub num_indices: usize,
555
556 pub pixels_per_point: f32,
561
562 pub(crate) intrinsic_size: Vec2,
563}
564
565#[derive(Clone, Debug, PartialEq)]
566#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
567pub struct PlacedRow {
568 pub pos: Pos2,
572
573 pub row: Arc<Row>,
575}
576
577impl PlacedRow {
578 pub fn rect(&self) -> Rect {
582 Rect::from_min_size(self.pos, self.row.size)
583 }
584
585 pub fn rect_without_leading_space(&self) -> Rect {
587 let x = self.glyphs.first().map_or(self.pos.x, |g| g.pos.x);
588 let size_x = self.size.x - x;
589 Rect::from_min_size(Pos2::new(x, self.pos.y), Vec2::new(size_x, self.size.y))
590 }
591}
592
593impl std::ops::Deref for PlacedRow {
594 type Target = Row;
595
596 fn deref(&self) -> &Self::Target {
597 &self.row
598 }
599}
600
601#[derive(Clone, Debug, PartialEq)]
602#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
603pub struct Row {
604 pub(crate) section_index_at_start: u32,
610
611 pub glyphs: Vec<Glyph>,
613
614 pub size: Vec2,
617
618 pub visuals: RowVisuals,
620
621 pub ends_with_newline: bool,
627}
628
629#[derive(Clone, Debug, PartialEq, Eq)]
631#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
632pub struct RowVisuals {
633 pub mesh: Mesh,
636
637 pub mesh_bounds: Rect,
640
641 pub glyph_index_start: usize,
646
647 pub glyph_vertex_range: Range<usize>,
651}
652
653impl Default for RowVisuals {
654 fn default() -> Self {
655 Self {
656 mesh: Default::default(),
657 mesh_bounds: Rect::NOTHING,
658 glyph_index_start: 0,
659 glyph_vertex_range: 0..0,
660 }
661 }
662}
663
664#[derive(Copy, Clone, Debug, PartialEq)]
665#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
666pub struct Glyph {
667 pub chr: char,
669
670 pub pos: Pos2,
673
674 pub advance_width: f32,
676
677 pub line_height: f32,
682
683 pub font_ascent: f32,
685
686 pub font_height: f32,
688
689 pub font_impl_ascent: f32,
691
692 pub font_impl_height: f32,
694
695 pub uv_rect: UvRect,
697
698 pub(crate) section_index: u32,
704}
705
706impl Glyph {
707 #[inline]
708 pub fn size(&self) -> Vec2 {
709 Vec2::new(self.advance_width, self.line_height)
710 }
711
712 #[inline]
713 pub fn max_x(&self) -> f32 {
714 self.pos.x + self.advance_width
715 }
716
717 #[inline]
719 pub fn logical_rect(&self) -> Rect {
720 Rect::from_min_size(self.pos - vec2(0.0, self.font_ascent), self.size())
721 }
722}
723
724impl Row {
727 pub fn text(&self) -> String {
729 self.glyphs.iter().map(|g| g.chr).collect()
730 }
731
732 #[inline]
734 pub fn char_count_excluding_newline(&self) -> usize {
735 self.glyphs.len()
736 }
737
738 #[inline]
740 pub fn char_count_including_newline(&self) -> usize {
741 self.glyphs.len() + (self.ends_with_newline as usize)
742 }
743
744 pub fn char_at(&self, desired_x: f32) -> usize {
747 for (i, glyph) in self.glyphs.iter().enumerate() {
748 if desired_x < glyph.logical_rect().center().x {
749 return i;
750 }
751 }
752 self.char_count_excluding_newline()
753 }
754
755 pub fn x_offset(&self, column: usize) -> f32 {
756 if let Some(glyph) = self.glyphs.get(column) {
757 glyph.pos.x
758 } else {
759 self.size.x
760 }
761 }
762
763 #[inline]
764 pub fn height(&self) -> f32 {
765 self.size.y
766 }
767}
768
769impl PlacedRow {
770 #[inline]
771 pub fn min_y(&self) -> f32 {
772 self.rect().top()
773 }
774
775 #[inline]
776 pub fn max_y(&self) -> f32 {
777 self.rect().bottom()
778 }
779}
780
781impl Galley {
782 #[inline]
783 pub fn is_empty(&self) -> bool {
784 self.job.is_empty()
785 }
786
787 #[inline]
789 pub fn text(&self) -> &str {
790 &self.job.text
791 }
792
793 #[inline]
794 pub fn size(&self) -> Vec2 {
795 self.rect.size()
796 }
797
798 #[inline]
803 pub fn intrinsic_size(&self) -> Vec2 {
804 if self.job.round_output_to_gui {
807 self.intrinsic_size.round_ui()
808 } else {
809 self.intrinsic_size
810 }
811 }
812
813 pub(crate) fn round_output_to_gui(&mut self) {
814 for placed_row in &mut self.rows {
815 let rounded_size = placed_row.row.size.round_ui();
817 if placed_row.row.size != rounded_size {
818 Arc::make_mut(&mut placed_row.row).size = rounded_size;
819 }
820 }
821
822 let rect = &mut self.rect;
823
824 let did_exceed_wrap_width_by_a_lot = rect.width() > self.job.wrap.max_width + 1.0;
825
826 *rect = rect.round_ui();
827
828 if did_exceed_wrap_width_by_a_lot {
829 } else {
832 rect.max.x = rect
834 .max
835 .x
836 .at_most(rect.min.x + self.job.wrap.max_width)
837 .floor_ui();
838 }
839 }
840
841 pub fn concat(job: Arc<LayoutJob>, galleys: &[Arc<Self>], pixels_per_point: f32) -> Self {
843 profiling::function_scope!();
844
845 let mut merged_galley = Self {
846 job,
847 rows: Vec::new(),
848 elided: false,
849 rect: Rect::ZERO,
850 mesh_bounds: Rect::NOTHING,
851 num_vertices: 0,
852 num_indices: 0,
853 pixels_per_point,
854 intrinsic_size: Vec2::ZERO,
855 };
856
857 for (i, galley) in galleys.iter().enumerate() {
858 let current_y_offset = merged_galley.rect.height();
859 let is_last_galley = i + 1 == galleys.len();
860
861 merged_galley
862 .rows
863 .extend(galley.rows.iter().enumerate().map(|(row_idx, placed_row)| {
864 let new_pos = placed_row.pos + current_y_offset * Vec2::Y;
865 let new_pos = new_pos.round_to_pixels(pixels_per_point);
866 merged_galley.mesh_bounds |=
867 placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2());
868 merged_galley.rect |= Rect::from_min_size(new_pos, placed_row.size);
869
870 let mut row = placed_row.row.clone();
871 let is_last_row_in_galley = row_idx + 1 == galley.rows.len();
872 if !is_last_galley && is_last_row_in_galley {
873 Arc::make_mut(&mut row).ends_with_newline = true;
875 }
876 super::PlacedRow { pos: new_pos, row }
877 }));
878
879 merged_galley.num_vertices += galley.num_vertices;
880 merged_galley.num_indices += galley.num_indices;
881 merged_galley.elided |= galley.elided;
884 merged_galley.intrinsic_size.x =
885 f32::max(merged_galley.intrinsic_size.x, galley.intrinsic_size.x);
886 merged_galley.intrinsic_size.y += galley.intrinsic_size.y;
887 }
888
889 if merged_galley.job.round_output_to_gui {
890 merged_galley.round_output_to_gui();
891 }
892
893 merged_galley
894 }
895}
896
897impl AsRef<str> for Galley {
898 #[inline]
899 fn as_ref(&self) -> &str {
900 self.text()
901 }
902}
903
904impl std::borrow::Borrow<str> for Galley {
905 #[inline]
906 fn borrow(&self) -> &str {
907 self.text()
908 }
909}
910
911impl std::ops::Deref for Galley {
912 type Target = str;
913 #[inline]
914 fn deref(&self) -> &str {
915 self.text()
916 }
917}
918
919impl Galley {
923 fn end_pos(&self) -> Rect {
925 if let Some(row) = self.rows.last() {
926 let x = row.rect().right();
927 Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
928 } else {
929 Rect::from_min_max(pos2(0.0, 0.0), pos2(0.0, 0.0))
931 }
932 }
933
934 fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect {
936 let Some(row) = self.rows.get(layout_cursor.row) else {
937 return self.end_pos();
938 };
939
940 let x = row.x_offset(layout_cursor.column);
941 Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
942 }
943
944 pub fn pos_from_cursor(&self, cursor: CCursor) -> Rect {
946 self.pos_from_layout_cursor(&self.layout_from_cursor(cursor))
947 }
948
949 pub fn cursor_from_pos(&self, pos: Vec2) -> CCursor {
957 const VMARGIN: f32 = 5.0;
959
960 if let Some(first_row) = self.rows.first()
961 && pos.y < first_row.min_y() - VMARGIN
962 {
963 return self.begin();
964 }
965 if let Some(last_row) = self.rows.last()
966 && last_row.max_y() + VMARGIN < pos.y
967 {
968 return self.end();
969 }
970
971 let mut best_y_dist = f32::INFINITY;
972 let mut cursor = CCursor::default();
973
974 let mut ccursor_index = 0;
975
976 for row in &self.rows {
977 let min_y = row.min_y();
978 let max_y = row.max_y();
979
980 let is_pos_within_row = min_y <= pos.y && pos.y <= max_y;
981 let y_dist = (min_y - pos.y).abs().min((max_y - pos.y).abs());
982 if is_pos_within_row || y_dist < best_y_dist {
983 best_y_dist = y_dist;
984 let column = row.char_at(pos.x - row.pos.x);
986 let prefer_next_row = column < row.char_count_excluding_newline();
987 cursor = CCursor {
988 index: ccursor_index + column,
989 prefer_next_row,
990 };
991
992 if is_pos_within_row {
993 return cursor;
994 }
995 }
996 ccursor_index += row.char_count_including_newline();
997 }
998
999 cursor
1000 }
1001}
1002
1003impl Galley {
1005 #[inline]
1009 #[expect(clippy::unused_self)]
1010 pub fn begin(&self) -> CCursor {
1011 CCursor::default()
1012 }
1013
1014 pub fn end(&self) -> CCursor {
1016 if self.rows.is_empty() {
1017 return Default::default();
1018 }
1019 let mut ccursor = CCursor {
1020 index: 0,
1021 prefer_next_row: true,
1022 };
1023 for row in &self.rows {
1024 let row_char_count = row.char_count_including_newline();
1025 ccursor.index += row_char_count;
1026 }
1027 ccursor
1028 }
1029}
1030
1031impl Galley {
1033 pub fn layout_from_cursor(&self, cursor: CCursor) -> LayoutCursor {
1035 let prefer_next_row = cursor.prefer_next_row;
1036 let mut ccursor_it = CCursor {
1037 index: 0,
1038 prefer_next_row,
1039 };
1040
1041 for (row_nr, row) in self.rows.iter().enumerate() {
1042 let row_char_count = row.char_count_excluding_newline();
1043
1044 if ccursor_it.index <= cursor.index && cursor.index <= ccursor_it.index + row_char_count
1045 {
1046 let column = cursor.index - ccursor_it.index;
1047
1048 let select_next_row_instead = prefer_next_row
1049 && !row.ends_with_newline
1050 && column >= row.char_count_excluding_newline();
1051 if !select_next_row_instead {
1052 return LayoutCursor {
1053 row: row_nr,
1054 column,
1055 };
1056 }
1057 }
1058 ccursor_it.index += row.char_count_including_newline();
1059 }
1060 debug_assert!(ccursor_it == self.end(), "Cursor out of bounds");
1061
1062 if let Some(last_row) = self.rows.last() {
1063 LayoutCursor {
1064 row: self.rows.len() - 1,
1065 column: last_row.char_count_including_newline(),
1066 }
1067 } else {
1068 Default::default()
1069 }
1070 }
1071
1072 fn cursor_from_layout(&self, layout_cursor: LayoutCursor) -> CCursor {
1073 if layout_cursor.row >= self.rows.len() {
1074 return self.end();
1075 }
1076
1077 let prefer_next_row =
1078 layout_cursor.column < self.rows[layout_cursor.row].char_count_excluding_newline();
1079 let mut cursor_it = CCursor {
1080 index: 0,
1081 prefer_next_row,
1082 };
1083
1084 for (row_nr, row) in self.rows.iter().enumerate() {
1085 if row_nr == layout_cursor.row {
1086 cursor_it.index += layout_cursor
1087 .column
1088 .at_most(row.char_count_excluding_newline());
1089
1090 return cursor_it;
1091 }
1092 cursor_it.index += row.char_count_including_newline();
1093 }
1094 cursor_it
1095 }
1096}
1097
1098impl Galley {
1100 #[expect(clippy::unused_self)]
1101 pub fn cursor_left_one_character(&self, cursor: &CCursor) -> CCursor {
1102 if cursor.index == 0 {
1103 Default::default()
1104 } else {
1105 CCursor {
1106 index: cursor.index - 1,
1107 prefer_next_row: true, }
1109 }
1110 }
1111
1112 pub fn cursor_right_one_character(&self, cursor: &CCursor) -> CCursor {
1113 CCursor {
1114 index: (cursor.index + 1).min(self.end().index),
1115 prefer_next_row: true, }
1117 }
1118
1119 pub fn clamp_cursor(&self, cursor: &CCursor) -> CCursor {
1120 self.cursor_from_layout(self.layout_from_cursor(*cursor))
1121 }
1122
1123 pub fn cursor_up_one_row(
1124 &self,
1125 cursor: &CCursor,
1126 h_pos: Option<f32>,
1127 ) -> (CCursor, Option<f32>) {
1128 let layout_cursor = self.layout_from_cursor(*cursor);
1129 let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
1130 if layout_cursor.row == 0 {
1131 (CCursor::default(), None)
1132 } else {
1133 let new_row = layout_cursor.row - 1;
1134
1135 let new_layout_cursor = {
1136 let column = self.rows[new_row].char_at(h_pos);
1138 LayoutCursor {
1139 row: new_row,
1140 column,
1141 }
1142 };
1143 (self.cursor_from_layout(new_layout_cursor), Some(h_pos))
1144 }
1145 }
1146
1147 pub fn cursor_down_one_row(
1148 &self,
1149 cursor: &CCursor,
1150 h_pos: Option<f32>,
1151 ) -> (CCursor, Option<f32>) {
1152 let layout_cursor = self.layout_from_cursor(*cursor);
1153 let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
1154 if layout_cursor.row + 1 < self.rows.len() {
1155 let new_row = layout_cursor.row + 1;
1156
1157 let new_layout_cursor = {
1158 let column = self.rows[new_row].char_at(h_pos);
1160 LayoutCursor {
1161 row: new_row,
1162 column,
1163 }
1164 };
1165
1166 (self.cursor_from_layout(new_layout_cursor), Some(h_pos))
1167 } else {
1168 (self.end(), None)
1169 }
1170 }
1171
1172 pub fn cursor_begin_of_row(&self, cursor: &CCursor) -> CCursor {
1173 let layout_cursor = self.layout_from_cursor(*cursor);
1174 self.cursor_from_layout(LayoutCursor {
1175 row: layout_cursor.row,
1176 column: 0,
1177 })
1178 }
1179
1180 pub fn cursor_end_of_row(&self, cursor: &CCursor) -> CCursor {
1181 let layout_cursor = self.layout_from_cursor(*cursor);
1182 self.cursor_from_layout(LayoutCursor {
1183 row: layout_cursor.row,
1184 column: self.rows[layout_cursor.row].char_count_excluding_newline(),
1185 })
1186 }
1187
1188 pub fn cursor_begin_of_paragraph(&self, cursor: &CCursor) -> CCursor {
1189 let mut layout_cursor = self.layout_from_cursor(*cursor);
1190 layout_cursor.column = 0;
1191
1192 loop {
1193 let prev_row = layout_cursor
1194 .row
1195 .checked_sub(1)
1196 .and_then(|row| self.rows.get(row));
1197
1198 let Some(prev_row) = prev_row else {
1199 break;
1201 };
1202
1203 if prev_row.ends_with_newline {
1204 break;
1205 }
1206
1207 layout_cursor.row -= 1;
1208 }
1209
1210 self.cursor_from_layout(layout_cursor)
1211 }
1212
1213 pub fn cursor_end_of_paragraph(&self, cursor: &CCursor) -> CCursor {
1214 let mut layout_cursor = self.layout_from_cursor(*cursor);
1215 loop {
1216 let row = &self.rows[layout_cursor.row];
1217 if row.ends_with_newline || layout_cursor.row == self.rows.len() - 1 {
1218 layout_cursor.column = row.char_count_excluding_newline();
1219 break;
1220 }
1221
1222 layout_cursor.row += 1;
1223 }
1224
1225 self.cursor_from_layout(layout_cursor)
1226 }
1227}