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