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