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};
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: &crate::Fonts) -> 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,
275
276 pub line_height: Option<f32>,
284
285 pub color: Color32,
287
288 pub background: Color32,
289
290 pub expand_bg: f32,
294
295 pub italics: bool,
296
297 pub underline: Stroke,
298
299 pub strikethrough: Stroke,
300
301 pub valign: Align,
311}
312
313impl Default for TextFormat {
314 #[inline]
315 fn default() -> Self {
316 Self {
317 font_id: FontId::default(),
318 extra_letter_spacing: 0.0,
319 line_height: None,
320 color: Color32::GRAY,
321 background: Color32::TRANSPARENT,
322 expand_bg: 1.0,
323 italics: false,
324 underline: Stroke::NONE,
325 strikethrough: Stroke::NONE,
326 valign: Align::BOTTOM,
327 }
328 }
329}
330
331impl std::hash::Hash for TextFormat {
332 #[inline]
333 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
334 let Self {
335 font_id,
336 extra_letter_spacing,
337 line_height,
338 color,
339 background,
340 expand_bg,
341 italics,
342 underline,
343 strikethrough,
344 valign,
345 } = self;
346 font_id.hash(state);
347 emath::OrderedFloat(*extra_letter_spacing).hash(state);
348 if let Some(line_height) = *line_height {
349 emath::OrderedFloat(line_height).hash(state);
350 }
351 color.hash(state);
352 background.hash(state);
353 emath::OrderedFloat(*expand_bg).hash(state);
354 italics.hash(state);
355 underline.hash(state);
356 strikethrough.hash(state);
357 valign.hash(state);
358 }
359}
360
361impl TextFormat {
362 #[inline]
363 pub fn simple(font_id: FontId, color: Color32) -> Self {
364 Self {
365 font_id,
366 color,
367 ..Default::default()
368 }
369 }
370}
371
372#[derive(Clone, Copy, Debug, PartialEq, Eq)]
378#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
379pub enum TextWrapMode {
380 Extend,
382
383 Wrap,
385
386 Truncate,
390}
391
392#[derive(Clone, Debug, PartialEq)]
394#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
395pub struct TextWrapping {
396 pub max_width: f32,
405
406 pub max_rows: usize,
420
421 pub break_anywhere: bool,
430
431 pub overflow_character: Option<char>,
437}
438
439impl std::hash::Hash for TextWrapping {
440 #[inline]
441 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
442 let Self {
443 max_width,
444 max_rows,
445 break_anywhere,
446 overflow_character,
447 } = self;
448 emath::OrderedFloat(*max_width).hash(state);
449 max_rows.hash(state);
450 break_anywhere.hash(state);
451 overflow_character.hash(state);
452 }
453}
454
455impl Default for TextWrapping {
456 fn default() -> Self {
457 Self {
458 max_width: f32::INFINITY,
459 max_rows: usize::MAX,
460 break_anywhere: false,
461 overflow_character: Some('…'),
462 }
463 }
464}
465
466impl TextWrapping {
467 pub fn from_wrap_mode_and_width(mode: TextWrapMode, max_width: f32) -> Self {
469 match mode {
470 TextWrapMode::Extend => Self::no_max_width(),
471 TextWrapMode::Wrap => Self::wrap_at_width(max_width),
472 TextWrapMode::Truncate => Self::truncate_at_width(max_width),
473 }
474 }
475
476 pub fn no_max_width() -> Self {
478 Self {
479 max_width: f32::INFINITY,
480 ..Default::default()
481 }
482 }
483
484 pub fn wrap_at_width(max_width: f32) -> Self {
486 Self {
487 max_width,
488 ..Default::default()
489 }
490 }
491
492 pub fn truncate_at_width(max_width: f32) -> Self {
494 Self {
495 max_width,
496 max_rows: 1,
497 break_anywhere: true,
498 ..Default::default()
499 }
500 }
501}
502
503#[derive(Clone, Debug, PartialEq)]
520#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
521pub struct Galley {
522 pub job: Arc<LayoutJob>,
525
526 pub rows: Vec<PlacedRow>,
534
535 pub elided: bool,
537
538 pub rect: Rect,
547
548 pub mesh_bounds: Rect,
551
552 pub num_vertices: usize,
554
555 pub num_indices: usize,
557
558 pub pixels_per_point: f32,
563
564 pub(crate) intrinsic_size: Vec2,
565}
566
567#[derive(Clone, Debug, PartialEq)]
568#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
569pub struct PlacedRow {
570 pub pos: Pos2,
574
575 pub row: Arc<Row>,
577}
578
579impl PlacedRow {
580 pub fn rect(&self) -> Rect {
584 Rect::from_min_size(self.pos, self.row.size)
585 }
586
587 pub fn rect_without_leading_space(&self) -> Rect {
589 let x = self.glyphs.first().map_or(self.pos.x, |g| g.pos.x);
590 let size_x = self.size.x - x;
591 Rect::from_min_size(Pos2::new(x, self.pos.y), Vec2::new(size_x, self.size.y))
592 }
593}
594
595impl std::ops::Deref for PlacedRow {
596 type Target = Row;
597
598 fn deref(&self) -> &Self::Target {
599 &self.row
600 }
601}
602
603#[derive(Clone, Debug, PartialEq)]
604#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
605pub struct Row {
606 pub(crate) section_index_at_start: u32,
612
613 pub glyphs: Vec<Glyph>,
615
616 pub size: Vec2,
619
620 pub visuals: RowVisuals,
622
623 pub ends_with_newline: bool,
629}
630
631#[derive(Clone, Debug, PartialEq, Eq)]
633#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
634pub struct RowVisuals {
635 pub mesh: Mesh,
638
639 pub mesh_bounds: Rect,
642
643 pub glyph_index_start: usize,
648
649 pub glyph_vertex_range: Range<usize>,
653}
654
655impl Default for RowVisuals {
656 fn default() -> Self {
657 Self {
658 mesh: Default::default(),
659 mesh_bounds: Rect::NOTHING,
660 glyph_index_start: 0,
661 glyph_vertex_range: 0..0,
662 }
663 }
664}
665
666#[derive(Copy, Clone, Debug, PartialEq)]
667#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
668pub struct Glyph {
669 pub chr: char,
671
672 pub pos: Pos2,
675
676 pub advance_width: f32,
678
679 pub line_height: f32,
684
685 pub font_ascent: f32,
687
688 pub font_height: f32,
690
691 pub font_impl_ascent: f32,
693
694 pub font_impl_height: f32,
696
697 pub uv_rect: UvRect,
699
700 pub(crate) section_index: u32,
706}
707
708impl Glyph {
709 #[inline]
710 pub fn size(&self) -> Vec2 {
711 Vec2::new(self.advance_width, self.line_height)
712 }
713
714 #[inline]
715 pub fn max_x(&self) -> f32 {
716 self.pos.x + self.advance_width
717 }
718
719 #[inline]
721 pub fn logical_rect(&self) -> Rect {
722 Rect::from_min_size(self.pos - vec2(0.0, self.font_ascent), self.size())
723 }
724}
725
726impl Row {
729 pub fn text(&self) -> String {
731 self.glyphs.iter().map(|g| g.chr).collect()
732 }
733
734 #[inline]
736 pub fn char_count_excluding_newline(&self) -> usize {
737 self.glyphs.len()
738 }
739
740 #[inline]
742 pub fn char_count_including_newline(&self) -> usize {
743 self.glyphs.len() + (self.ends_with_newline as usize)
744 }
745
746 pub fn char_at(&self, desired_x: f32) -> usize {
749 for (i, glyph) in self.glyphs.iter().enumerate() {
750 if desired_x < glyph.logical_rect().center().x {
751 return i;
752 }
753 }
754 self.char_count_excluding_newline()
755 }
756
757 pub fn x_offset(&self, column: usize) -> f32 {
758 if let Some(glyph) = self.glyphs.get(column) {
759 glyph.pos.x
760 } else {
761 self.size.x
762 }
763 }
764
765 #[inline]
766 pub fn height(&self) -> f32 {
767 self.size.y
768 }
769}
770
771impl PlacedRow {
772 #[inline]
773 pub fn min_y(&self) -> f32 {
774 self.rect().top()
775 }
776
777 #[inline]
778 pub fn max_y(&self) -> f32 {
779 self.rect().bottom()
780 }
781}
782
783impl Galley {
784 #[inline]
785 pub fn is_empty(&self) -> bool {
786 self.job.is_empty()
787 }
788
789 #[inline]
791 pub fn text(&self) -> &str {
792 &self.job.text
793 }
794
795 #[inline]
796 pub fn size(&self) -> Vec2 {
797 self.rect.size()
798 }
799
800 #[inline]
805 pub fn intrinsic_size(&self) -> Vec2 {
806 if self.job.round_output_to_gui {
809 self.intrinsic_size.round_ui()
810 } else {
811 self.intrinsic_size
812 }
813 }
814
815 pub(crate) fn round_output_to_gui(&mut self) {
816 for placed_row in &mut self.rows {
817 let rounded_size = placed_row.row.size.round_ui();
819 if placed_row.row.size != rounded_size {
820 Arc::make_mut(&mut placed_row.row).size = rounded_size;
821 }
822 }
823
824 let rect = &mut self.rect;
825
826 let did_exceed_wrap_width_by_a_lot = rect.width() > self.job.wrap.max_width + 1.0;
827
828 *rect = rect.round_ui();
829
830 if did_exceed_wrap_width_by_a_lot {
831 } else {
834 rect.max.x = rect
836 .max
837 .x
838 .at_most(rect.min.x + self.job.wrap.max_width)
839 .floor_ui();
840 }
841 }
842
843 pub fn concat(job: Arc<LayoutJob>, galleys: &[Arc<Self>], pixels_per_point: f32) -> Self {
845 profiling::function_scope!();
846
847 let mut merged_galley = Self {
848 job,
849 rows: Vec::new(),
850 elided: false,
851 rect: Rect::ZERO,
852 mesh_bounds: Rect::NOTHING,
853 num_vertices: 0,
854 num_indices: 0,
855 pixels_per_point,
856 intrinsic_size: Vec2::ZERO,
857 };
858
859 for (i, galley) in galleys.iter().enumerate() {
860 let current_y_offset = merged_galley.rect.height();
861 let is_last_galley = i + 1 == galleys.len();
862
863 merged_galley
864 .rows
865 .extend(galley.rows.iter().enumerate().map(|(row_idx, placed_row)| {
866 let new_pos = placed_row.pos + current_y_offset * Vec2::Y;
867 let new_pos = new_pos.round_to_pixels(pixels_per_point);
868 merged_galley.mesh_bounds |=
869 placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2());
870 merged_galley.rect |= Rect::from_min_size(new_pos, placed_row.size);
871
872 let mut row = placed_row.row.clone();
873 let is_last_row_in_galley = row_idx + 1 == galley.rows.len();
874 if !is_last_galley && is_last_row_in_galley {
875 Arc::make_mut(&mut row).ends_with_newline = true;
877 }
878 super::PlacedRow { pos: new_pos, row }
879 }));
880
881 merged_galley.num_vertices += galley.num_vertices;
882 merged_galley.num_indices += galley.num_indices;
883 merged_galley.elided |= galley.elided;
886 merged_galley.intrinsic_size.x =
887 f32::max(merged_galley.intrinsic_size.x, galley.intrinsic_size.x);
888 merged_galley.intrinsic_size.y += galley.intrinsic_size.y;
889 }
890
891 if merged_galley.job.round_output_to_gui {
892 merged_galley.round_output_to_gui();
893 }
894
895 merged_galley
896 }
897}
898
899impl AsRef<str> for Galley {
900 #[inline]
901 fn as_ref(&self) -> &str {
902 self.text()
903 }
904}
905
906impl std::borrow::Borrow<str> for Galley {
907 #[inline]
908 fn borrow(&self) -> &str {
909 self.text()
910 }
911}
912
913impl std::ops::Deref for Galley {
914 type Target = str;
915 #[inline]
916 fn deref(&self) -> &str {
917 self.text()
918 }
919}
920
921impl Galley {
925 fn end_pos(&self) -> Rect {
927 if let Some(row) = self.rows.last() {
928 let x = row.rect().right();
929 Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
930 } else {
931 Rect::from_min_max(pos2(0.0, 0.0), pos2(0.0, 0.0))
933 }
934 }
935
936 fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect {
938 let Some(row) = self.rows.get(layout_cursor.row) else {
939 return self.end_pos();
940 };
941
942 let x = row.x_offset(layout_cursor.column);
943 Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
944 }
945
946 pub fn pos_from_cursor(&self, cursor: CCursor) -> Rect {
948 self.pos_from_layout_cursor(&self.layout_from_cursor(cursor))
949 }
950
951 pub fn cursor_from_pos(&self, pos: Vec2) -> CCursor {
959 const VMARGIN: f32 = 5.0;
961
962 if let Some(first_row) = self.rows.first() {
963 if pos.y < first_row.min_y() - VMARGIN {
964 return self.begin();
965 }
966 }
967 if let Some(last_row) = self.rows.last() {
968 if last_row.max_y() + VMARGIN < pos.y {
969 return self.end();
970 }
971 }
972
973 let mut best_y_dist = f32::INFINITY;
974 let mut cursor = CCursor::default();
975
976 let mut ccursor_index = 0;
977
978 for row in &self.rows {
979 let min_y = row.min_y();
980 let max_y = row.max_y();
981
982 let is_pos_within_row = min_y <= pos.y && pos.y <= max_y;
983 let y_dist = (min_y - pos.y).abs().min((max_y - pos.y).abs());
984 if is_pos_within_row || y_dist < best_y_dist {
985 best_y_dist = y_dist;
986 let column = row.char_at(pos.x - row.pos.x);
988 let prefer_next_row = column < row.char_count_excluding_newline();
989 cursor = CCursor {
990 index: ccursor_index + column,
991 prefer_next_row,
992 };
993
994 if is_pos_within_row {
995 return cursor;
996 }
997 }
998 ccursor_index += row.char_count_including_newline();
999 }
1000
1001 cursor
1002 }
1003}
1004
1005impl Galley {
1007 #[inline]
1011 #[expect(clippy::unused_self)]
1012 pub fn begin(&self) -> CCursor {
1013 CCursor::default()
1014 }
1015
1016 pub fn end(&self) -> CCursor {
1018 if self.rows.is_empty() {
1019 return Default::default();
1020 }
1021 let mut ccursor = CCursor {
1022 index: 0,
1023 prefer_next_row: true,
1024 };
1025 for row in &self.rows {
1026 let row_char_count = row.char_count_including_newline();
1027 ccursor.index += row_char_count;
1028 }
1029 ccursor
1030 }
1031}
1032
1033impl Galley {
1035 pub fn layout_from_cursor(&self, cursor: CCursor) -> LayoutCursor {
1037 let prefer_next_row = cursor.prefer_next_row;
1038 let mut ccursor_it = CCursor {
1039 index: 0,
1040 prefer_next_row,
1041 };
1042
1043 for (row_nr, row) in self.rows.iter().enumerate() {
1044 let row_char_count = row.char_count_excluding_newline();
1045
1046 if ccursor_it.index <= cursor.index && cursor.index <= ccursor_it.index + row_char_count
1047 {
1048 let column = cursor.index - ccursor_it.index;
1049
1050 let select_next_row_instead = prefer_next_row
1051 && !row.ends_with_newline
1052 && column >= row.char_count_excluding_newline();
1053 if !select_next_row_instead {
1054 return LayoutCursor {
1055 row: row_nr,
1056 column,
1057 };
1058 }
1059 }
1060 ccursor_it.index += row.char_count_including_newline();
1061 }
1062 debug_assert!(ccursor_it == self.end(), "Cursor out of bounds");
1063
1064 if let Some(last_row) = self.rows.last() {
1065 LayoutCursor {
1066 row: self.rows.len() - 1,
1067 column: last_row.char_count_including_newline(),
1068 }
1069 } else {
1070 Default::default()
1071 }
1072 }
1073
1074 fn cursor_from_layout(&self, layout_cursor: LayoutCursor) -> CCursor {
1075 if layout_cursor.row >= self.rows.len() {
1076 return self.end();
1077 }
1078
1079 let prefer_next_row =
1080 layout_cursor.column < self.rows[layout_cursor.row].char_count_excluding_newline();
1081 let mut cursor_it = CCursor {
1082 index: 0,
1083 prefer_next_row,
1084 };
1085
1086 for (row_nr, row) in self.rows.iter().enumerate() {
1087 if row_nr == layout_cursor.row {
1088 cursor_it.index += layout_cursor
1089 .column
1090 .at_most(row.char_count_excluding_newline());
1091
1092 return cursor_it;
1093 }
1094 cursor_it.index += row.char_count_including_newline();
1095 }
1096 cursor_it
1097 }
1098}
1099
1100impl Galley {
1102 #[expect(clippy::unused_self)]
1103 pub fn cursor_left_one_character(&self, cursor: &CCursor) -> CCursor {
1104 if cursor.index == 0 {
1105 Default::default()
1106 } else {
1107 CCursor {
1108 index: cursor.index - 1,
1109 prefer_next_row: true, }
1111 }
1112 }
1113
1114 pub fn cursor_right_one_character(&self, cursor: &CCursor) -> CCursor {
1115 CCursor {
1116 index: (cursor.index + 1).min(self.end().index),
1117 prefer_next_row: true, }
1119 }
1120
1121 pub fn clamp_cursor(&self, cursor: &CCursor) -> CCursor {
1122 self.cursor_from_layout(self.layout_from_cursor(*cursor))
1123 }
1124
1125 pub fn cursor_up_one_row(
1126 &self,
1127 cursor: &CCursor,
1128 h_pos: Option<f32>,
1129 ) -> (CCursor, Option<f32>) {
1130 let layout_cursor = self.layout_from_cursor(*cursor);
1131 let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
1132 if layout_cursor.row == 0 {
1133 (CCursor::default(), None)
1134 } else {
1135 let new_row = layout_cursor.row - 1;
1136
1137 let new_layout_cursor = {
1138 let column = self.rows[new_row].char_at(h_pos);
1140 LayoutCursor {
1141 row: new_row,
1142 column,
1143 }
1144 };
1145 (self.cursor_from_layout(new_layout_cursor), Some(h_pos))
1146 }
1147 }
1148
1149 pub fn cursor_down_one_row(
1150 &self,
1151 cursor: &CCursor,
1152 h_pos: Option<f32>,
1153 ) -> (CCursor, Option<f32>) {
1154 let layout_cursor = self.layout_from_cursor(*cursor);
1155 let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
1156 if layout_cursor.row + 1 < self.rows.len() {
1157 let new_row = layout_cursor.row + 1;
1158
1159 let new_layout_cursor = {
1160 let column = self.rows[new_row].char_at(h_pos);
1162 LayoutCursor {
1163 row: new_row,
1164 column,
1165 }
1166 };
1167
1168 (self.cursor_from_layout(new_layout_cursor), Some(h_pos))
1169 } else {
1170 (self.end(), None)
1171 }
1172 }
1173
1174 pub fn cursor_begin_of_row(&self, cursor: &CCursor) -> CCursor {
1175 let layout_cursor = self.layout_from_cursor(*cursor);
1176 self.cursor_from_layout(LayoutCursor {
1177 row: layout_cursor.row,
1178 column: 0,
1179 })
1180 }
1181
1182 pub fn cursor_end_of_row(&self, cursor: &CCursor) -> CCursor {
1183 let layout_cursor = self.layout_from_cursor(*cursor);
1184 self.cursor_from_layout(LayoutCursor {
1185 row: layout_cursor.row,
1186 column: self.rows[layout_cursor.row].char_count_excluding_newline(),
1187 })
1188 }
1189
1190 pub fn cursor_begin_of_paragraph(&self, cursor: &CCursor) -> CCursor {
1191 let mut layout_cursor = self.layout_from_cursor(*cursor);
1192 layout_cursor.column = 0;
1193
1194 loop {
1195 let prev_row = layout_cursor
1196 .row
1197 .checked_sub(1)
1198 .and_then(|row| self.rows.get(row));
1199
1200 let Some(prev_row) = prev_row else {
1201 break;
1203 };
1204
1205 if prev_row.ends_with_newline {
1206 break;
1207 }
1208
1209 layout_cursor.row -= 1;
1210 }
1211
1212 self.cursor_from_layout(layout_cursor)
1213 }
1214
1215 pub fn cursor_end_of_paragraph(&self, cursor: &CCursor) -> CCursor {
1216 let mut layout_cursor = self.layout_from_cursor(*cursor);
1217 loop {
1218 let row = &self.rows[layout_cursor.row];
1219 if row.ends_with_newline || layout_cursor.row == self.rows.len() - 1 {
1220 layout_cursor.column = row.char_count_excluding_newline();
1221 break;
1222 }
1223
1224 layout_cursor.row += 1;
1225 }
1226
1227 self.cursor_from_layout(layout_cursor)
1228 }
1229}