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