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    /// Returns `true` if this direction is byte-wise backwards for
16    /// the provided [`WritingDirection`].
17    ///
18    /// The provided direction *must not be* `WritingDirection::Natural`.
19    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
64/// Compute the result of a [`Movement`] on a [`Selection`].
65///
66/// returns a new selection representing the state after the movement.
67///
68/// If `modify` is true, only the 'active' edge (the `end`) of the selection
69/// should be changed; this is the case when the user moves with the shift
70/// key pressed.
71pub 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 = if crate::piet::util::first_strong_rtl(text.as_str()) {
79    //     WritingDirection::RightToLeft
80    // } else {
81    //     WritingDirection::LeftToRight
82    // };
83
84    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                // if up_pos.is_inside {
121                (s, Some(h_pos))
122                // } else {
123                //     // because we can't specify affinity, moving up when h_pos
124                //     // is wider than both the current line and the previous line
125                //     // can result in a cursor position at the visual start of the
126                //     // current line; so we handle this as a special-case.
127                //     let lm_prev =
128                //         paragraph.get_line_metrics_at(line.saturating_sub(1)).unwrap();
129                //     let up_pos = lm_prev.end_excluding_whitespaces;
130                //     (up_pos, Some(h_pos))
131                // }
132            }
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                // may not work correctly for point sizes below 1.0
143                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        // These two are not handled; they require knowledge of the size
181        // of the viewport.
182        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}