druid_shell/text.rs
1// Copyright 2020 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Types and functions for cross-platform text input.
16//!
17//! Text input is a notoriously difficult problem. Unlike many other aspects of
18//! user interfaces, text input can not be correctly modeled using discrete
19//! events passed from the platform to the application. For example, many mobile
20//! phones implement autocorrect: when the user presses the spacebar, the
21//! platform peeks at the word directly behind the caret, and potentially
22//! replaces it if it's misspelled. This means the platform needs to know the
23//! contents of a text field. On other devices, the platform may need to draw an
24//! emoji window under the caret, or look up the on-screen locations of letters
25//! for crossing out with a stylus, both of which require fetching on-screen
26//! coordinates from the application.
27//!
28//! This is all to say: text editing is a bidirectional conversation between the
29//! application and the platform. The application, when the platform asks for
30//! it, serves up text field coordinates and content. The platform looks at
31//! this information and combines it with input from keyboards (physical or
32//! onscreen), voice, styluses, the user's language settings, and then sends
33//! edit commands to the application.
34//!
35//! Many platforms have an additional complication: this input fusion often
36//! happens in a different process from your application. If we don't
37//! specifically account for this fact, we might get race conditions! In the
38//! autocorrect example, if I sloppily type "meoow" and press space, the
39//! platform might issue edits to "delete backwards one word and insert meow".
40//! However, if I concurrently click somewhere else in the document to move the
41//! caret, this will replace some *other* word with "meow", and leave the
42//! "meoow" disappointingly present. To mitigate this problem, we use locks,
43//! represented by the `InputHandler` trait.
44//!
45//! ## Lifecycle of a Text Input
46//!
47//! 1. The user clicks a link or switches tabs, and the window content now
48//! contains a new text field. The application registers this new field by
49//! calling `WindowHandle::add_text_field`, and gets a `TextFieldToken` that
50//! represents this new field.
51//! 2. The user clicks on that text field, focusing it. The application lets the
52//! platform know by calling `WindowHandle::set_focused_text_field` with that
53//! field's `TextFieldToken`.
54//! 3. The user presses a key on the keyboard. The platform first calls
55//! `WinHandler::key_down`. If this method returns `true`, the application
56//! has indicated the keypress was captured, and we skip the remaining steps.
57//! 4. If `key_down` returned `false`, `druid-shell` forwards the key event to the
58//! platform's text input system
59//! 5. The platform, in response to either this key event or some other user
60//! action, determines it's time for some text input. It calls
61//! `WinHandler::text_input` to acquire a lock on the text field's state.
62//! The application returns an `InputHandler` object corresponding to the
63//! requested text field. To prevent race conditions, your application may
64//! not make any changes
65//! to the text field's state until the platform drops the `InputHandler`.
66//! 6. The platform calls various `InputHandler` methods to inspect and edit the
67//! text field's state. Later, usually within a few milliseconds, the
68//! platform drops the `InputHandler`, allowing the application to once again
69//! make changes to the text field's state. These commands might be "insert
70//! `q`" for a smartphone user tapping on their virtual keyboard, or
71//! "move the caret one word left" for a user pressing the left arrow key
72//! while holding control.
73//! 7. Eventually, after many keypresses cause steps 3–6 to repeat, the user
74//! unfocuses the text field. The application indicates this to the platform
75//! by calling `set_focused_text_field`. Note that even though focus has
76//! shifted away from our text field, the platform may still send edits to it
77//! by calling `WinHandler::text_input`.
78//! 8. At some point, the user clicks a link or switches a tab, and the text
79//! field is no longer present in the window. The application calls
80//! `WindowHandle::remove_text_field`, and the platform may no longer call
81//! `WinHandler::text_input` to make changes to it.
82//!
83//! The application also has a series of steps it follows if it wants to make
84//! its own changes to the text field's state:
85//!
86//! 1. The application determines it would like to make a change to the text
87//! field; perhaps the user has scrolled and and the text field has changed
88//! its visible location on screen, or perhaps the user has clicked to move
89//! the caret to a new location.
90//! 2. The application first checks to see if there's an outstanding
91//! `InputHandler` lock for this text field; if so, it waits until the last
92//! `InputHandler` is dropped before continuing.
93//! 3. The application then makes the change to the text input. If the change
94//! would affect state visible from an `InputHandler`, the application must
95//! notify the platform via `WinHandler::update_text_field`.
96//!
97//! ## Supported Platforms
98//!
99//! Currently, `druid-shell` text input is fully implemented on macOS. Our goal
100//! is to have full support for all `druid-shell` targets, but for now,
101//! `InputHandler` calls are simulated from keypresses on other platforms, which
102//! doesn't allow for IME input, dead keys, etc.
103
104use crate::keyboard::{KbKey, KeyEvent};
105use crate::kurbo::{Point, Rect};
106use crate::piet::HitTestPoint;
107use crate::window::{TextFieldToken, WinHandler};
108use std::borrow::Cow;
109use std::ops::Range;
110
111/// An event representing an application-initiated change in [`InputHandler`]
112/// state.
113///
114/// When we change state that may have previously been retrieved from an
115/// [`InputHandler`], we notify the platform so that it can invalidate any
116/// data if necessary.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118#[non_exhaustive]
119pub enum Event {
120 /// Indicates the value returned by `InputHandler::selection` may have changed.
121 SelectionChanged,
122
123 /// Indicates the values returned by one or more of these methods may have changed:
124 /// - `InputHandler::hit_test_point`
125 /// - `InputHandler::line_range`
126 /// - `InputHandler::bounding_box`
127 /// - `InputHandler::slice_bounding_box`
128 LayoutChanged,
129
130 /// Indicates any value returned from any `InputHandler` method may have changed.
131 Reset,
132}
133
134/// A range of selected text, or a caret.
135///
136/// A caret is the blinking vertical bar where text is to be inserted. We
137/// represent it as a selection with zero length, where `anchor == active`.
138/// Indices are always expressed in UTF-8 bytes, and must be between 0 and the
139/// document length, inclusive.
140///
141/// As an example, if the input caret is at the start of the document `hello
142/// world`, we would expect both `anchor` and `active` to be `0`. If the user
143/// holds shift and presses the right arrow key five times, we would expect the
144/// word `hello` to be selected, the `anchor` to still be `0`, and the `active`
145/// to now be `5`.
146#[derive(Clone, Copy, Debug, Default, PartialEq)]
147#[non_exhaustive]
148pub struct Selection {
149 /// The 'anchor' end of the selection.
150 ///
151 /// This is the end of the selection that stays unchanged while holding
152 /// shift and pressing the arrow keys.
153 pub anchor: usize,
154 /// The 'active' end of the selection.
155 ///
156 /// This is the end of the selection that moves while holding shift and
157 /// pressing the arrow keys.
158 pub active: usize,
159 /// The saved horizontal position, during vertical movement.
160 ///
161 /// This should not be set by the IME; it will be tracked and handled by
162 /// the text field.
163 pub h_pos: Option<f64>,
164}
165
166#[allow(clippy::len_without_is_empty)]
167impl Selection {
168 /// Create a new `Selection` with the provided `anchor` and `active` positions.
169 ///
170 /// Both positions refer to UTF-8 byte indices in some text.
171 ///
172 /// If your selection is a caret, you can use [`Selection::caret`] instead.
173 pub fn new(anchor: usize, active: usize) -> Selection {
174 Selection {
175 anchor,
176 active,
177 h_pos: None,
178 }
179 }
180
181 /// Create a new caret (zero-length selection) at the provided UTF-8 byte index.
182 ///
183 /// `index` must be a grapheme cluster boundary.
184 pub fn caret(index: usize) -> Selection {
185 Selection {
186 anchor: index,
187 active: index,
188 h_pos: None,
189 }
190 }
191
192 /// Construct a new selection from this selection, with the provided h_pos.
193 ///
194 /// # Note
195 ///
196 /// `h_pos` is used to track the *pixel* location of the cursor when moving
197 /// vertically; lines may have available cursor positions at different
198 /// positions, and arrowing down and then back up should always result
199 /// in a cursor at the original starting location; doing this correctly
200 /// requires tracking this state.
201 ///
202 /// You *probably* don't need to use this, unless you are implementing a new
203 /// text field, or otherwise implementing vertical cursor motion, in which
204 /// case you will want to set this during vertical motion if it is not
205 /// already set.
206 pub fn with_h_pos(mut self, h_pos: Option<f64>) -> Self {
207 self.h_pos = h_pos;
208 self
209 }
210
211 /// Create a new selection that is guaranteed to be valid for the provided
212 /// text.
213 #[must_use = "constrained constructs a new Selection"]
214 pub fn constrained(mut self, s: &str) -> Self {
215 let s_len = s.len();
216 self.anchor = self.anchor.min(s_len);
217 self.active = self.active.min(s_len);
218 while !s.is_char_boundary(self.anchor) {
219 self.anchor += 1;
220 }
221 while !s.is_char_boundary(self.active) {
222 self.active += 1;
223 }
224 self
225 }
226
227 /// Return the position of the upstream end of the selection.
228 ///
229 /// This is end with the lesser byte index.
230 ///
231 /// Because of bidirectional text, this is not necessarily "left".
232 pub fn min(&self) -> usize {
233 usize::min(self.anchor, self.active)
234 }
235
236 /// Return the position of the downstream end of the selection.
237 ///
238 /// This is the end with the greater byte index.
239 ///
240 /// Because of bidirectional text, this is not necessarily "right".
241 pub fn max(&self) -> usize {
242 usize::max(self.anchor, self.active)
243 }
244
245 /// The sequential range of the document represented by this selection.
246 ///
247 /// This is the range that would be replaced if text were inserted at this
248 /// selection.
249 pub fn range(&self) -> Range<usize> {
250 self.min()..self.max()
251 }
252
253 /// The length, in bytes of the selected region.
254 ///
255 /// If the selection is a caret, this is `0`.
256 pub fn len(&self) -> usize {
257 if self.anchor > self.active {
258 self.anchor - self.active
259 } else {
260 self.active - self.anchor
261 }
262 }
263
264 /// Returns `true` if the selection's length is `0`.
265 pub fn is_caret(&self) -> bool {
266 self.len() == 0
267 }
268}
269
270/// A lock on a text field that allows the platform to retrieve state and make
271/// edits.
272///
273/// This trait is implemented by the application or UI framework. The platform
274/// acquires this lock temporarily to apply edits corresponding to some user
275/// input. So long as the `InputHandler` has not been dropped, the only changes
276/// to the document state must come from calls to `InputHandler`.
277///
278/// Some methods require a mutable lock, as indicated when acquiring the lock
279/// with `WinHandler::text_input`. If a mutable method is called on a immutable
280/// lock, `InputHandler` may panic.
281///
282/// All ranges, lengths, and indices are specified in UTF-8 code units, unless
283/// specified otherwise.
284pub trait InputHandler {
285 /// The document's current [`Selection`].
286 ///
287 /// If the selection is a vertical caret bar, then `range.start == range.end`.
288 /// Both `selection.anchor` and `selection.active` must be less than or
289 /// equal to the value returned from `InputHandler::len()`, and must land on
290 /// a extended grapheme cluster boundary in the document.
291 fn selection(&self) -> Selection;
292
293 /// Set the document's selection.
294 ///
295 /// If the selection is a vertical caret bar, then `range.start == range.end`.
296 /// Both `selection.anchor` and `selection.active` must be less
297 /// than or equal to the value returned from `InputHandler::len()`.
298 ///
299 /// Properties of the `Selection` *other* than `anchor` and `active` may
300 /// be ignored by the handler.
301 ///
302 /// The `set_selection` implementation should round up (downstream) both
303 /// `selection.anchor` and `selection.active` to the nearest extended
304 /// grapheme cluster boundary.
305 ///
306 /// Requires a mutable lock.
307 fn set_selection(&mut self, selection: Selection);
308
309 /// The current composition region.
310 ///
311 /// This should be `Some` only if the IME is currently active, in which
312 /// case it represents the range of text that may be modified by the IME.
313 ///
314 /// Both `range.start` and `range.end` must be less than or equal
315 /// to the value returned from `InputHandler::len()`, and must land on a
316 /// extended grapheme cluster boundary in the document.
317 fn composition_range(&self) -> Option<Range<usize>>;
318
319 /// Set the composition region.
320 ///
321 /// If this is `Some` it means that the IME is currently active for this
322 /// region of the document. If it is `None` it means that the IME is not
323 /// currently active.
324 ///
325 /// Both `range.start` and `range.end` must be less than or equal to the
326 /// value returned from `InputHandler::len()`.
327 ///
328 /// The `set_selection` implementation should round up (downstream) both
329 /// `range.start` and `range.end` to the nearest extended grapheme cluster
330 /// boundary.
331 ///
332 /// Requires a mutable lock.
333 fn set_composition_range(&mut self, range: Option<Range<usize>>);
334
335 /// Check if the provided index is the first byte of a UTF-8 code point
336 /// sequence, or is the end of the document.
337 ///
338 /// Equivalent in functionality to [`str::is_char_boundary`].
339 fn is_char_boundary(&self, i: usize) -> bool;
340
341 /// The length of the document in UTF-8 code units.
342 fn len(&self) -> usize;
343
344 /// Returns `true` if the length of the document is `0`.
345 fn is_empty(&self) -> bool {
346 self.len() == 0
347 }
348
349 /// Returns the subslice of the document represented by `range`.
350 ///
351 /// # Panics
352 ///
353 /// Panics if the start or end of the range do not fall on a code point
354 /// boundary.
355 fn slice(&self, range: Range<usize>) -> Cow<str>;
356
357 /// Returns the number of UTF-16 code units in the provided UTF-8 range.
358 ///
359 /// Converts the document into UTF-8, looks up the range specified by
360 /// `utf8_range` (in UTF-8 code units), reencodes that substring into
361 /// UTF-16, and then returns the number of UTF-16 code units in that
362 /// substring.
363 ///
364 /// This is automatically implemented, but you can override this if you have
365 /// some faster system to determine string length.
366 ///
367 /// # Panics
368 ///
369 /// Panics if the start or end of the range do not fall on a code point
370 /// boundary.
371 fn utf8_to_utf16(&self, utf8_range: Range<usize>) -> usize {
372 self.slice(utf8_range).encode_utf16().count()
373 }
374
375 /// Returns the number of UTF-8 code units in the provided UTF-16 range.
376 ///
377 /// Converts the document into UTF-16, looks up the range specified by
378 /// `utf16_range` (in UTF-16 code units), reencodes that substring into
379 /// UTF-8, and then returns the number of UTF-8 code units in that
380 /// substring.
381 ///
382 /// This is automatically implemented, but you can override this if you have
383 /// some faster system to determine string length.
384 fn utf16_to_utf8(&self, utf16_range: Range<usize>) -> usize {
385 if utf16_range.is_empty() {
386 return 0;
387 }
388 let doc_range = 0..self.len();
389 let text = self.slice(doc_range);
390 //FIXME: we can do this without allocating; there's an impl in piet
391 let utf16: Vec<u16> = text
392 .encode_utf16()
393 .skip(utf16_range.start)
394 .take(utf16_range.end)
395 .collect();
396 String::from_utf16_lossy(&utf16).len()
397 }
398
399 /// Replaces a range of the text document with `text`.
400 ///
401 /// This method also sets the composition range to `None`, and updates the
402 /// selection:
403 ///
404 /// - If both the selection's anchor and active are `< range.start`, then
405 /// nothing is updated. - If both the selection's anchor and active are `>
406 /// range.end`, then subtract `range.len()` from both, and add `text.len()`.
407 /// - If neither of the previous two conditions are true, then set both
408 /// anchor and active to `range.start + text.len()`.
409 ///
410 /// After the above update, if we increase each end of the selection if
411 /// necessary to put it on a grapheme cluster boundary.
412 ///
413 /// Requires a mutable lock.
414 ///
415 /// # Panics
416 ///
417 /// Panics if either end of the range does not fall on a code point
418 /// boundary.
419 fn replace_range(&mut self, range: Range<usize>, text: &str);
420
421 /// Given a `Point`, determine the corresponding text position.
422 fn hit_test_point(&self, point: Point) -> HitTestPoint;
423
424 /// Returns the range, in UTF-8 code units, of the line (soft- or hard-wrapped)
425 /// containing the byte specified by `index`.
426 fn line_range(&self, index: usize, affinity: Affinity) -> Range<usize>;
427
428 /// Returns the bounding box, in window coordinates, of the visible text
429 /// document.
430 ///
431 /// For instance, a text box's bounding box would be the rectangle
432 /// of the border surrounding it, even if the text box is empty. If the
433 /// text document is completely offscreen, return `None`.
434 fn bounding_box(&self) -> Option<Rect>;
435
436 /// Returns the bounding box, in window coordinates, of the range of text specified by `range`.
437 ///
438 /// Ranges will always be equal to or a subrange of some line range returned
439 /// by `InputHandler::line_range`. If a range spans multiple lines,
440 /// `slice_bounding_box` may panic.
441 fn slice_bounding_box(&self, range: Range<usize>) -> Option<Rect>;
442
443 /// Applies an [`Action`] to the text field.
444 ///
445 /// Requires a mutable lock.
446 fn handle_action(&mut self, action: Action);
447}
448
449#[allow(dead_code)]
450/// Simulates `InputHandler` calls on `handler` for a given keypress `event`.
451///
452/// This circumvents the platform, and so can't work with important features
453/// like input method editors! However, it's necessary while we build up our
454/// input support on various platforms, which takes a lot of time. We want
455/// applications to start building on the new `InputHandler` interface
456/// immediately, with a hopefully seamless upgrade process as we implement IME
457/// input on more platforms.
458pub fn simulate_input<H: WinHandler + ?Sized>(
459 handler: &mut H,
460 token: Option<TextFieldToken>,
461 event: KeyEvent,
462) -> bool {
463 if handler.key_down(event.clone()) {
464 return true;
465 }
466
467 let token = match token {
468 Some(v) => v,
469 None => return false,
470 };
471 let mut input_handler = handler.acquire_input_lock(token, true);
472 match event.key {
473 KbKey::Character(c) if !event.mods.ctrl() && !event.mods.meta() && !event.mods.alt() => {
474 let selection = input_handler.selection();
475 input_handler.replace_range(selection.range(), &c);
476 let new_caret_index = selection.min() + c.len();
477 input_handler.set_selection(Selection::caret(new_caret_index));
478 }
479 KbKey::ArrowLeft => {
480 let movement = if event.mods.ctrl() {
481 Movement::Word(Direction::Left)
482 } else {
483 Movement::Grapheme(Direction::Left)
484 };
485 if event.mods.shift() {
486 input_handler.handle_action(Action::MoveSelecting(movement));
487 } else {
488 input_handler.handle_action(Action::Move(movement));
489 }
490 }
491 KbKey::ArrowRight => {
492 let movement = if event.mods.ctrl() {
493 Movement::Word(Direction::Right)
494 } else {
495 Movement::Grapheme(Direction::Right)
496 };
497 if event.mods.shift() {
498 input_handler.handle_action(Action::MoveSelecting(movement));
499 } else {
500 input_handler.handle_action(Action::Move(movement));
501 }
502 }
503 KbKey::ArrowUp => {
504 let movement = Movement::Vertical(VerticalMovement::LineUp);
505 if event.mods.shift() {
506 input_handler.handle_action(Action::MoveSelecting(movement));
507 } else {
508 input_handler.handle_action(Action::Move(movement));
509 }
510 }
511 KbKey::ArrowDown => {
512 let movement = Movement::Vertical(VerticalMovement::LineDown);
513 if event.mods.shift() {
514 input_handler.handle_action(Action::MoveSelecting(movement));
515 } else {
516 input_handler.handle_action(Action::Move(movement));
517 }
518 }
519 KbKey::Backspace => {
520 let movement = if event.mods.ctrl() {
521 Movement::Word(Direction::Upstream)
522 } else {
523 Movement::Grapheme(Direction::Upstream)
524 };
525 input_handler.handle_action(Action::Delete(movement));
526 }
527 KbKey::Delete => {
528 let movement = if event.mods.ctrl() {
529 Movement::Word(Direction::Downstream)
530 } else {
531 Movement::Grapheme(Direction::Downstream)
532 };
533 input_handler.handle_action(Action::Delete(movement));
534 }
535 KbKey::Enter => {
536 // I'm sorry windows, you'll get IME soon.
537 input_handler.handle_action(Action::InsertNewLine {
538 ignore_hotkey: false,
539 newline_type: '\n',
540 });
541 }
542 KbKey::Tab => {
543 let action = if event.mods.shift() {
544 Action::InsertBacktab
545 } else {
546 Action::InsertTab {
547 ignore_hotkey: false,
548 }
549 };
550 input_handler.handle_action(action);
551 }
552 KbKey::Home => {
553 let movement = if event.mods.ctrl() {
554 Movement::Vertical(VerticalMovement::DocumentStart)
555 } else {
556 Movement::Line(Direction::Upstream)
557 };
558 if event.mods.shift() {
559 input_handler.handle_action(Action::MoveSelecting(movement));
560 } else {
561 input_handler.handle_action(Action::Move(movement));
562 }
563 }
564 KbKey::End => {
565 let movement = if event.mods.ctrl() {
566 Movement::Vertical(VerticalMovement::DocumentEnd)
567 } else {
568 Movement::Line(Direction::Downstream)
569 };
570 if event.mods.shift() {
571 input_handler.handle_action(Action::MoveSelecting(movement));
572 } else {
573 input_handler.handle_action(Action::Move(movement));
574 }
575 }
576 KbKey::PageUp => {
577 let movement = Movement::Vertical(VerticalMovement::PageUp);
578 if event.mods.shift() {
579 input_handler.handle_action(Action::MoveSelecting(movement));
580 } else {
581 input_handler.handle_action(Action::Move(movement));
582 }
583 }
584 KbKey::PageDown => {
585 let movement = Movement::Vertical(VerticalMovement::PageDown);
586 if event.mods.shift() {
587 input_handler.handle_action(Action::MoveSelecting(movement));
588 } else {
589 input_handler.handle_action(Action::Move(movement));
590 }
591 }
592 _ => {
593 handler.release_input_lock(token);
594 return false;
595 }
596 };
597 handler.release_input_lock(token);
598 true
599}
600
601/// Indicates a movement that transforms a particular text position in a
602/// document.
603///
604/// These movements transform only single indices — not selections.
605///
606/// You'll note that a lot of these operations are idempotent, but you can get
607/// around this by first sending a `Grapheme` movement. If for instance, you
608/// want a `ParagraphStart` that is not idempotent, you can first send
609/// `Movement::Grapheme(Direction::Upstream)`, and then follow it with
610/// `ParagraphStart`.
611#[non_exhaustive]
612#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
613pub enum Movement {
614 /// A movement that stops when it reaches an extended grapheme cluster boundary.
615 ///
616 /// This movement is achieved on most systems by pressing the left and right
617 /// arrow keys. For more information on grapheme clusters, see
618 /// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries).
619 Grapheme(Direction),
620 /// A movement that stops when it reaches a word boundary.
621 ///
622 /// This movement is achieved on most systems by pressing the left and right
623 /// arrow keys while holding control. For more information on words, see
624 /// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Word_Boundaries).
625 Word(Direction),
626 /// A movement that stops when it reaches a soft line break.
627 ///
628 /// This movement is achieved on macOS by pressing the left and right arrow
629 /// keys while holding command. `Line` should be idempotent: if the
630 /// position is already at the end of a soft-wrapped line, this movement
631 /// should never push it onto another soft-wrapped line.
632 ///
633 /// In order to implement this properly, your text positions should remember
634 /// their affinity.
635 Line(Direction),
636 /// An upstream movement that stops when it reaches a hard line break.
637 ///
638 /// `ParagraphStart` should be idempotent: if the position is already at the
639 /// start of a hard-wrapped line, this movement should never push it onto
640 /// the previous line.
641 ParagraphStart,
642 /// A downstream movement that stops when it reaches a hard line break.
643 ///
644 /// `ParagraphEnd` should be idempotent: if the position is already at the
645 /// end of a hard-wrapped line, this movement should never push it onto the
646 /// next line.
647 ParagraphEnd,
648 /// A vertical movement, see `VerticalMovement` for more details.
649 Vertical(VerticalMovement),
650}
651
652/// Indicates a horizontal direction in the text.
653#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
654pub enum Direction {
655 /// The direction visually to the left.
656 ///
657 /// This may be byte-wise forwards or backwards in the document, depending
658 /// on the text direction around the position being moved.
659 Left,
660 /// The direction visually to the right.
661 ///
662 /// This may be byte-wise forwards or backwards in the document, depending
663 /// on the text direction around the position being moved.
664 Right,
665 /// Byte-wise backwards in the document.
666 ///
667 /// In a left-to-right context, this value is the same as `Left`.
668 Upstream,
669 /// Byte-wise forwards in the document.
670 ///
671 /// In a left-to-right context, this value is the same as `Right`.
672 Downstream,
673}
674
675impl Direction {
676 /// Returns `true` if this direction is byte-wise backwards for
677 /// the provided [`WritingDirection`].
678 ///
679 /// The provided direction *must not be* `WritingDirection::Natural`.
680 pub fn is_upstream_for_direction(self, direction: WritingDirection) -> bool {
681 assert!(
682 !matches!(direction, WritingDirection::Natural),
683 "writing direction must be resolved"
684 );
685 match self {
686 Direction::Upstream => true,
687 Direction::Downstream => false,
688 Direction::Left => matches!(direction, WritingDirection::LeftToRight),
689 Direction::Right => matches!(direction, WritingDirection::RightToLeft),
690 }
691 }
692}
693
694/// Distinguishes between two visually distinct locations with the same byte
695/// index.
696///
697/// Sometimes, a byte location in a document has two visual locations. For
698/// example, the end of a soft-wrapped line and the start of the subsequent line
699/// have different visual locations (and we want to be able to place an input
700/// caret in either place!) but the same byte-wise location. This also shows up
701/// in bidirectional text contexts. Affinity allows us to disambiguate between
702/// these two visual locations.
703pub enum Affinity {
704 Upstream,
705 Downstream,
706}
707
708/// Indicates a horizontal direction for writing text.
709#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
710pub enum WritingDirection {
711 LeftToRight,
712 RightToLeft,
713 /// Indicates writing direction should be automatically detected based on
714 /// the text contents.
715 Natural,
716}
717
718/// Indicates a vertical movement in a text document.
719#[non_exhaustive]
720#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
721pub enum VerticalMovement {
722 LineUp,
723 LineDown,
724 PageUp,
725 PageDown,
726 DocumentStart,
727 DocumentEnd,
728}
729
730/// A special text editing command sent from the platform to the application.
731#[non_exhaustive]
732#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
733pub enum Action {
734 /// Moves the selection.
735 ///
736 /// Before moving, if the active and the anchor of the selection are not at
737 /// the same position (it's a non-caret selection), then:
738 ///
739 /// 1. First set both active and anchor to the same position: the
740 /// selection's upstream index if `Movement` is an upstream movement, or
741 /// the downstream index if `Movement` is a downstream movement.
742 ///
743 /// 2. If `Movement` is `Grapheme`, then stop. Otherwise, apply the
744 /// `Movement` as per the usual rules.
745 Move(Movement),
746
747 /// Moves just the selection's active edge.
748 ///
749 /// Equivalent to holding shift while performing movements or clicks on most
750 /// operating systems.
751 MoveSelecting(Movement),
752
753 /// Select the entire document.
754 SelectAll,
755
756 /// Expands the selection to the entire soft-wrapped line.
757 ///
758 /// If multiple lines are already selected, expands the selection to
759 /// encompass all soft-wrapped lines that intersected with the prior
760 /// selection. If the selection is a caret is on a soft line break, uses
761 /// the affinity of the caret to determine which of the two lines to select.
762 /// `SelectLine` should be idempotent: it should never expand onto adjacent
763 /// lines.
764 SelectLine,
765
766 /// Expands the selection to the entire hard-wrapped line.
767 ///
768 /// If multiple lines are already selected, expands the selection to
769 /// encompass all hard-wrapped lines that intersected with the prior
770 /// selection. `SelectParagraph` should be idempotent: it should never
771 /// expand onto adjacent lines.
772 SelectParagraph,
773
774 /// Expands the selection to the entire word.
775 ///
776 /// If multiple words are already selected, expands the selection to
777 /// encompass all words that intersected with the prior selection. If the
778 /// selection is a caret is on a word boundary, selects the word downstream
779 /// of the caret. `SelectWord` should be idempotent: it should never expand
780 /// onto adjacent words.
781 ///
782 /// For more information on what these so-called "words" are, see
783 /// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Word_Boundaries).
784 SelectWord,
785
786 /// Deletes some text.
787 ///
788 /// If some text is already selected, `Movement` is ignored, and the
789 /// selection is deleted. If the selection's anchor is the same as the
790 /// active, then first apply `MoveSelecting(Movement)` and then delete the
791 /// resulting selection.
792 Delete(Movement),
793
794 /// Delete backwards, potentially breaking graphemes.
795 ///
796 /// A special kind of backspace that, instead of deleting the entire
797 /// grapheme upstream of the caret, may in some cases and character sets
798 /// delete a subset of that grapheme's code points.
799 DecomposingBackspace,
800
801 /// Maps the characters in the selection to uppercase.
802 ///
803 /// For more information on case mapping, see the
804 /// [Unicode Case Mapping FAQ](https://unicode.org/faq/casemap_charprop.html#7)
805 UppercaseSelection,
806
807 /// Maps the characters in the selection to lowercase.
808 ///
809 /// For more information on case mapping, see the
810 /// [Unicode Case Mapping FAQ](https://unicode.org/faq/casemap_charprop.html#7)
811 LowercaseSelection,
812
813 /// Maps the characters in the selection to titlecase.
814 ///
815 /// When calculating whether a character is at the beginning of a word, you
816 /// may have to peek outside the selection to other characters in the document.
817 ///
818 /// For more information on case mapping, see the
819 /// [Unicode Case Mapping FAQ](https://unicode.org/faq/casemap_charprop.html#7)
820 TitlecaseSelection,
821
822 /// Inserts a newline character into the document.
823 InsertNewLine {
824 /// If `true`, then always insert a newline, even if normally you
825 /// would run a keyboard shortcut attached to the return key, like
826 /// sending a message or activating autocomplete.
827 ///
828 /// On macOS, this is triggered by pressing option-return.
829 ignore_hotkey: bool,
830 /// Either `U+000A`, `U+2029`, or `U+2028`. For instance, on macOS, control-enter inserts `U+2028`.
831 //FIXME: what about windows?
832 newline_type: char,
833 },
834
835 /// Inserts a tab character into the document.
836 InsertTab {
837 /// If `true`, then always insert a tab, even if normally you would run
838 /// a keyboard shortcut attached to the return key, like indenting a
839 /// line or activating autocomplete.
840 ///
841 /// On macOS, this is triggered by pressing option-tab.
842 ignore_hotkey: bool,
843 },
844
845 /// Indicates the reverse of inserting tab; corresponds to shift-tab on most
846 /// operating systems.
847 InsertBacktab,
848
849 InsertSingleQuoteIgnoringSmartQuotes,
850 InsertDoubleQuoteIgnoringSmartQuotes,
851
852 /// Scrolls the text field without modifying the selection.
853 Scroll(VerticalMovement),
854
855 /// Centers the selection vertically in the text field.
856 ///
857 /// The average of the anchor's y and the active's y should be exactly
858 /// halfway down the field. If the selection is taller than the text
859 /// field's visible height, then instead scrolls the minimum distance such
860 /// that the text field is completely vertically filled by the selection.
861 ScrollToSelection,
862
863 /// Sets the writing direction of the selected text or caret.
864 SetSelectionWritingDirection(WritingDirection),
865
866 /// Sets the writing direction of all paragraphs that partially or fully
867 /// intersect with the selection or caret.
868 SetParagraphWritingDirection(WritingDirection),
869
870 /// Cancels the current window or operation.
871 ///
872 /// Triggered on most operating systems with escape.
873 Cancel,
874}