vizia_core/text/
movement.rs
1use log::warn;
2use skia_safe::textlayout::Paragraph;
3
4use super::{EditableText, Selection};
5
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum Direction {
8 Left,
9 Right,
10 Upstream,
11 Downstream,
12}
13
14impl Direction {
15 pub fn is_upstream_for_direction(self, direction: WritingDirection) -> bool {
20 assert!(
21 !matches!(direction, WritingDirection::Natural),
22 "writing direction must be resolved"
23 );
24 match self {
25 Direction::Upstream => true,
26 Direction::Downstream => false,
27 Direction::Left => matches!(direction, WritingDirection::LeftToRight),
28 Direction::Right => matches!(direction, WritingDirection::RightToLeft),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq)]
34pub enum Movement {
35 Grapheme(Direction),
36 Word(Direction),
37 Line(Direction),
38 Page(Direction),
39 Body(Direction),
40 LineStart,
41 LineEnd,
42 Vertical(VerticalMovement),
43 ParagraphStart,
44 ParagraphEnd,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq)]
48pub enum VerticalMovement {
49 LineUp,
50 LineDown,
51 PageUp,
52 PageDown,
53 DocumentStart,
54 DocumentEnd,
55}
56
57#[derive(Debug, Clone, Copy)]
58pub enum WritingDirection {
59 LeftToRight,
60 RightToLeft,
61 Natural,
62}
63
64pub fn apply_movement<T: EditableText>(
72 m: Movement,
73 s: Selection,
74 text: &T,
75 paragraph: &Paragraph,
76 modify: bool,
77) -> Selection {
78 let writing_direction = WritingDirection::LeftToRight;
85
86 let (offset, h_pos) = match m {
87 Movement::Grapheme(d) if d.is_upstream_for_direction(writing_direction) => {
88 if s.is_caret() || modify {
89 text.prev_grapheme_offset(s.active).map(|off| (off, None)).unwrap_or((0, s.h_pos))
90 } else {
91 (s.min(), None)
92 }
93 }
94 Movement::Grapheme(_) => {
95 if s.is_caret() || modify {
96 text.next_grapheme_offset(s.active)
97 .map(|off| (off, None))
98 .unwrap_or((s.active, s.h_pos))
99 } else {
100 (s.max(), None)
101 }
102 }
103 Movement::Vertical(VerticalMovement::LineUp) => {
104 let cluster = paragraph.get_glyph_cluster_at(s.active).unwrap();
105 let glyph_bounds = cluster.bounds;
106 let line = paragraph.get_line_number_at(s.active).unwrap();
107 let h_pos = s.h_pos.unwrap_or(glyph_bounds.x());
108 if line == 0 {
109 (0, Some(h_pos))
110 } else {
111 let lm = paragraph.get_line_metrics_at(line).unwrap();
112 let up_pos = paragraph
113 .get_closest_glyph_cluster_at((h_pos, glyph_bounds.y() - lm.height as f32))
114 .unwrap();
115 let s = if h_pos < up_pos.bounds.center_x() {
116 up_pos.text_range.start
117 } else {
118 up_pos.text_range.end
119 };
120 (s, Some(h_pos))
122 }
133 }
134 Movement::Vertical(VerticalMovement::LineDown) => {
135 let cluster = paragraph.get_glyph_cluster_at(s.active).unwrap();
136 let h_pos = s.h_pos.unwrap_or(cluster.bounds.x());
137 let line = paragraph.get_line_number_at(s.active).unwrap();
138 if line == paragraph.line_number() - 1 {
139 (text.len(), Some(h_pos))
140 } else {
141 let lm = paragraph.get_line_metrics_at(line).unwrap();
142 let y_below = lm.baseline - lm.ascent + lm.height + 1.0;
144 let down_pos =
145 paragraph.get_closest_glyph_cluster_at((h_pos, y_below as f32)).unwrap();
146 let s = if h_pos < down_pos.bounds.center_x() {
147 down_pos.text_range.start
148 } else {
149 down_pos.text_range.end
150 };
151 (s.min(text.len()), Some(h_pos))
152 }
153 }
154 Movement::Vertical(VerticalMovement::DocumentStart) => (0, None),
155 Movement::Vertical(VerticalMovement::DocumentEnd) => (text.len(), None),
156
157 Movement::ParagraphStart => (text.preceding_line_break(s.active), None),
158 Movement::ParagraphEnd => (text.next_line_break(s.active), None),
159
160 Movement::Line(_) => {
161 todo!()
162 }
163 Movement::Word(d) if d.is_upstream_for_direction(writing_direction) => {
164 let offset = if s.is_caret() || modify {
165 text.prev_word_offset(s.active).unwrap_or(0)
166 } else {
167 s.min()
168 };
169 (offset, None)
170 }
171 Movement::Word(_) => {
172 let offset = if s.is_caret() || modify {
173 text.next_word_offset(s.active).unwrap_or(s.active)
174 } else {
175 s.max()
176 };
177 (offset, None)
178 }
179
180 Movement::Vertical(VerticalMovement::PageDown)
183 | Movement::Vertical(VerticalMovement::PageUp) => (s.active, s.h_pos),
184
185 Movement::LineStart => {
186 let line = paragraph.get_line_number_at(s.active).unwrap();
187 let lm = paragraph.get_line_metrics_at(line).unwrap();
188 (lm.start_index, None)
189 }
190
191 Movement::LineEnd => {
192 let line = paragraph.get_line_number_at(s.active).unwrap();
193 let lm = paragraph.get_line_metrics_at(line).unwrap();
194 (lm.end_index - 1, None)
195 }
196
197 other => {
198 warn!("unhandled movement {:?}", other);
199 (s.anchor, s.h_pos)
200 }
201 };
202
203 let start = if modify { s.anchor } else { offset };
204 Selection::new(start, offset).with_h_pos(h_pos)
205}