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 pub first_vertex: u32,
707}
708
709impl Glyph {
710 #[inline]
711 pub fn size(&self) -> Vec2 {
712 Vec2::new(self.advance_width, self.line_height)
713 }
714
715 #[inline]
716 pub fn max_x(&self) -> f32 {
717 self.pos.x + self.advance_width
718 }
719
720 #[inline]
722 pub fn logical_rect(&self) -> Rect {
723 Rect::from_min_size(self.pos - vec2(0.0, self.font_ascent), self.size())
724 }
725}
726
727impl Row {
730 pub fn text(&self) -> String {
732 self.glyphs.iter().map(|g| g.chr).collect()
733 }
734
735 #[inline]
737 pub fn char_count_excluding_newline(&self) -> usize {
738 self.glyphs.len()
739 }
740
741 #[inline]
743 pub fn char_count_including_newline(&self) -> usize {
744 self.glyphs.len() + (self.ends_with_newline as usize)
745 }
746
747 pub fn char_at(&self, desired_x: f32) -> usize {
750 for (i, glyph) in self.glyphs.iter().enumerate() {
751 if desired_x < glyph.logical_rect().center().x {
752 return i;
753 }
754 }
755 self.char_count_excluding_newline()
756 }
757
758 pub fn x_offset(&self, column: usize) -> f32 {
759 if let Some(glyph) = self.glyphs.get(column) {
760 glyph.pos.x
761 } else {
762 self.size.x
763 }
764 }
765
766 #[inline]
767 pub fn height(&self) -> f32 {
768 self.size.y
769 }
770}
771
772impl PlacedRow {
773 #[inline]
774 pub fn min_y(&self) -> f32 {
775 self.rect().top()
776 }
777
778 #[inline]
779 pub fn max_y(&self) -> f32 {
780 self.rect().bottom()
781 }
782}
783
784impl Galley {
785 #[inline]
786 pub fn is_empty(&self) -> bool {
787 self.job.is_empty()
788 }
789
790 #[inline]
792 pub fn text(&self) -> &str {
793 &self.job.text
794 }
795
796 #[inline]
797 pub fn size(&self) -> Vec2 {
798 self.rect.size()
799 }
800
801 #[inline]
806 pub fn intrinsic_size(&self) -> Vec2 {
807 if self.job.round_output_to_gui {
810 self.intrinsic_size.round_ui()
811 } else {
812 self.intrinsic_size
813 }
814 }
815
816 pub(crate) fn round_output_to_gui(&mut self) {
817 for placed_row in &mut self.rows {
818 let rounded_size = placed_row.row.size.round_ui();
820 if placed_row.row.size != rounded_size {
821 Arc::make_mut(&mut placed_row.row).size = rounded_size;
822 }
823 }
824
825 let rect = &mut self.rect;
826
827 let did_exceed_wrap_width_by_a_lot = rect.width() > self.job.wrap.max_width + 1.0;
828
829 *rect = rect.round_ui();
830
831 if did_exceed_wrap_width_by_a_lot {
832 } else {
835 rect.max.x = rect
837 .max
838 .x
839 .at_most(rect.min.x + self.job.wrap.max_width)
840 .floor_ui();
841 }
842 }
843
844 pub fn concat(job: Arc<LayoutJob>, galleys: &[Arc<Self>], pixels_per_point: f32) -> Self {
846 profiling::function_scope!();
847
848 let mut merged_galley = Self {
849 job,
850 rows: Vec::new(),
851 elided: false,
852 rect: Rect::ZERO,
853 mesh_bounds: Rect::NOTHING,
854 num_vertices: 0,
855 num_indices: 0,
856 pixels_per_point,
857 intrinsic_size: Vec2::ZERO,
858 };
859
860 for (i, galley) in galleys.iter().enumerate() {
861 let current_y_offset = merged_galley.rect.height();
862 let is_last_galley = i + 1 == galleys.len();
863
864 merged_galley
865 .rows
866 .extend(galley.rows.iter().enumerate().map(|(row_idx, placed_row)| {
867 let new_pos = placed_row.pos + current_y_offset * Vec2::Y;
868 let new_pos = new_pos.round_to_pixels(pixels_per_point);
869 merged_galley.mesh_bounds |=
870 placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2());
871 merged_galley.rect |= Rect::from_min_size(new_pos, placed_row.size);
872
873 let mut row = placed_row.row.clone();
874 let is_last_row_in_galley = row_idx + 1 == galley.rows.len();
875 if !is_last_galley && is_last_row_in_galley {
876 Arc::make_mut(&mut row).ends_with_newline = true;
878 }
879 super::PlacedRow { pos: new_pos, row }
880 }));
881
882 merged_galley.num_vertices += galley.num_vertices;
883 merged_galley.num_indices += galley.num_indices;
884 merged_galley.elided |= galley.elided;
887 merged_galley.intrinsic_size.x =
888 f32::max(merged_galley.intrinsic_size.x, galley.intrinsic_size.x);
889 merged_galley.intrinsic_size.y += galley.intrinsic_size.y;
890 }
891
892 if merged_galley.job.round_output_to_gui {
893 merged_galley.round_output_to_gui();
894 }
895
896 merged_galley
897 }
898}
899
900impl AsRef<str> for Galley {
901 #[inline]
902 fn as_ref(&self) -> &str {
903 self.text()
904 }
905}
906
907impl std::borrow::Borrow<str> for Galley {
908 #[inline]
909 fn borrow(&self) -> &str {
910 self.text()
911 }
912}
913
914impl std::ops::Deref for Galley {
915 type Target = str;
916 #[inline]
917 fn deref(&self) -> &str {
918 self.text()
919 }
920}
921
922impl Galley {
926 fn end_pos(&self) -> Rect {
928 if let Some(row) = self.rows.last() {
929 let x = row.rect().right();
930 Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
931 } else {
932 Rect::from_min_max(pos2(0.0, 0.0), pos2(0.0, 0.0))
934 }
935 }
936
937 fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect {
939 let Some(row) = self.rows.get(layout_cursor.row) else {
940 return self.end_pos();
941 };
942
943 let x = row.x_offset(layout_cursor.column);
944 Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
945 }
946
947 pub fn pos_from_cursor(&self, cursor: CCursor) -> Rect {
949 self.pos_from_layout_cursor(&self.layout_from_cursor(cursor))
950 }
951
952 pub fn cursor_from_pos(&self, pos: Vec2) -> CCursor {
960 const VMARGIN: f32 = 5.0;
962
963 if let Some(first_row) = self.rows.first()
964 && pos.y < first_row.min_y() - VMARGIN
965 {
966 return self.begin();
967 }
968 if let Some(last_row) = self.rows.last()
969 && last_row.max_y() + VMARGIN < pos.y
970 {
971 return self.end();
972 }
973
974 let mut best_y_dist = f32::INFINITY;
975 let mut cursor = CCursor::default();
976
977 let mut ccursor_index = 0;
978
979 for row in &self.rows {
980 let min_y = row.min_y();
981 let max_y = row.max_y();
982
983 let is_pos_within_row = min_y <= pos.y && pos.y <= max_y;
984 let y_dist = (min_y - pos.y).abs().min((max_y - pos.y).abs());
985 if is_pos_within_row || y_dist < best_y_dist {
986 best_y_dist = y_dist;
987 let column = row.char_at(pos.x - row.pos.x);
989 let prefer_next_row = column < row.char_count_excluding_newline();
990 cursor = CCursor {
991 index: ccursor_index + column,
992 prefer_next_row,
993 };
994
995 if is_pos_within_row {
996 return cursor;
997 }
998 }
999 ccursor_index += row.char_count_including_newline();
1000 }
1001
1002 cursor
1003 }
1004}
1005
1006impl Galley {
1008 #[inline]
1012 #[expect(clippy::unused_self)]
1013 pub fn begin(&self) -> CCursor {
1014 CCursor::default()
1015 }
1016
1017 pub fn end(&self) -> CCursor {
1019 if self.rows.is_empty() {
1020 return Default::default();
1021 }
1022 let mut ccursor = CCursor {
1023 index: 0,
1024 prefer_next_row: true,
1025 };
1026 for row in &self.rows {
1027 let row_char_count = row.char_count_including_newline();
1028 ccursor.index += row_char_count;
1029 }
1030 ccursor
1031 }
1032}
1033
1034impl Galley {
1036 pub fn layout_from_cursor(&self, cursor: CCursor) -> LayoutCursor {
1038 let prefer_next_row = cursor.prefer_next_row;
1039 let mut ccursor_it = CCursor {
1040 index: 0,
1041 prefer_next_row,
1042 };
1043
1044 for (row_nr, row) in self.rows.iter().enumerate() {
1045 let row_char_count = row.char_count_excluding_newline();
1046
1047 if ccursor_it.index <= cursor.index && cursor.index <= ccursor_it.index + row_char_count
1048 {
1049 let column = cursor.index - ccursor_it.index;
1050
1051 let select_next_row_instead = prefer_next_row
1052 && !row.ends_with_newline
1053 && column >= row.char_count_excluding_newline();
1054 if !select_next_row_instead {
1055 return LayoutCursor {
1056 row: row_nr,
1057 column,
1058 };
1059 }
1060 }
1061 ccursor_it.index += row.char_count_including_newline();
1062 }
1063 debug_assert!(ccursor_it == self.end(), "Cursor out of bounds");
1064
1065 if let Some(last_row) = self.rows.last() {
1066 LayoutCursor {
1067 row: self.rows.len() - 1,
1068 column: last_row.char_count_including_newline(),
1069 }
1070 } else {
1071 Default::default()
1072 }
1073 }
1074
1075 fn cursor_from_layout(&self, layout_cursor: LayoutCursor) -> CCursor {
1076 if layout_cursor.row >= self.rows.len() {
1077 return self.end();
1078 }
1079
1080 let prefer_next_row =
1081 layout_cursor.column < self.rows[layout_cursor.row].char_count_excluding_newline();
1082 let mut cursor_it = CCursor {
1083 index: 0,
1084 prefer_next_row,
1085 };
1086
1087 for (row_nr, row) in self.rows.iter().enumerate() {
1088 if row_nr == layout_cursor.row {
1089 cursor_it.index += layout_cursor
1090 .column
1091 .at_most(row.char_count_excluding_newline());
1092
1093 return cursor_it;
1094 }
1095 cursor_it.index += row.char_count_including_newline();
1096 }
1097 cursor_it
1098 }
1099}
1100
1101impl Galley {
1103 #[expect(clippy::unused_self)]
1104 pub fn cursor_left_one_character(&self, cursor: &CCursor) -> CCursor {
1105 if cursor.index == 0 {
1106 Default::default()
1107 } else {
1108 CCursor {
1109 index: cursor.index - 1,
1110 prefer_next_row: true, }
1112 }
1113 }
1114
1115 pub fn cursor_right_one_character(&self, cursor: &CCursor) -> CCursor {
1116 CCursor {
1117 index: (cursor.index + 1).min(self.end().index),
1118 prefer_next_row: true, }
1120 }
1121
1122 pub fn clamp_cursor(&self, cursor: &CCursor) -> CCursor {
1123 self.cursor_from_layout(self.layout_from_cursor(*cursor))
1124 }
1125
1126 pub fn cursor_up_one_row(
1127 &self,
1128 cursor: &CCursor,
1129 h_pos: Option<f32>,
1130 ) -> (CCursor, Option<f32>) {
1131 let layout_cursor = self.layout_from_cursor(*cursor);
1132 let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
1133 if layout_cursor.row == 0 {
1134 (CCursor::default(), None)
1135 } else {
1136 let new_row = layout_cursor.row - 1;
1137
1138 let new_layout_cursor = {
1139 let column = self.rows[new_row].char_at(h_pos);
1141 LayoutCursor {
1142 row: new_row,
1143 column,
1144 }
1145 };
1146 (self.cursor_from_layout(new_layout_cursor), Some(h_pos))
1147 }
1148 }
1149
1150 pub fn cursor_down_one_row(
1151 &self,
1152 cursor: &CCursor,
1153 h_pos: Option<f32>,
1154 ) -> (CCursor, Option<f32>) {
1155 let layout_cursor = self.layout_from_cursor(*cursor);
1156 let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
1157 if layout_cursor.row + 1 < self.rows.len() {
1158 let new_row = layout_cursor.row + 1;
1159
1160 let new_layout_cursor = {
1161 let column = self.rows[new_row].char_at(h_pos);
1163 LayoutCursor {
1164 row: new_row,
1165 column,
1166 }
1167 };
1168
1169 (self.cursor_from_layout(new_layout_cursor), Some(h_pos))
1170 } else {
1171 (self.end(), None)
1172 }
1173 }
1174
1175 pub fn cursor_begin_of_row(&self, cursor: &CCursor) -> CCursor {
1176 let layout_cursor = self.layout_from_cursor(*cursor);
1177 self.cursor_from_layout(LayoutCursor {
1178 row: layout_cursor.row,
1179 column: 0,
1180 })
1181 }
1182
1183 pub fn cursor_end_of_row(&self, cursor: &CCursor) -> CCursor {
1184 let layout_cursor = self.layout_from_cursor(*cursor);
1185 self.cursor_from_layout(LayoutCursor {
1186 row: layout_cursor.row,
1187 column: self.rows[layout_cursor.row].char_count_excluding_newline(),
1188 })
1189 }
1190
1191 pub fn cursor_begin_of_paragraph(&self, cursor: &CCursor) -> CCursor {
1192 let mut layout_cursor = self.layout_from_cursor(*cursor);
1193 layout_cursor.column = 0;
1194
1195 loop {
1196 let prev_row = layout_cursor
1197 .row
1198 .checked_sub(1)
1199 .and_then(|row| self.rows.get(row));
1200
1201 let Some(prev_row) = prev_row else {
1202 break;
1204 };
1205
1206 if prev_row.ends_with_newline {
1207 break;
1208 }
1209
1210 layout_cursor.row -= 1;
1211 }
1212
1213 self.cursor_from_layout(layout_cursor)
1214 }
1215
1216 pub fn cursor_end_of_paragraph(&self, cursor: &CCursor) -> CCursor {
1217 let mut layout_cursor = self.layout_from_cursor(*cursor);
1218 loop {
1219 let row = &self.rows[layout_cursor.row];
1220 if row.ends_with_newline || layout_cursor.row == self.rows.len() - 1 {
1221 layout_cursor.column = row.char_count_excluding_newline();
1222 break;
1223 }
1224
1225 layout_cursor.row += 1;
1226 }
1227
1228 self.cursor_from_layout(layout_cursor)
1229 }
1230}