Skip to main content

cranpose_ui/
selection_container.rs

1//! Selecting the read-only text inside a
2//! [`SelectionContainer`](crate::widgets::SelectionContainer).
3//!
4//! A container keeps a [`SelectionRegistrar`]. Every `Text` composed inside it
5//! registers what it shows and where its content sits in the window, and draws
6//! its part of the selection behind its glyphs. The container turns pointer
7//! and key input into one [`TextSelection`] across those texts, in the order a
8//! person reads them.
9
10use std::{
11    cell::{Cell, RefCell},
12    ops::Range,
13    rc::{Rc, Weak},
14};
15
16use cranpose_core::MutableState;
17use cranpose_ui_graphics::{Point, ProjectiveTransform, Rect, Size};
18
19use crate::{
20    text::{
21        AnnotatedString, TextLayoutOptions, TextStyle, get_offset_for_position, measure_text,
22        prepare_text_layout, text_align_fraction, wrapped_line_ranges,
23    },
24    text_selection::{
25        MULTI_TAP_SLOP_PX, MULTI_TAP_TIMEOUT_MS, SelectionAnchor, SelectionGranularity,
26        classify_tap_count, granularity_boundaries, tap_selection_granularity,
27    },
28};
29
30/// Hold before a resting touch selects the word under it.
31const LONG_PRESS_MS: u64 = 500;
32/// Travel before the hold ends that makes a touch a scroll rather than a
33/// long press.
34const LONG_PRESS_SLOP: f32 = 12.0;
35
36/// One end of a selection: a byte offset into one registered text.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct SelectionEdge {
39    /// The registered text the edge is in.
40    pub selectable: u64,
41    /// Byte offset into that text.
42    pub offset: usize,
43}
44
45/// A selection across the texts of one container. `anchor` stays where the
46/// gesture started and `focus` follows the pointer, so either may come first
47/// in reading order.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct TextSelection {
50    /// Where the selection started.
51    pub anchor: SelectionEdge,
52    /// Where the selection ends now.
53    pub focus: SelectionEdge,
54}
55
56/// Where a registered text's content sits in the window: layout writes the
57/// node's window transform and places the text in it, and the text's highlight the
58/// size its content draws in.
59#[derive(Default)]
60pub(crate) struct SelectableGeometry {
61    local_to_window: Rc<Cell<ProjectiveTransform>>,
62    content_origin: RefCell<crate::modifier::CoordinatorRect>,
63    content_size: Cell<Size>,
64}
65
66impl SelectableGeometry {
67    pub(crate) fn window_transform_sink(&self) -> Rc<Cell<ProjectiveTransform>> {
68        Rc::clone(&self.local_to_window)
69    }
70
71    pub(crate) fn set_content_origin(&self, origin: crate::modifier::CoordinatorRect) {
72        *self.content_origin.borrow_mut() = origin;
73    }
74
75    pub(crate) fn set_content_size(&self, size: Size) {
76        self.content_size.set(size);
77    }
78
79    fn rect(&self) -> Rect {
80        let offset = self.content_origin.borrow().origin();
81        let size = self.content_size.get();
82        self.local_to_window
83            .get()
84            .bounds_for_rect(Rect::from_origin_size(offset, size))
85    }
86
87    fn content_to_window(&self) -> ProjectiveTransform {
88        let offset = self.content_origin.borrow().origin();
89        ProjectiveTransform::translation(offset.x, offset.y).then(self.local_to_window.get())
90    }
91}
92
93/// One visual line of a registered text, in its content's coordinates.
94struct SelectableLine {
95    range: Range<usize>,
96    left: f32,
97    top: f32,
98}
99
100struct Selectable {
101    key: u64,
102    text: Rc<AnnotatedString>,
103    style: TextStyle,
104    options: TextLayoutOptions,
105    geometry: Rc<SelectableGeometry>,
106}
107
108impl Selectable {
109    fn len(&self) -> usize {
110        self.text.text.len()
111    }
112
113    fn wrap_width(&self) -> Option<f32> {
114        let width = self.geometry.content_size.get().width;
115        (width > 0.0).then_some(width)
116    }
117
118    fn line_height(&self) -> f32 {
119        prepare_text_layout(&self.text, &self.style, self.options, self.wrap_width())
120            .metrics
121            .line_height
122    }
123
124    /// The visual lines as the renderer lays them out: wrapped at the content
125    /// width and aligned line by line.
126    fn lines(&self, line_height: f32) -> Vec<SelectableLine> {
127        let width = self.geometry.content_size.get().width;
128        let fraction = text_align_fraction(&self.style, &self.text.text);
129        wrapped_line_ranges(
130            None,
131            &self.text,
132            &self.style,
133            self.options,
134            self.wrap_width(),
135        )
136        .into_iter()
137        .enumerate()
138        .map(|(index, range)| {
139            let line_width = self.prefix_width(range.start, range.end);
140            SelectableLine {
141                left: (width - line_width).max(0.0) * fraction,
142                top: index as f32 * line_height,
143                range,
144            }
145        })
146        .collect()
147    }
148
149    fn prefix_width(&self, start: usize, end: usize) -> f32 {
150        measure_text(
151            &AnnotatedString::from(&self.text.text[start..end]),
152            &self.style,
153        )
154        .width
155    }
156
157    /// The byte offset nearest `local`, a point in the content's coordinates.
158    fn offset_at(&self, local: Point) -> usize {
159        let line_height = self.line_height();
160        let lines = self.lines(line_height);
161        let Some(last) = lines.len().checked_sub(1) else {
162            return 0;
163        };
164        let index = if line_height > 0.0 {
165            ((local.y / line_height).floor().max(0.0) as usize).min(last)
166        } else {
167            0
168        };
169        let line = &lines[index];
170        let text = &self.text.text[line.range.clone()];
171        let within = get_offset_for_position(
172            &AnnotatedString::from(text),
173            &self.style,
174            local.x - line.left,
175            0.0,
176        );
177        line.range.start + within.min(text.len())
178    }
179
180    /// The highlight for bytes `start..end`, a rectangle per visual line, in
181    /// the content's coordinates.
182    fn highlight(&self, start: usize, end: usize) -> Vec<Rect> {
183        let line_height = self.line_height();
184        self.lines(line_height)
185            .into_iter()
186            .filter(|line| start < line.range.end && end > line.range.start)
187            .filter_map(|line| {
188                let from = start.max(line.range.start);
189                let to = end.min(line.range.end);
190                let x0 = line.left + self.prefix_width(line.range.start, from);
191                let x1 = line.left + self.prefix_width(line.range.start, to);
192                (x1 > x0).then_some(Rect {
193                    x: x0,
194                    y: line.top,
195                    width: x1 - x0,
196                    height: line_height,
197                })
198            })
199            .collect()
200    }
201}
202
203struct RegistrarInner {
204    next_key: Cell<u64>,
205    selectables: RefCell<Vec<Selectable>>,
206    selection: MutableState<Option<TextSelection>>,
207}
208
209/// The texts of one [`SelectionContainer`](crate::widgets::SelectionContainer)
210/// and the selection across them.
211#[derive(Clone)]
212pub struct SelectionRegistrar {
213    inner: Rc<RegistrarInner>,
214}
215
216impl PartialEq for SelectionRegistrar {
217    fn eq(&self, other: &Self) -> bool {
218        Rc::ptr_eq(&self.inner, &other.inner)
219    }
220}
221
222impl SelectionRegistrar {
223    pub(crate) fn new(selection: MutableState<Option<TextSelection>>) -> Self {
224        Self {
225            inner: Rc::new(RegistrarInner {
226                next_key: Cell::new(1),
227                selectables: RefCell::new(Vec::new()),
228                selection,
229            }),
230        }
231    }
232
233    /// A key for a text joining the container.
234    pub(crate) fn subscribe(&self) -> u64 {
235        let key = self.inner.next_key.get();
236        self.inner.next_key.set(key + 1);
237        key
238    }
239
240    /// What the text under `key` shows and where it draws.
241    pub(crate) fn update(
242        &self,
243        key: u64,
244        text: Rc<AnnotatedString>,
245        style: TextStyle,
246        options: TextLayoutOptions,
247        geometry: Rc<SelectableGeometry>,
248    ) {
249        let entry = Selectable {
250            key,
251            text,
252            style,
253            options,
254            geometry,
255        };
256        let mut selectables = self.inner.selectables.borrow_mut();
257        match selectables.iter_mut().find(|held| held.key == key) {
258            Some(held) => *held = entry,
259            None => selectables.push(entry),
260        }
261    }
262
263    /// The text under `key` left the container; a selection with an end in it
264    /// goes with it.
265    pub(crate) fn unsubscribe(&self, key: u64) {
266        self.inner
267            .selectables
268            .borrow_mut()
269            .retain(|held| held.key != key);
270        let touched = self
271            .inner
272            .selection
273            .get_non_reactive()
274            .is_some_and(|selection| {
275                selection.anchor.selectable == key || selection.focus.selectable == key
276            });
277        if touched {
278            self.inner.selection.set(None);
279        }
280    }
281
282    /// The current selection. Reading it subscribes the reader.
283    pub fn selection(&self) -> Option<TextSelection> {
284        self.inner.selection.get()
285    }
286
287    pub(crate) fn set_selection(&self, selection: Option<TextSelection>) {
288        self.inner.selection.set(selection);
289    }
290
291    /// Keys in reading order: top to bottom, then left to right, in the order
292    /// the texts joined where their places tie.
293    fn reading_order(&self) -> Vec<u64> {
294        let selectables = self.inner.selectables.borrow();
295        let mut order: Vec<(u64, Rect)> = selectables
296            .iter()
297            .map(|held| (held.key, held.geometry.rect()))
298            .collect();
299        order.sort_by(|(_, a), (_, b)| a.y.total_cmp(&b.y).then(a.x.total_cmp(&b.x)));
300        order.into_iter().map(|(key, _)| key).collect()
301    }
302
303    fn with_selectable<R>(&self, key: u64, read: impl FnOnce(&Selectable) -> R) -> Option<R> {
304        self.inner
305            .selectables
306            .borrow()
307            .iter()
308            .find(|held| held.key == key)
309            .map(read)
310    }
311
312    /// The edge a pointer at window point `point` selects to: inside a text,
313    /// the offset under it; beside the texts of a row, the nearest of them;
314    /// between rows, the start of the next text; past every text, the end of
315    /// the last.
316    pub(crate) fn edge_at(&self, point: Point) -> Option<SelectionEdge> {
317        let order = self.reading_order();
318        let selectables = self.inner.selectables.borrow();
319        let find = |key: u64| selectables.iter().find(|held| held.key == key);
320        let row: Vec<&Selectable> = order
321            .iter()
322            .filter_map(|key| find(*key))
323            .filter(|held| {
324                let rect = held.geometry.rect();
325                point.y >= rect.y && point.y <= rect.y + rect.height
326            })
327            .collect();
328        let horizontal_gap = |held: &&Selectable| {
329            let rect = held.geometry.rect();
330            (rect.x - point.x)
331                .max(point.x - (rect.x + rect.width))
332                .max(0.0)
333        };
334        if let Some(held) = row
335            .iter()
336            .min_by(|a, b| horizontal_gap(a).total_cmp(&horizontal_gap(b)))
337        {
338            let local = held
339                .geometry
340                .content_to_window()
341                .inverse()?
342                .map_point(point);
343            return Some(SelectionEdge {
344                selectable: held.key,
345                offset: held.offset_at(local),
346            });
347        }
348        let next = order
349            .iter()
350            .filter_map(|key| find(*key))
351            .find(|held| point.y < held.geometry.rect().y);
352        if let Some(held) = next {
353            return Some(SelectionEdge {
354                selectable: held.key,
355                offset: 0,
356            });
357        }
358        order
359            .last()
360            .and_then(|key| find(*key))
361            .map(|held| SelectionEdge {
362                selectable: held.key,
363                offset: held.len(),
364            })
365    }
366
367    /// The selection's two edges in reading order.
368    fn ordered(&self, selection: TextSelection) -> Option<(SelectionEdge, SelectionEdge)> {
369        let order = self.reading_order();
370        let rank = |edge: SelectionEdge| {
371            order
372                .iter()
373                .position(|key| *key == edge.selectable)
374                .map(|index| (index, edge.offset))
375        };
376        let (anchor, focus) = (rank(selection.anchor)?, rank(selection.focus)?);
377        Some(if anchor <= focus {
378            (selection.anchor, selection.focus)
379        } else {
380            (selection.focus, selection.anchor)
381        })
382    }
383
384    /// The bytes of the text under `key` that `selection` covers.
385    fn covered(&self, selection: TextSelection, key: u64) -> Option<Range<usize>> {
386        let (first, last) = self.ordered(selection)?;
387        let order = self.reading_order();
388        let index = |key: u64| order.iter().position(|held| *held == key);
389        let (at, from, to) = (
390            index(key)?,
391            index(first.selectable)?,
392            index(last.selectable)?,
393        );
394        if at < from || at > to {
395            return None;
396        }
397        let len = self.with_selectable(key, Selectable::len)?;
398        let start = if key == first.selectable {
399            first.offset
400        } else {
401            0
402        };
403        let end = if key == last.selectable {
404            last.offset
405        } else {
406            len
407        };
408        (start < end).then_some(start..end.min(len))
409    }
410
411    /// The highlight the text under `key` draws for the current selection, in
412    /// its content's coordinates. Reading it subscribes the reader.
413    pub(crate) fn highlight(&self, key: u64) -> Vec<Rect> {
414        let Some(selection) = self.selection() else {
415            return Vec::new();
416        };
417        let Some(range) = self.covered(selection, key) else {
418            return Vec::new();
419        };
420        self.with_selectable(key, |held| held.highlight(range.start, range.end))
421            .unwrap_or_default()
422    }
423
424    /// The window rectangle of the selection's first highlighted line, where a
425    /// menu for it belongs.
426    pub(crate) fn first_highlight_in_window(&self) -> Option<Rect> {
427        let selection = self.inner.selection.get_non_reactive()?;
428        let (first, _) = self.ordered(selection)?;
429        let range = self.covered(selection, first.selectable)?;
430        self.with_selectable(first.selectable, |held| {
431            let transform = held.geometry.content_to_window();
432            held.highlight(range.start, range.end)
433                .first()
434                .map(|line| transform.bounds_for_rect(*line))
435        })
436        .flatten()
437    }
438
439    /// What is selected, the texts joined by line breaks in reading order.
440    pub fn selected_text(&self) -> String {
441        let Some(selection) = self.inner.selection.get_non_reactive() else {
442            return String::new();
443        };
444        self.reading_order()
445            .into_iter()
446            .filter_map(|key| {
447                let range = self.covered(selection, key)?;
448                self.with_selectable(key, |held| held.text.text[range].to_string())
449            })
450            .collect::<Vec<_>>()
451            .join("\n")
452    }
453
454    /// Selects every registered text from the first to the last.
455    pub(crate) fn select_all(&self) {
456        let order = self.reading_order();
457        let (Some(first), Some(last)) = (order.first(), order.last()) else {
458            return;
459        };
460        let Some(len) = self.with_selectable(*last, Selectable::len) else {
461            return;
462        };
463        self.set_selection(Some(TextSelection {
464            anchor: SelectionEdge {
465                selectable: *first,
466                offset: 0,
467            },
468            focus: SelectionEdge {
469                selectable: *last,
470                offset: len,
471            },
472        }));
473    }
474
475    /// The unit under `edge` that `granularity` selects.
476    fn unit_at(&self, edge: SelectionEdge, granularity: SelectionGranularity) -> Option<Unit> {
477        self.with_selectable(edge.selectable, |held| Unit {
478            selectable: edge.selectable,
479            anchor: SelectionAnchor::at(&held.text.text, edge.offset, granularity),
480        })
481    }
482
483    /// The selection a drag from the pressed `unit` to `edge` makes: the
484    /// pressed unit together with the unit under `edge`, anchored at the far
485    /// side of the pressed unit.
486    fn dragged(&self, unit: Unit, edge: SelectionEdge) -> Option<TextSelection> {
487        let granularity = unit.anchor.granularity;
488        if edge.selectable == unit.selectable {
489            let range = self.with_selectable(edge.selectable, |held| {
490                unit.anchor.dragged_to(&held.text.text, edge.offset)
491            })?;
492            return Some(TextSelection {
493                anchor: SelectionEdge {
494                    selectable: edge.selectable,
495                    offset: range.start,
496                },
497                focus: SelectionEdge {
498                    selectable: edge.selectable,
499                    offset: range.end,
500                },
501            });
502        }
503        let (unit_start, unit_end) = self.with_selectable(edge.selectable, |held| {
504            granularity_boundaries(&held.text.text, edge.offset, granularity)
505        })?;
506        let pressed_start = SelectionEdge {
507            selectable: unit.selectable,
508            offset: unit.anchor.start,
509        };
510        let forward = self
511            .ordered(TextSelection {
512                anchor: pressed_start,
513                focus: edge,
514            })
515            .is_some_and(|(first, _)| first == pressed_start);
516        Some(if forward {
517            TextSelection {
518                anchor: pressed_start,
519                focus: SelectionEdge {
520                    selectable: edge.selectable,
521                    offset: unit_end,
522                },
523            }
524        } else {
525            TextSelection {
526                anchor: SelectionEdge {
527                    selectable: unit.selectable,
528                    offset: unit.anchor.end,
529                },
530                focus: SelectionEdge {
531                    selectable: edge.selectable,
532                    offset: unit_start,
533                },
534            }
535        })
536    }
537}
538
539/// What a press selected in one text, grown by a drag in the same unit.
540#[derive(Clone, Copy, Debug, PartialEq)]
541struct Unit {
542    selectable: u64,
543    anchor: SelectionAnchor,
544}
545
546impl Unit {
547    fn selection(self) -> TextSelection {
548        TextSelection {
549            anchor: SelectionEdge {
550                selectable: self.selectable,
551                offset: self.anchor.start,
552            },
553            focus: SelectionEdge {
554                selectable: self.selectable,
555                offset: self.anchor.end,
556            },
557        }
558    }
559}
560
561/// A pointer held down on the container.
562#[derive(Clone, Copy)]
563struct Press {
564    start: Point,
565    touch: bool,
566    /// The unit the press selected, once it selects: at once for a mouse,
567    /// after the hold for a touch.
568    unit: Option<Unit>,
569    /// A touch that moved before its hold ended: the finger is scrolling.
570    released_to_scroll: bool,
571}
572
573thread_local! {
574    /// The container a person last pressed: its selection is the one a copy
575    /// or a select-all acts on, and a press in another container clears it.
576    static ACTIVE: RefCell<Weak<RegistrarInner>> = const { RefCell::new(Weak::new()) };
577}
578
579/// Turns a container's pointer input into its selection.
580pub(crate) struct SelectionGesture {
581    registrar: SelectionRegistrar,
582    press: Cell<Option<Press>>,
583    last_tap: Cell<Option<(web_time::Instant, Point, u8)>>,
584    frame_clock: cranpose_core::internal::FrameClock,
585    long_press: RefCell<Option<cranpose_core::internal::FrameCallbackRegistration>>,
586    long_press_start: Cell<Option<u64>>,
587    menu_open: MutableState<bool>,
588}
589
590impl SelectionGesture {
591    pub(crate) fn new(
592        registrar: SelectionRegistrar,
593        frame_clock: cranpose_core::internal::FrameClock,
594        menu_open: MutableState<bool>,
595    ) -> Self {
596        Self {
597            registrar,
598            press: Cell::new(None),
599            last_tap: Cell::new(None),
600            frame_clock,
601            long_press: RefCell::new(None),
602            long_press_start: Cell::new(None),
603            menu_open,
604        }
605    }
606
607    /// Makes this container the one a copy acts on, clearing the selection of
608    /// the container pressed before it.
609    fn activate(&self) {
610        ACTIVE.with(|active| {
611            let previous = active.borrow().upgrade();
612            if let Some(previous) = previous
613                && !Rc::ptr_eq(&previous, &self.registrar.inner)
614            {
615                previous.selection.set(None);
616            }
617            *active.borrow_mut() = Rc::downgrade(&self.registrar.inner);
618        });
619    }
620
621    fn is_active(&self) -> bool {
622        ACTIVE.with(|active| {
623            active
624                .borrow()
625                .upgrade()
626                .is_some_and(|held| Rc::ptr_eq(&held, &self.registrar.inner))
627        })
628    }
629
630    pub(crate) fn on_down(self: &Rc<Self>, event: &crate::PointerEvent) {
631        self.activate();
632        crate::text_field_focus::clear_focus();
633        self.menu_open.set(false);
634        let touch = event.source.is_touch_like();
635        let point = event.global_position;
636        let mut press = Press {
637            start: point,
638            touch,
639            unit: None,
640            released_to_scroll: false,
641        };
642        if touch {
643            self.registrar.set_selection(None);
644            self.press.set(Some(press));
645            self.arm_long_press();
646            return;
647        }
648        let granularity = tap_selection_granularity(self.tap_count(point));
649        press.unit = self
650            .registrar
651            .edge_at(point)
652            .and_then(|edge| self.registrar.unit_at(edge, granularity));
653        let selection = press
654            .unit
655            .filter(|unit| unit.anchor.start < unit.anchor.end)
656            .map(Unit::selection);
657        self.registrar.set_selection(selection);
658        self.press.set(Some(press));
659    }
660
661    fn tap_count(&self, point: Point) -> u8 {
662        let now = web_time::Instant::now();
663        let previous = self.last_tap.get();
664        let count = classify_tap_count(
665            previous.map(|(_, at, count)| (count, at.x, at.y)),
666            previous.map_or(u128::MAX, |(time, _, _)| {
667                now.duration_since(time).as_millis()
668            }),
669            point.x,
670            point.y,
671            MULTI_TAP_TIMEOUT_MS,
672            MULTI_TAP_SLOP_PX,
673        );
674        self.last_tap.set(Some((now, point, count)));
675        count
676    }
677
678    /// Grows the selection with the pointer. Says whether the move belongs to
679    /// the selection, so the container can keep it from scrolling.
680    pub(crate) fn on_move(&self, event: &crate::PointerEvent) -> bool {
681        let Some(mut press) = self.press.get() else {
682            return false;
683        };
684        let point = event.global_position;
685        let Some(unit) = press.unit else {
686            if press.touch && !press.released_to_scroll {
687                let moved = (point.x - press.start.x)
688                    .abs()
689                    .max((point.y - press.start.y).abs());
690                if moved > LONG_PRESS_SLOP {
691                    press.released_to_scroll = true;
692                    self.press.set(Some(press));
693                    self.cancel_long_press();
694                }
695            }
696            return false;
697        };
698        if let Some(selection) = self
699            .registrar
700            .edge_at(point)
701            .and_then(|edge| self.registrar.dragged(unit, edge))
702        {
703            self.registrar.set_selection(Some(selection));
704        }
705        true
706    }
707
708    pub(crate) fn on_up(&self) {
709        self.cancel_long_press();
710        let press = self.press.take();
711        let selected = self
712            .registrar
713            .selection()
714            .is_some_and(|selection| selection.anchor != selection.focus);
715        if press.is_some_and(|press| press.touch && press.unit.is_some()) && selected {
716            self.menu_open.set(true);
717        }
718    }
719
720    pub(crate) fn on_cancel(&self) {
721        self.cancel_long_press();
722        self.press.set(None);
723    }
724
725    fn arm_long_press(self: &Rc<Self>) {
726        let weak = Rc::downgrade(self);
727        let registration = self.frame_clock.with_frame_nanos(move |now| {
728            if let Some(gesture) = weak.upgrade() {
729                gesture.long_press_tick(now);
730            }
731        });
732        *self.long_press.borrow_mut() = Some(registration);
733        crate::request_render_invalidation();
734    }
735
736    fn cancel_long_press(&self) {
737        self.long_press.borrow_mut().take();
738        self.long_press_start.set(None);
739    }
740
741    fn long_press_tick(self: Rc<Self>, now: u64) {
742        self.long_press.borrow_mut().take();
743        let Some(mut press) = self.press.get() else {
744            return;
745        };
746        if press.released_to_scroll || press.unit.is_some() {
747            return;
748        }
749        let start = self.long_press_start.get().unwrap_or(now);
750        self.long_press_start.set(Some(start));
751        if now.saturating_sub(start) < LONG_PRESS_MS * 1_000_000 {
752            self.arm_long_press();
753            return;
754        }
755        press.unit = self
756            .registrar
757            .edge_at(press.start)
758            .and_then(|edge| self.registrar.unit_at(edge, SelectionGranularity::Word));
759        self.press.set(Some(press));
760        self.registrar
761            .set_selection(press.unit.map(Unit::selection));
762        crate::request_render_invalidation();
763    }
764
765    /// Copy, select all and dismiss for the container a person last pressed.
766    /// Says whether the key was one of them.
767    pub(crate) fn on_key(&self, event: &crate::KeyEvent) -> bool {
768        if !event.is_key_down() || !self.is_active() {
769            return false;
770        }
771        let command = event.modifiers.command_or_ctrl();
772        match event.key_code {
773            crate::KeyCode::C if command => self.copy(),
774            crate::KeyCode::A if command => {
775                self.registrar.select_all();
776                true
777            }
778            crate::KeyCode::Escape if self.registrar.selection().is_some() => {
779                self.dismiss();
780                true
781            }
782            _ => false,
783        }
784    }
785
786    /// Puts the selection on the clipboard. Says whether there was one.
787    pub(crate) fn copy(&self) -> bool {
788        let text = self.registrar.selected_text();
789        if text.is_empty() {
790            return false;
791        }
792        crate::clipboard_session::clipboard_write_text(&text);
793        true
794    }
795
796    /// Clears the selection and closes its menu.
797    pub(crate) fn dismiss(&self) {
798        self.registrar.set_selection(None);
799        self.menu_open.set(false);
800    }
801}
802
803#[cfg(test)]
804#[path = "tests/selection_container_tests.rs"]
805mod tests;