limn_text_layout/cursor.rs
1/// Logic related to the positioning of the cursor within text.
2
3use std;
4use types::{Range, Align, Rect, RectExt, Point};
5use rusttype;
6use rusttype::LayoutIter;
7
8use super::line::{LineRects, LineInfo};
9use super::Font;
10
11/// Every possible cursor position within each line of text yielded by the given iterator.
12///
13/// Yields `(xs, y_range)`, where `y_range` is the `Range` occupied by the line across the *y*
14/// axis and `xs` is every possible cursor position along the *x* axis
15#[derive(Clone)]
16pub struct XysPerLine<'a, I> {
17 lines_with_rects: I,
18 font: &'a Font,
19 text: &'a str,
20 font_size: f32,
21}
22
23/// Similarly to `XysPerLine`, yields every possible cursor position within each line of text
24/// yielded by the given iterator.
25///
26/// Rather than taking an iterator type yielding lines and positioning data, this method
27/// constructs its own iterator to do so internally, saving some boilerplate involved in common
28/// `XysPerLine` use cases.
29///
30/// Yields `(xs, y_range)`, where `y_range` is the `Range` occupied by the line across the *y*
31/// axis and `xs` is every possible cursor position along the *x* axis.
32#[derive(Clone)]
33pub struct XysPerLineFromText<'a> {
34 xys_per_line: XysPerLine<'a,
35 std::iter::Zip<std::iter::Cloned<std::slice::Iter<'a, LineInfo>>,
36 LineRects<std::iter::Cloned<std::slice::Iter<'a, LineInfo>>>>
37 >,
38 }
39
40/// Each possible cursor position along the *x* axis within a line of text.
41///
42/// `Xs` iterators are produced by the `XysPerLine` iterator.
43pub struct Xs<'a, 'b> {
44 next_x: Option<f32>,
45 layout: LayoutIter<'a, 'b>,
46}
47
48/// An index representing the position of a cursor within some text.
49#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
50pub struct Index {
51 /// The index of the line upon which the cursor is situated.
52 pub line: usize,
53 /// The index within all possible cursor positions for the line.
54 ///
55 /// For example, for the line `foo`, a `char` of `1` would indicate the cursor's position
56 /// as `f|oo` where `|` is the cursor.
57 pub char: usize,
58}
59
60
61impl Index {
62 /// The cursor index of the beginning of the word (block of non-whitespace) before `self`.
63 ///
64 /// If `self` is at the beginning of the line, call previous, which returns the last
65 /// index position of the previous line, or None if it's the first line
66 ///
67 /// If `self` points to whitespace, skip past that whitespace, then return the index of
68 /// the start of the word that precedes the whitespace
69 ///
70 /// If `self` is in the middle or end of a word, return the index of the start of that word
71 pub fn previous_word_start<I>(self, text: &str, mut line_infos: I) -> Option<Self>
72 where I: Iterator<Item = LineInfo>
73 {
74 let Index { line, char } = self;
75 if char > 0 {
76 line_infos.nth(line).and_then(|line_info| {
77 let line_count = line_info.char_range().count();
78 let mut chars_rev = (&text[line_info.byte_range()]).chars().rev();
79 if char != line_count {
80 chars_rev.nth(line_count - char - 1);
81 }
82 let mut new_char = 0;
83 let mut hit_non_whitespace = false;
84 for (i, char_) in chars_rev.enumerate() {
85 // loop until word starts, then continue until the word ends
86 if !char_.is_whitespace() {
87 hit_non_whitespace = true;
88 }
89 if char_.is_whitespace() && hit_non_whitespace {
90 new_char = char - i;
91 break;
92 }
93 }
94 Some(Index {
95 line: line,
96 char: new_char,
97 })
98 })
99 } else {
100 self.previous(line_infos)
101 }
102 }
103
104 /// The cursor index of the end of the first word (block of non-whitespace) after `self`.
105 ///
106 /// If `self` is at the end of the text, this returns `None`.
107 ///
108 /// If `self` is at the end of a line other than the last, this returns the first index of
109 /// the next line.
110 ///
111 /// If `self` points to whitespace, skip past that whitespace, then return the index of
112 /// the end of the word after the whitespace
113 ///
114 /// If `self` is in the middle or start of a word, return the index of the end of that word
115 pub fn next_word_end<I>(self, text: &str, mut line_infos: I) -> Option<Self>
116 where I: Iterator<Item = LineInfo>
117 {
118 let Index { line, char } = self;
119 line_infos.nth(line)
120 .and_then(|line_info| {
121 let line_count = line_info.char_range().count();
122 if char < line_count {
123 let mut chars = (&text[line_info.byte_range()]).chars();
124 let mut new_char = line_count;
125 let mut hit_non_whitespace = false;
126 if char != 0 {
127 chars.nth(char - 1);
128 }
129 for (i, char_) in chars.enumerate() {
130 // loop until word starts, then continue until the word ends
131 if !char_.is_whitespace() {
132 hit_non_whitespace = true;
133 }
134 if char_.is_whitespace() && hit_non_whitespace {
135 new_char = char + i;
136 break;
137 }
138 }
139 Some(Index {
140 line: line,
141 char: new_char,
142 })
143 } else {
144 line_infos.next().map(|_| {
145 Index {
146 line: line + 1,
147 char: 0,
148 }
149 })
150 }
151 })
152 }
153
154 /// The cursor index that comes before `self`.
155 ///
156 /// If `self` is at the beginning of the text, this returns `None`.
157 ///
158 /// If `self` is at the beginning of a line other than the first, this returns the last
159 /// index position of the previous line.
160 ///
161 /// If `self` is a position other than the start of a line, it will return the position
162 /// that is immediately to the left.
163 pub fn previous<I>(self, mut line_infos: I) -> Option<Self>
164 where I: Iterator<Item = LineInfo>
165 {
166 let Index { line, char } = self;
167 if char > 0 {
168 let new_char = char - 1;
169 line_infos.nth(line)
170 .and_then(|info| if new_char <= info.char_range().count() {
171 Some(Index {
172 line: line,
173 char: new_char,
174 })
175 } else {
176 None
177 })
178 } else if line > 0 {
179 let new_line = line - 1;
180 line_infos.nth(new_line)
181 .map(|info| {
182 let new_char = info.end_char() - info.start_char;
183 Index {
184 line: new_line,
185 char: new_char,
186 }
187 })
188 } else {
189 None
190 }
191 }
192
193 /// The cursor index that follows `self`.
194 ///
195 /// If `self` is at the end of the text, this returns `None`.
196 ///
197 /// If `self` is at the end of a line other than the last, this returns the first index of
198 /// the next line.
199 ///
200 /// If `self` is a position other than the end of a line, it will return the position that
201 /// is immediately to the right.
202 pub fn next<I>(self, mut line_infos: I) -> Option<Self>
203 where I: Iterator<Item = LineInfo>
204 {
205 let Index { line, char } = self;
206 line_infos.nth(line)
207 .and_then(|info| if char >= info.char_range().count() {
208 line_infos.next().map(|_| {
209 Index {
210 line: line + 1,
211 char: 0,
212 }
213 })
214 } else {
215 Some(Index {
216 line: line,
217 char: char + 1,
218 })
219 })
220 }
221
222 /// Clamps `self` to the given lines.
223 ///
224 /// If `self` would lie after the end of the last line, return the index at the end of the
225 /// last line.
226 ///
227 /// If `line_infos` is empty, returns cursor at line=0 char=0.
228 pub fn clamp_to_lines<I>(self, line_infos: I) -> Self
229 where I: Iterator<Item = LineInfo>
230 {
231 let mut last = None;
232 for (i, info) in line_infos.enumerate() {
233 if i == self.line {
234 let num_chars = info.char_range().len();
235 let char = std::cmp::min(self.char, num_chars);
236 return Index {
237 line: i,
238 char: char,
239 };
240 }
241 last = Some((i, info));
242 }
243 match last {
244 Some((i, info)) => {
245 Index {
246 line: i,
247 char: info.char_range().len(),
248 }
249 }
250 None => Index { line: 0, char: 0 },
251 }
252 }
253}
254
255
256/// Every possible cursor position within each line of text yielded by the given iterator.
257///
258/// Yields `(xs, y_range)`, where `y_range` is the `Range` occupied by the line across the *y*
259/// axis and `xs` is every possible cursor position along the *x* axis
260pub fn xys_per_line<'a, I>(lines_with_rects: I,
261 font: &'a Font,
262 text: &'a str,
263 font_size: f32)
264 -> XysPerLine<'a, I> {
265 XysPerLine {
266 lines_with_rects: lines_with_rects,
267 font: font,
268 text: text,
269 font_size: font_size,
270 }
271}
272
273/// Similarly to `xys_per_line`, this produces an iterator yielding every possible cursor
274/// position within each line of text yielded by the given iterator.
275///
276/// Rather than taking an iterator yielding lines and their positioning data, this method
277/// constructs its own iterator to do so internally, saving some boilerplate involved in common
278/// `xys_per_line` use cases.
279///
280/// Yields `(xs, y_range)`, where `y_range` is the `Range` occupied by the line across the *y*
281/// axis and `xs` is every possible cursor position along the *x* axis.
282pub fn xys_per_line_from_text<'a>(text: &'a str,
283 line_infos: &'a [LineInfo],
284 font: &'a Font,
285 font_size: f32,
286 align: Align,
287 line_spacing: f32,
288 rect: Rect)
289 -> XysPerLineFromText<'a> {
290 let line_infos = line_infos.iter().cloned();
291 let line_rects = LineRects::new(line_infos.clone(),
292 font_size,
293 rect,
294 align,
295 line_spacing);
296 let lines = line_infos.clone();
297 let lines_with_rects = lines.zip(line_rects.clone());
298 XysPerLineFromText {
299 xys_per_line: xys_per_line(lines_with_rects, font, text, font_size),
300 }
301}
302
303/// Convert the given character index into a cursor `Index`.
304pub fn index_before_char<I>(line_infos: I, char_index: usize) -> Option<Index>
305 where I: Iterator<Item = LineInfo>
306{
307 for (i, line_info) in line_infos.enumerate() {
308 let start_char = line_info.start_char;
309 let end_char = line_info.end_char();
310 if start_char <= char_index && char_index <= end_char {
311 return Some(Index {
312 line: i,
313 char: char_index - start_char,
314 });
315 }
316 }
317 None
318}
319
320/// Determine the *xy* location of the cursor at the given cursor `Index`.
321pub fn xy_at<'a, I>(xys_per_line: I, idx: Index) -> Option<(f32, Range)>
322 where I: Iterator<Item = (Xs<'a, 'a>, Range)>
323{
324 for (i, (xs, y)) in xys_per_line.enumerate() {
325 if i == idx.line {
326 for (j, x) in xs.enumerate() {
327 if j == idx.char {
328 return Some((x, y));
329 }
330 }
331 }
332 }
333 None
334}
335
336/// Find the closest line for the given `y` position, and
337/// return the line index, Xs iterator, and y-range of that line
338///
339/// Returns `None` if there are no lines
340pub fn closest_line<'a, I>(y_pos: f32, xys_per_line: I) -> Option<(usize, Xs<'a, 'a>, Range)>
341 where I: Iterator<Item = (Xs<'a, 'a>, Range)>
342{
343 let mut xys_per_line_enumerated = xys_per_line.enumerate();
344 xys_per_line_enumerated.next().and_then(|(first_line_idx, (first_line_xs, first_line_y))| {
345 let mut closest_line = (first_line_idx, first_line_xs, first_line_y);
346 let mut closest_diff = (y_pos - first_line_y.middle()).abs();
347 for (line_idx, (line_xs, line_y)) in xys_per_line_enumerated {
348 if line_y.is_over(y_pos) {
349 closest_line = (line_idx, line_xs, line_y);
350 break;
351 } else {
352 let diff = (y_pos - line_y.middle()).abs();
353 if diff < closest_diff {
354 closest_line = (line_idx, line_xs, line_y);
355 closest_diff = diff;
356 } else {
357 break;
358 }
359 }
360 }
361 Some(closest_line)
362 })
363}
364
365/// Find the closest cursor index to the given `xy` position, and the center `Point` of that
366/// cursor.
367///
368/// Returns `None` if the given `text` is empty.
369pub fn closest_cursor_index_and_xy<'a, I>(point: Point, xys_per_line: I) -> Option<(Index, Point)>
370 where I: Iterator<Item = (Xs<'a, 'a>, Range)>
371{
372 closest_line(point.x, xys_per_line)
373 .and_then(|(closest_line_idx, closest_line_xs, closest_line_y)| {
374 let (closest_char_idx, closest_x) = closest_cursor_index_on_line(point.y,
375 closest_line_xs);
376 let index = Index {
377 line: closest_line_idx,
378 char: closest_char_idx,
379 };
380 let point = Point::new(closest_x, closest_line_y.middle());
381 Some((index, point))
382 })
383}
384
385/// Find the closest cursor index to the given `x` position on the given line along with the
386/// `x` position of that cursor.
387pub fn closest_cursor_index_on_line<'a>(x_pos: f32, line_xs: Xs<'a, 'a>) -> (usize, f32) {
388 let mut xs_enumerated = line_xs.enumerate();
389 // `xs` always yields at least one `x` (the start of the line).
390 let (first_idx, first_x) = xs_enumerated.next().unwrap();
391 let first_diff = (x_pos - first_x).abs();
392 let mut closest = (first_idx, first_x);
393 let mut closest_diff = first_diff;
394 for (i, x) in xs_enumerated {
395 let diff = (x_pos - x).abs();
396 if diff < closest_diff {
397 closest = (i, x);
398 closest_diff = diff;
399 } else {
400 break;
401 }
402 }
403 closest
404}
405
406
407impl<'a, I> Iterator for XysPerLine<'a, I>
408 where I: Iterator<Item = (LineInfo, Rect)>
409{
410 // The `Range` occupied by the line across the *y* axis, along with an iterator yielding
411 // each possible cursor position along the *x* axis.
412 type Item = (Xs<'a, 'a>, Range);
413 fn next(&mut self) -> Option<Self::Item> {
414 let XysPerLine { ref mut lines_with_rects, font, text, font_size } = *self;
415 let scale = super::pt_to_scale(font_size);
416 lines_with_rects.next().map(|(line_info, line_rect)| {
417 let line = &text[line_info.byte_range()];
418 let (x, y) = (line_rect.left(), line_rect.top());
419 let point = rusttype::Point { x: x, y: y };
420 let y = line_rect.y_range();
421 let layout = font.layout(line, scale, point);
422 let xs = Xs {
423 next_x: Some(line_rect.left()),
424 layout: layout,
425 };
426 (xs, y)
427 })
428 }
429}
430
431impl<'a> Iterator for XysPerLineFromText<'a> {
432 type Item = (Xs<'a, 'a>, Range);
433 fn next(&mut self) -> Option<Self::Item> {
434 self.xys_per_line.next()
435 }
436}
437
438impl<'a, 'b> Iterator for Xs<'a, 'b> {
439 // Each possible cursor position along the *x* axis.
440 type Item = f32;
441 fn next(&mut self) -> Option<Self::Item> {
442 self.next_x.map(|x| {
443 self.next_x = self.layout
444 .next()
445 .map(|g| {
446 g.pixel_bounding_box()
447 .map(|r| r.max.x as f32)
448 .unwrap_or_else(|| x + g.unpositioned().h_metrics().advance_width)
449 });
450 x
451 })
452 }
453}