Skip to main content

kui_core/runtime/
select_api.rs

1//! `Core`'s selection surface: what a host, a binding or the pointer
2//! model does to the window's selection, and what it reads back.
3//!
4//! Every query here answers from the frame that finished while one is
5//! being built, the way `text_hit` and `caret_rect` do: a press is made
6//! against the layout the user could see, not against the one being
7//! assembled in response to it.
8
9use crate::geom::{Rect, Vec2};
10use crate::input::{EditKey, Mods};
11use crate::key::Key;
12use crate::runtime::Core;
13use crate::select::{
14    CellEnd, CellSelection, CopyRequest, DragAnchor, Endpoint, Grain, RangeEnd, SelectDrag,
15    Selection, grained_edges, unbuilt_row_is_after,
16};
17use crate::tree::NodeContent;
18use crate::value::Value;
19
20impl Core {
21    /// The window's text selection outside an editor, if it has one.
22    pub fn selection(&self) -> Option<Selection> {
23        self.selection
24    }
25
26    /// The window's selection when it lives in a `cells` grid.
27    pub fn cell_selection(&self) -> Option<CellSelection> {
28        self.cell_selection
29    }
30
31    /// Sets it, clearing whatever else the window had selected.
32    pub fn set_cell_selection(&mut self, sel: CellSelection) {
33        self.cell_selection = Some(sel);
34        self.selection = None;
35        self.collapse_editor_selection();
36    }
37
38    /// Where `point` lands in the grid `key` drew, as an absolute line and
39    /// a column — the address a cell selection's end is. `None` when the
40    /// node drew no grid.
41    pub fn cell_at(&mut self, key: Key, point: Vec2) -> Option<CellEnd> {
42        let (row, col) = self.cell_row_col(key, point)?;
43        let id = self.cells_id_of(key)?;
44        Some(CellEnd::new(
45            self.cells.origin_line(id, self.building) + row as u64,
46            col,
47        ))
48    }
49
50    /// The grid a keyed node drew this frame, if it drew one.
51    pub(crate) fn cells_id_of(&mut self, key: Key) -> Option<crate::cells::CellsId> {
52        self.cells_id_of_ref(key)
53    }
54
55    pub(crate) fn cells_id_of_ref(&self, key: Key) -> Option<crate::cells::CellsId> {
56        // Off the cell store rather than off the tree: a host reading the
57        // selection from inside its own `view` is asking while this
58        // frame's tree is half-built, and the grid it means is last
59        // frame's — the same rule the text places follow.
60        self.cells.find(key, self.building)
61    }
62
63    /// The top-left of the cells themselves, which is the node's box
64    /// moved in by its padding — the same corner the grid is painted
65    /// from, so a hit test and the glyphs agree about where row 0 is.
66    pub(crate) fn cells_origin(&self, i: usize) -> Vec2 {
67        let pad = self.tree.specs[i].layout.padding;
68        let pos = self.tree.pos[i];
69        Vec2::new(pos.x + pad.l, pos.y + pad.t)
70    }
71
72    /// Where `point` lands in that grid, as a row and column clamped to
73    /// it — the one arithmetic, so the `cell` a click's payload carries
74    /// (`attach_pointer`), a selection and an app's own hit test agree.
75    pub(crate) fn cell_row_col(&mut self, key: Key, point: Vec2) -> Option<(usize, usize)> {
76        // Off the tree, not off the store: a hit test needs the node's
77        // box, and only a built frame has one. Which is also why this one
78        // reads the drawn frame rather than `building` — nothing hit-tests
79        // a frame that is still being declared.
80        let i = self.tree.index_of(key)?;
81        let crate::tree::NodeContent::Cells(id) = self.tree.content[i] else {
82            return None;
83        };
84        let cell = {
85            let sess = &mut *self.session.state();
86            self.cells
87                .cell_size(id, false, &sess.resources, &mut sess.fonts)
88        };
89        let (rows, cols) = self.cells.dims(id, false);
90        let pos = self.cells_origin(i);
91        let col = ((point.x - pos.x) / cell.w.max(f32::EPSILON)).floor();
92        let row = ((point.y - pos.y) / cell.h.max(f32::EPSILON)).floor();
93        Some((
94            (row.max(0.0) as usize).min(rows.saturating_sub(1)),
95            (col.max(0.0) as usize).min(cols.saturating_sub(1)),
96        ))
97    }
98
99    /// Selects the word under `point` in the grid `key` — a double click
100    /// on a terminal. Answers the span it took, as an absolute line and a
101    /// half-open column range, so a drag that follows can round to it.
102    pub fn select_word_in_cells(
103        &mut self,
104        key: Key,
105        point: Vec2,
106        block: bool,
107    ) -> Option<(u64, usize, usize)> {
108        let (row, col) = self.cell_row_col(key, point)?;
109        let id = self.cells_id_of_ref(key)?;
110        let (from, to) = self.cells.word_at(id, row, col, self.building)?;
111        let line = self.cells.origin_line(id, self.building) + row as u64;
112        self.set_cell_selection(
113            CellSelection::new(key, CellEnd::new(line, from), CellEnd::new(line, to)).block(block),
114        );
115        Some((line, from, to))
116    }
117
118    /// Selects the whole row under `point` — a triple click. Edge to edge,
119    /// the way a line in the middle of a linewise selection runs; the
120    /// copy is what trims the blanks off the end of it.
121    pub fn select_line_in_cells(
122        &mut self,
123        key: Key,
124        point: Vec2,
125        block: bool,
126    ) -> Option<(u64, usize, usize)> {
127        let (row, _) = self.cell_row_col(key, point)?;
128        let id = self.cells_id_of_ref(key)?;
129        let (_, cols) = self.cells.dims(id, self.building);
130        let line = self.cells.origin_line(id, self.building) + row as u64;
131        self.set_cell_selection(
132            CellSelection::new(key, CellEnd::new(line, 0), CellEnd::new(line, cols)).block(block),
133        );
134        Some((line, 0, cols))
135    }
136
137    /// Starts a cell selection at `point` in the grid `key`.
138    pub fn begin_cell_selection(&mut self, key: Key, point: Vec2, block: bool) -> bool {
139        let Some(at) = self.cell_at(key, point) else {
140            return false;
141        };
142        self.set_cell_selection(CellSelection::new(key, at, at).block(block));
143        true
144    }
145
146    /// Moves the live end of a cell selection to `point`.
147    pub fn extend_cell_selection(&mut self, point: Vec2) -> bool {
148        let Some(sel) = self.cell_selection else {
149            return false;
150        };
151        let Some(focus) = self.cell_at(sel.node, point) else {
152            return false;
153        };
154        if focus == sel.focus {
155            return false;
156        }
157        self.cell_selection = Some(CellSelection { focus, ..sel });
158        true
159    }
160
161    /// Moves the live end of a cell selection by whatever the press armed
162    /// it with: cells, words, or whole rows. The *anchor* rounds outwards
163    /// too, so a double-click-drag that turns back on itself keeps the
164    /// word it started in whole — the same rule the text side follows.
165    pub(crate) fn extend_cell_selection_grained(
166        &mut self,
167        drag: crate::select::SelectDrag,
168        point: Vec2,
169    ) -> bool {
170        let Some(crate::select::DragAnchor::Cells(a_line, a_from, a_to)) =
171            drag.anchor.filter(|_| drag.grain != Grain::Char)
172        else {
173            return self.extend_cell_selection(point);
174        };
175        let Some(sel) = self.cell_selection else {
176            return false;
177        };
178        let Some((row, col)) = self.cell_row_col(sel.node, point) else {
179            return false;
180        };
181        let Some(id) = self.cells_id_of_ref(sel.node) else {
182            return false;
183        };
184        let (_, cols) = self.cells.dims(id, self.building);
185        let line = self.cells.origin_line(id, self.building) + row as u64;
186        // The unit under the live end.
187        let (f_from, f_to) = match drag.grain {
188            Grain::Word => match self.cells.word_at(id, row, col, self.building) {
189                Some(span) => span,
190                None => return false,
191            },
192            _ => (0, cols),
193        };
194        // A block selection is ordered by column alone, because that is
195        // the only axis its two ends disagree on.
196        let backwards = if sel.block {
197            col < a_from
198        } else {
199            (line, col) < (a_line, a_from)
200        };
201        let (a_edge, f_edge) = grained_edges((a_from, a_to), (f_from, f_to), backwards);
202        let next = CellSelection::new(
203            sel.node,
204            CellEnd::new(a_line, a_edge),
205            CellEnd::new(line, f_edge),
206        )
207        .block(sel.block);
208        if next == sel {
209            return false;
210        }
211        self.cell_selection = Some(next);
212        true
213    }
214
215    /// The selected cells as text: one line per grid row it covers, each
216    /// with its trailing blanks trimmed — the rule that makes a copied
217    /// screen paste like text instead of like a rectangle of spaces.
218    ///
219    /// Only what the grid *holds*: a selection whose ends reach into the
220    /// scrollback copies the lines on screen, because the lines behind it
221    /// were never handed to the core.
222    pub fn cell_selection_text(&self) -> Option<String> {
223        let sel = self.cell_selection?;
224        let id = self.cells_id_of_ref(sel.node)?;
225        let (rows, cols) = self.cells.dims(id, self.building);
226        let origin = self.cells.origin_line(id, self.building);
227        let mut out = String::new();
228        let mut first = true;
229        let mut any = false;
230        for row in 0..rows {
231            let line = origin + row as u64;
232            let Some((from, to)) = sel.cols_on(line, cols) else {
233                continue;
234            };
235            if !first {
236                out.push('\n');
237            }
238            first = false;
239            any = true;
240            let mut text = String::new();
241            for col in from..to {
242                match self.cells.cell_char(id, row, col, self.building) {
243                    // The cell after a wide glyph is the app's spacer, and
244                    // copying it would put a blank in the middle of a word.
245                    Some((_, true)) => {}
246                    Some((ch, _)) => text.push(ch),
247                    None => {}
248                }
249            }
250            out.push_str(text.trim_end());
251        }
252        // Nothing of the grid is inside the selection — it is scrolled
253        // away entirely — so there is nothing to copy. `Some("")` here
254        // would let Cmd-C wipe whatever was on the clipboard.
255        any.then_some(out)
256    }
257
258    /// Sets it. The scope is a node that declared `selectable`; the two
259    /// ends are addresses inside it (a node key and a byte in that node's
260    /// own text). Ends the frame cannot resolve paint nothing rather than
261    /// something else, so setting a selection against a tree that has
262    /// since changed is safe.
263    ///
264    /// Clears the focused editor's own selection: there is one selection
265    /// per window.
266    pub fn set_selection(&mut self, sel: Selection) {
267        self.selection = Some(sel);
268        self.cell_selection = None;
269        self.collapse_editor_selection();
270    }
271
272    /// Drops the selection. Returns whether there was one.
273    pub fn clear_selection(&mut self) -> bool {
274        self.selection.take().is_some() | self.cell_selection.take().is_some()
275    }
276
277    /// Selects every run in `scope`, first byte to last — what Select All
278    /// does inside one. `false` when the scope drew no text.
279    pub fn select_all_in(&mut self, scope: Key) -> bool {
280        // A grid selects in cells: the whole screen it was given, from
281        // its first absolute line to its last.
282        if let Some(id) = self.cells_id_of_ref(scope) {
283            let (rows, cols) = self.cells.dims(id, self.building);
284            if rows == 0 || cols == 0 {
285                return false;
286            }
287            let origin = self.cells.origin_line(id, self.building);
288            self.set_cell_selection(CellSelection::new(
289                scope,
290                CellEnd::new(origin, 0),
291                CellEnd::new(origin + rows as u64 - 1, cols),
292            ));
293            return true;
294        }
295        // A virtual list selects its *data*: rows `0..count`, whether the
296        // frame built them or not (ADR 0017, tier 3). An end in a row the
297        // frame built is that row's first or last run, as a drag would
298        // have made it; one in a row it did not build is placed by its
299        // index alone, on a node no run matches — the scope's — with the
300        // last row's end spelled `ROW_END`, since nothing here knows how
301        // long a row it never laid out is.
302        if let Some(count) = self.row_count_in(scope) {
303            if count == 0 {
304                return false;
305            }
306            let last_row = count - 1;
307            let runs = self.text.scope_runs(scope, self.building);
308            let (mut first, mut last) = (None, None);
309            for run in &runs {
310                match self.row_of(run.place.key) {
311                    Some(0) if first.is_none() => first = Some(run.place.key),
312                    Some(r) if r == last_row => {
313                        last = Some((run.place.key, run.text.content().len()));
314                    }
315                    _ => {}
316                }
317            }
318            drop(runs);
319            let anchor = first.map_or(Endpoint::new(scope, 0), |k| Endpoint::new(k, 0));
320            let focus = last.map_or(Endpoint::new(scope, crate::select::ROW_END), |(k, len)| {
321                Endpoint::new(k, len)
322            });
323            self.set_selection(Selection::new(
324                scope,
325                anchor.in_row(Some(0)),
326                focus.in_row(Some(last_row)),
327            ));
328            return true;
329        }
330        let runs = self.text.scope_runs(scope, self.building);
331        let (Some(first), Some(last)) = (runs.first(), runs.last()) else {
332            return false;
333        };
334        let (fk, lk, llen) = (first.place.key, last.place.key, last.text.content().len());
335        drop(runs);
336        let sel = Selection::new(
337            scope,
338            Endpoint::new(fk, 0).in_row(self.row_of(fk)),
339            Endpoint::new(lk, llen).in_row(self.row_of(lk)),
340        );
341        self.set_selection(sel);
342        true
343    }
344
345    /// The `rowCount` declared on `scope` or on a node inside it, if any:
346    /// the size of the virtual list a Select All in that scope spans. The
347    /// first in tree order where two lists share one scope, which is not
348    /// a shape Select All can serve anyway.
349    fn row_count_in(&self, scope: Key) -> Option<u64> {
350        if self.tree.row_counts.is_empty() {
351            return None;
352        }
353        let top = self.tree.index_of(scope)?;
354        self.tree
355            .row_counts
356            .iter()
357            .find(|(node, _)| {
358                let mut i = *node as usize;
359                loop {
360                    if i == top {
361                        return true;
362                    }
363                    match self.tree.parent[i] {
364                        crate::tree::NIL => return false,
365                        p => i = p as usize,
366                    }
367                }
368            })
369            .map(|(_, n)| *n)
370    }
371
372    /// Where `point` (logical viewport px) lands inside `scope`, as the
373    /// address a selection end is made of. `None` when the scope drew
374    /// nothing the pointer could land in — an off-screen run is part of
375    /// the scope's text but is under no pointer.
376    pub fn selection_hit(&self, scope: Key, point: Vec2) -> Option<Endpoint> {
377        let (node, byte) = self.text.scope_hit(scope, point, self.building)?;
378        Some(Endpoint::new(node, byte).in_row(self.row_of(node)))
379    }
380
381    /// The virtualised row a node sits in, if any — what an endpoint keeps
382    /// so it can be placed after its row stops being built.
383    pub(crate) fn row_of(&self, node: Key) -> Option<u64> {
384        // Off the tree the key is looked up in, rather than off the map
385        // the last *emission* filled: during a build those are two
386        // different trees, and an index into one says nothing about the
387        // other. Free where it does not apply — a frame with no
388        // virtualised rows answers on the first line.
389        if self.tree.indexed.is_empty() {
390            return None;
391        }
392        let mut i = self.tree.index_of(node)?;
393        loop {
394            if let Some(&(_, row)) = self.tree.indexed.iter().find(|(n, _)| *n as usize == i) {
395                return Some(row);
396            }
397            match self.tree.parent[i] {
398                crate::tree::NIL => return None,
399                p => i = p as usize,
400            }
401        }
402    }
403
404    /// The selected text, assembled across every run the selection
405    /// covers — including runs the frame built but never drew, which is
406    /// what makes a selection that ran past the bottom of a scroller copy
407    /// what the reader dragged over.
408    ///
409    /// `None` with no selection; an empty string when the selection is
410    /// empty or its ends no longer resolve.
411    pub fn selection_text(&self) -> Option<String> {
412        let sel = self.selection?;
413        let prev = self.building;
414        let from = self
415            .text
416            .scope_offset(sel.scope, sel.anchor.node, sel.anchor.byte, prev)?;
417        let to = self
418            .text
419            .scope_offset(sel.scope, sel.focus.node, sel.focus.byte, prev)?;
420        Some(self.text.scope_slice(sel.scope, from, to, prev))
421    }
422
423    /// The box the window's selection occupies, logical viewport px —
424    /// what a platform panel about that selection is anchored to. The
425    /// union of the drawn runs it covers, so a selection that runs off
426    /// the screen is anchored by the part the reader can see.
427    ///
428    /// `None` with no selection, an empty one, or one whose runs the
429    /// frame never drew.
430    pub fn selection_rect(&self) -> Option<Rect> {
431        let sel = self.selection?;
432        let prev = self.building;
433        let from = self
434            .text
435            .scope_offset(sel.scope, sel.anchor.node, sel.anchor.byte, prev)?;
436        let to = self
437            .text
438            .scope_offset(sel.scope, sel.focus.node, sel.focus.byte, prev)?;
439        self.text.scope_selection_rect(sel.scope, from, to, prev)
440    }
441
442    /// Where a platform panel about the selection should point: the
443    /// baseline origin of its first line, logical viewport px. See
444    /// `TextSystem::scope_selection_anchor`.
445    pub fn selection_anchor(&self) -> Option<Vec2> {
446        let sel = self.selection?;
447        let prev = self.building;
448        let from = self
449            .text
450            .scope_offset(sel.scope, sel.anchor.node, sel.anchor.byte, prev)?;
451        let to = self
452            .text
453            .scope_offset(sel.scope, sel.focus.node, sel.focus.byte, prev)?;
454        self.text.scope_selection_anchor(sel.scope, from, to, prev)
455    }
456
457    /// The selection as HTML — the same text `selection_text` gives, with
458    /// the bold, the italic and the span colours it was declared with.
459    /// `None` with no text selection; a cells
460    /// selection has no styling to carry and answers `None` too.
461    ///
462    /// Meant as the *second* clipboard flavour, beside the plain text and
463    /// never instead of it: an editor that understands HTML takes the
464    /// formatting, and everything else takes the words.
465    pub fn selection_html(&self) -> Option<String> {
466        let sel = self.selection?;
467        let prev = self.building;
468        let from = self
469            .text
470            .scope_offset(sel.scope, sel.anchor.node, sel.anchor.byte, prev)?;
471        let to = self
472            .text
473            .scope_offset(sel.scope, sel.focus.node, sel.focus.byte, prev)?;
474        let html = self.text.scope_html(sel.scope, from, to, prev);
475        (!html.is_empty()).then_some(html)
476    }
477
478    /// The selection's two ends as the app's own addresses — the data
479    /// index of the virtualised row each is in, and the byte inside that
480    /// row's text — **in reading order**: `from` precedes `to` whichever
481    /// way the drag was made, so an app answering a `selectionrange` ask
482    /// can iterate `from..=to` (the clipboard examples do). `None` when
483    /// there is no selection, or when neither end is in a virtualised row
484    /// (nothing to ask about: the core has it all). The directed pair is
485    /// [`Self::selection_ends`].
486    pub fn selection_range(&self) -> Option<(RangeEnd, RangeEnd)> {
487        let sel = self.selection?;
488        let (a, f) = (sel.anchor, sel.focus);
489        if a.row.is_none() && f.row.is_none() {
490            return None;
491        }
492        let end = |e: Endpoint| RangeEnd {
493            row: e.row,
494            byte: e.byte,
495        };
496        if self.end_precedes(sel.scope, f, a) {
497            Some((end(f), end(a)))
498        } else {
499            Some((end(a), end(f)))
500        }
501    }
502
503    /// Whether `x` comes before `y` in the scope's reading order — the
504    /// question a backwards drag makes of two ends. Both built this frame:
505    /// by their offset in the scope's concatenation, the order the drag
506    /// itself is decided by. Both in virtualised rows: by row, then byte.
507    /// One built and one in a row the frame never built: the unbuilt row
508    /// is placed after the scope's built rows or before them by the one
509    /// rule `resolve_selection` paints by (`unbuilt_row_is_after`), so
510    /// the highlight and the answer agree. Nothing to compare by: the
511    /// pair keeps its order — `false` here, since the caller asks whether
512    /// the focus precedes the anchor.
513    fn end_precedes(&self, scope: Key, x: Endpoint, y: Endpoint) -> bool {
514        let prev = self.building;
515        let ox = self.text.scope_offset(scope, x.node, x.byte, prev);
516        let oy = self.text.scope_offset(scope, y.node, y.byte, prev);
517        match (ox, oy, x.row, y.row) {
518            (Some(ox), Some(oy), ..) => ox < oy,
519            (_, _, Some(rx), Some(ry)) => (rx, x.byte) < (ry, y.byte),
520            (Some(_), None, _, Some(ry)) => unbuilt_row_is_after(ry, self.last_built_row_in(scope)),
521            (None, Some(_), Some(rx), _) => {
522                !unbuilt_row_is_after(rx, self.last_built_row_in(scope))
523            }
524            _ => false,
525        }
526    }
527
528    /// The row of the last text run the frame built inside `scope` that
529    /// carries one — what an end the frame did not build is placed
530    /// against. This scope's rows only: a second virtual list on screen
531    /// says nothing about where a row of this one sits.
532    fn last_built_row_in(&self, scope: Key) -> Option<u64> {
533        (0..self.tree.len()).rev().find_map(|i| {
534            (self.scopes.get(i).copied().flatten() == Some(scope)
535                && matches!(self.tree.content[i], NodeContent::Text(_)))
536            .then(|| self.rows.get(i).copied().flatten())
537            .flatten()
538        })
539    }
540
541    /// The selection's two ends as the drag made them — the anchor where
542    /// the press landed, the focus where the pointer is — each as the
543    /// data index of the virtualised row it is in (`None` outside every
544    /// virtualised row) and the byte inside that node's own text. The
545    /// directed pair, unlike [`Self::selection_range`]'s: what a test or
546    /// a model that mirrors the selection reads, and what says whether a
547    /// Shift-press kept the anchor. `None` with no text
548    /// selection; a grid's is `cell_selection`.
549    pub fn selection_ends(&self) -> Option<(RangeEnd, RangeEnd)> {
550        let sel = self.selection?;
551        Some((
552            RangeEnd {
553                row: sel.anchor.row,
554                byte: sel.anchor.byte,
555            },
556            RangeEnd {
557                row: sel.focus.row,
558                byte: sel.focus.byte,
559            },
560        ))
561    }
562
563    /// Whether the core can answer a copy on its own: both ends resolve
564    /// against runs this frame built.
565    fn selection_is_whole(&self) -> bool {
566        let Some(sel) = self.selection else {
567            return false;
568        };
569        let prev = self.building;
570        self.text
571            .scope_offset(sel.scope, sel.anchor.node, sel.anchor.byte, prev)
572            .is_some()
573            && self
574                .text
575                .scope_offset(sel.scope, sel.focus.node, sel.focus.byte, prev)
576                .is_some()
577    }
578
579    /// Asks for the selection as text, and says how the answer will come.
580    ///
581    /// [`CopyRequest::Ready`] is the ordinary case: everything selected is
582    /// text the core shaped, so it hands it over. [`CopyRequest::Asked`]
583    /// is a selection that reaches rows a virtual list never built — the
584    /// core posts `{kind:"selectionrange", from:{index, byte}, to:{index,
585    /// byte}}` on the scope and waits for
586    /// [`Self::answer_selection_range`], because the rows behind that gap
587    /// are the app's and only the app has them.
588    ///
589    /// The event goes out with the frame's pending events, so a host that
590    /// calls this outside `handle_input` drains `take_pending_events`
591    /// after it.
592    pub fn request_copy(&mut self) -> CopyRequest {
593        if self.selection.is_none() && self.cell_selection.is_none() {
594            return match self
595                .edit
596                .focused()
597                .and_then(|k| self.edit.copy_selection(k))
598            {
599                Some(text) => CopyRequest::Ready(text),
600                None => CopyRequest::Nothing,
601            };
602        }
603        if self.cell_selection.is_some() || self.selection_is_whole() {
604            return match self.copy_selection() {
605                Some(text) => CopyRequest::Ready(text),
606                None => CopyRequest::Nothing,
607            };
608        }
609        let Some(sel) = self.selection else {
610            return CopyRequest::Nothing;
611        };
612        let Some((from, to)) = self.selection_range() else {
613            // Not virtualised and not resolvable: nothing to ask anyone
614            // about, and nothing to hand over.
615            return CopyRequest::Nothing;
616        };
617        self.pending.push(crate::input::UiEvent {
618            // The scope's own origin: an extension that declared the
619            // list is the one that can answer for its rows.
620            origin: self
621                .tree
622                .keys
623                .iter()
624                .position(|k| *k == sel.scope)
625                .map_or(crate::tree::OriginId::HOST, |i| self.tree.origins[i]),
626            window: crate::window::WindowId::MAIN,
627            key: sel.scope,
628            payload: Value::map([
629                ("kind", Value::str("selectionrange")),
630                ("from", from.to_value()),
631                ("to", to.to_value()),
632            ]),
633            slot: None,
634        });
635        self.awaiting_selection = true;
636        CopyRequest::Asked
637    }
638
639    /// The app's answer to a `selectionrange` ask: the text for the range
640    /// it was asked about, whole. Queues it for the clipboard the way a
641    /// menu's Copy does, and is ignored when nothing asked — a stale
642    /// answer cannot overwrite what somebody copied since.
643    pub fn answer_selection_range(&mut self, text: &str) -> bool {
644        if !std::mem::take(&mut self.awaiting_selection) {
645            return false;
646        }
647        self.menu_actions
648            .push(crate::menu::MenuAction::SetClipboard {
649                text: text.to_string(),
650                html: None,
651            });
652        true
653    }
654
655    // -- The clipboard, for an app that owns its text ---------------------
656    // A key sink hears the raw `Ctrl-c` / `Ctrl-v` and brings its own
657    // bindings — and had nowhere to bind them to (backlog C33): the only
658    // ways onto the system clipboard were a menu's Copy and Paste. These
659    // are those two actions with a door on them. The clipboard stays the
660    // host's: the core never reads it, and what a paste brings back
661    // arrives as input, the way a menu's Paste does.
662
663    /// Puts `text` on the system clipboard — queued as the
664    /// `MenuAction::SetClipboard` a menu's Copy produces, for the host to
665    /// apply at its next drain (the runner's is after every input and
666    /// every frame). `html` is a second flavour beside the text for a
667    /// host that offers one, never in place of it.
668    pub fn set_clipboard(&mut self, text: impl Into<String>, html: Option<String>) {
669        self.menu_actions
670            .push(crate::menu::MenuAction::SetClipboard {
671                text: text.into(),
672                html,
673            });
674    }
675
676    /// Puts a secret on the system clipboard the way a password manager
677    /// does — queued as `MenuAction::SetClipboardSecret`,
678    /// which the runner writes marked concealed and transient, so a
679    /// clipboard manager neither shows nor keeps it. What the marks are on
680    /// each platform is on the action. The text alone: a secret has no
681    /// second flavour to offer.
682    pub fn set_clipboard_secret(&mut self, text: impl Into<String>) {
683        self.menu_actions
684            .push(crate::menu::MenuAction::SetClipboardSecret { text: text.into() });
685    }
686
687    /// Asks for what is on the clipboard — queued as the
688    /// `MenuAction::Paste` a menu's Paste produces. The host reads the
689    /// clipboard and hands the text back as `InputEvent::Paste` (or a
690    /// bare `InputEvent::Commit`), which reaches a focused editor as
691    /// typing and a focused sink as `{kind:"text", text, tag}`, with
692    /// `concealed: true` / `transient: true` beside the text
693    /// when the pasteboard marked it so — so the app that asked
694    /// inserts it the way it inserts a committed IME string, and never
695    /// sees the clipboard any other way. The read stays on the driver's
696    /// side, where the permission lives.
697    ///
698    /// One ask at a time: while a paste is outstanding — queued, or taken
699    /// by the driver and not yet answered — a second ask is dropped, so a
700    /// view that asks on every frame until the answer lands asks once.
701    /// The answer is the `Paste` (or `Commit`) the driver sends, an empty
702    /// one when the clipboard held nothing, and [`Core::awaiting_paste`]
703    /// reads the state.
704    pub fn request_paste(&mut self) {
705        self.queue_paste();
706    }
707
708    /// Whether a paste ask is outstanding: asked and not yet answered
709    /// with a `Paste` or a `Commit`.
710    pub fn awaiting_paste(&self) -> bool {
711        self.awaiting_paste
712    }
713
714    /// Asks the host for a file dialog: an Open, a Save or
715    /// a folder picker, which the host shows as the platform's own. The
716    /// answer is an event, `{kind:"files", paths, tag}` — the `drop`
717    /// payload's shape, `paths` empty when the user cancelled — delivered
718    /// to whoever asked: the host from its own view or between frames, the
719    /// extension from inside its fill. A host drains the ask with
720    /// [`Core::take_file_requests`] and answers with `InputEvent::Files`;
721    /// the runner does both.
722    ///
723    /// One ask at a time, as for a paste: while one is outstanding —
724    /// queued, or taken and not yet answered — another is dropped and this
725    /// returns false, so a view that asks every frame until the answer
726    /// lands asks once. Between frames it asks for the frame that hands
727    /// the ask to the host.
728    #[track_caller]
729    pub fn request_files(&mut self, dialog: crate::dialog::FileDialog) -> bool {
730        if self.file_ask.pending() {
731            return false;
732        }
733        self.file_ask = crate::dialog::FileAsk::Queued(dialog, self.origin);
734        if !self.building {
735            self.owe_frame("request_files");
736        }
737        true
738    }
739
740    /// Whether a file dialog asked for is still unanswered.
741    pub fn awaiting_files(&self) -> bool {
742        self.file_ask.pending()
743    }
744
745    /// The file dialog asked for and not yet taken — at most one — for the
746    /// host to show. Taking it keeps the ask outstanding until the answer.
747    pub fn take_file_requests(&mut self) -> Vec<crate::dialog::FileDialog> {
748        match std::mem::take(&mut self.file_ask) {
749            crate::dialog::FileAsk::Queued(dialog, origin) => {
750                self.file_ask = crate::dialog::FileAsk::Taken(dialog.tag.clone(), origin);
751                vec![dialog]
752            }
753            other => {
754                self.file_ask = other;
755                Vec::new()
756            }
757        }
758    }
759
760    /// The one place a `Paste` is queued — the app's ask and a menu's
761    /// Paste row alike — so the gate is one.
762    pub(crate) fn queue_paste(&mut self) {
763        if self.awaiting_paste {
764            return;
765        }
766        self.awaiting_paste = true;
767        self.menu_actions.push(crate::menu::MenuAction::Paste);
768    }
769
770    /// Starts a selection at `point` inside `scope` — the press half of a
771    /// drag-select. Both ends land together, so nothing is selected until
772    /// the pointer moves.
773    pub fn begin_selection(&mut self, scope: Key, point: Vec2) -> bool {
774        let Some(at) = self.selection_hit(scope, point) else {
775            return false;
776        };
777        self.set_selection(Selection::new(scope, at, at));
778        true
779    }
780
781    /// Moves the live end of the selection to `point` — the motion half.
782    /// The anchor stays where the press put it, so dragging back past it
783    /// selects the other way rather than starting again.
784    pub fn extend_selection(&mut self, point: Vec2) -> bool {
785        let Some(sel) = self.selection else {
786            return false;
787        };
788        let Some(focus) = self.selection_hit(sel.scope, point) else {
789            return false;
790        };
791        if focus == sel.focus {
792            return false;
793        }
794        self.selection = Some(Selection { focus, ..sel });
795        true
796    }
797
798    /// A keyboard's selection in a `selectable` scope:
799    /// Shift with an arrow, Home or End on a focused node inside `scope`
800    /// — the scope itself when it is focusable, a control inside it —
801    /// moves the selection's focus the way the stock editor's Shift-
802    /// motions move its caret: a character (a word with `mods.word`)
803    /// left or right through the scope's runs in order, Home and End to
804    /// the scope's first and last byte. Nothing selected yet, the anchor
805    /// is placed at the scope's start, so Shift-End from a freshly
806    /// focused label selects it whole. Returns whether the selection
807    /// changed. Answered from the frame that finished, like a drag; the
808    /// endpoints carry their virtual rows like every other selection, so
809    /// a copy past the built range asks the app for the text, as any
810    /// virtualised selection does. Up and Down are not motions here: a scope has no line
811    /// geometry a caret could keep a column in.
812    pub fn keyboard_select(&mut self, scope: Key, key: EditKey, mods: Mods) -> bool {
813        let prev = self.building;
814        // Every character of the scope with its offset in the
815        // concatenation and the run it is in: what the motions step
816        // through. A run's end is a word's end — the concatenation has no
817        // separator, and a copy puts a newline there.
818        let chars: Vec<(usize, char, usize)> = self
819            .text
820            .scope_runs(scope, prev)
821            .iter()
822            .enumerate()
823            .flat_map(|(n, r)| {
824                let base = r.base;
825                r.text
826                    .content()
827                    .char_indices()
828                    .map(move |(i, c)| (base + i, c, n))
829                    .collect::<Vec<_>>()
830            })
831            .collect();
832        let total = chars.last().map_or(0, |(o, c, _)| o + c.len_utf8());
833        let sel = self.selection.filter(|s| s.scope == scope);
834        let at = |e: Endpoint| self.text.scope_offset(scope, e.node, e.byte, prev);
835        let (anchor, focus) = match sel {
836            Some(s) => match (at(s.anchor), at(s.focus)) {
837                (Some(a), Some(f)) => (a, f),
838                _ => (0, 0),
839            },
840            None => (0, 0),
841        };
842        let word = mods.word;
843        let ws = |i: usize| chars[i].1.is_whitespace();
844        let next = match key {
845            EditKey::Right => {
846                let mut i = chars
847                    .iter()
848                    .position(|(o, ..)| *o >= focus)
849                    .unwrap_or(chars.len());
850                if word {
851                    while i < chars.len() && ws(i) {
852                        i += 1;
853                    }
854                    let run = chars.get(i).map(|c| c.2);
855                    while i < chars.len() && !ws(i) && Some(chars[i].2) == run {
856                        i += 1;
857                    }
858                    chars.get(i).map_or(total, |(o, ..)| *o)
859                } else {
860                    chars.get(i).map_or(total, |(o, c, _)| o + c.len_utf8())
861                }
862            }
863            EditKey::Left => {
864                let mut i = chars
865                    .iter()
866                    .rposition(|(o, ..)| *o < focus)
867                    .map_or(0, |i| i + 1);
868                if word {
869                    while i > 0 && ws(i - 1) {
870                        i -= 1;
871                    }
872                    let run = (i > 0).then(|| chars[i - 1].2);
873                    while i > 0 && !ws(i - 1) && Some(chars[i - 1].2) == run {
874                        i -= 1;
875                    }
876                    chars.get(i).map_or(total, |(o, ..)| *o)
877                } else if i == 0 {
878                    0
879                } else {
880                    chars[i - 1].0
881                }
882            }
883            EditKey::Home => 0,
884            EditKey::End => total,
885            _ => return false,
886        };
887        if sel.is_some() && next == focus {
888            return false;
889        }
890        let Some(anchor) = self.endpoint_at_offset(scope, anchor, prev) else {
891            return false;
892        };
893        let Some(focus) = self.endpoint_at_offset(scope, next, prev) else {
894            return false;
895        };
896        self.set_selection(Selection::new(scope, anchor, focus));
897        true
898    }
899
900    /// The endpoint at `offset` in the scope's concatenation: the run it
901    /// falls in and the byte inside that run's text — the last run's end
902    /// for the offset past everything. `None` for a scope with no runs.
903    fn endpoint_at_offset(&self, scope: Key, offset: usize, prev: bool) -> Option<Endpoint> {
904        let runs = self.text.scope_runs(scope, prev);
905        let run = runs
906            .iter()
907            .find(|r| {
908                let (start, end) = r.span();
909                offset >= start && offset < end
910            })
911            .or_else(|| runs.last())?;
912        let node = run.place.key;
913        let byte = offset
914            .saturating_sub(run.base)
915            .min(run.text.content().len());
916        Some(Endpoint::new(node, byte).in_row(self.row_of(node)))
917    }
918
919    /// The motion half of a drag that is moving by *words* or by whole
920    /// runs: the live end rounds outwards to its own word (or run), and so
921    /// does the anchor, so the word the press took stays whole however far
922    /// back over itself the drag turns.
923    ///
924    /// This is what a double-click-and-drag does in every text UI, and
925    /// what the stock `<edit>` gets for free from cosmic-text's
926    /// `Selection::Word`; a `selectable` scope is the one that had to be
927    /// taught.
928    pub(crate) fn extend_selection_grained(
929        &mut self,
930        drag: crate::select::SelectDrag,
931        point: Vec2,
932    ) -> bool {
933        let grain = drag.grain;
934        if grain == Grain::Char {
935            return self.extend_selection(point);
936        }
937        let Some(sel) = self.selection else {
938            return false;
939        };
940        let Some(hit) = self.selection_hit(sel.scope, point) else {
941            return false;
942        };
943        let Some(crate::select::DragAnchor::Bytes(anode, a_from, a_to)) = drag.anchor else {
944            return self.extend_selection(point);
945        };
946        // The unit under the live end, in that node's own bytes.
947        let (f_from, f_to) = match grain {
948            Grain::Word => match self
949                .text
950                .word_at(sel.scope, hit.node, hit.byte, self.building)
951            {
952                Some(span) => span,
953                None => return false,
954            },
955            _ => match self
956                .text
957                .scope_runs(sel.scope, self.building)
958                .into_iter()
959                .find(|r| r.place.key == hit.node)
960                .map(|r| r.text.content().len())
961            {
962                Some(len) => (0, len),
963                None => return false,
964            },
965        };
966        // Which side of the anchor the live end is on, in the scope's
967        // concatenation.
968        let prev = self.building;
969        let ga = self.text.scope_offset(sel.scope, anode, a_from, prev);
970        let gf = self.text.scope_offset(sel.scope, hit.node, f_from, prev);
971        let (Some(ga), Some(gf)) = (ga, gf) else {
972            return false;
973        };
974        let (arow, frow) = (self.row_of(anode), self.row_of(hit.node));
975        let (a_edge, f_edge) = grained_edges((a_from, a_to), (f_from, f_to), gf < ga);
976        let next = Selection::new(
977            sel.scope,
978            Endpoint::new(anode, a_edge).in_row(arow),
979            Endpoint::new(hit.node, f_edge).in_row(frow),
980        );
981        if Some(next) == self.selection {
982            return false;
983        }
984        self.selection = Some(next);
985        true
986    }
987
988    /// Selects the word under `point` inside `scope` — a double click,
989    /// and (on macOS) a force click. Answers the span it took, in the
990    /// node's own bytes, so a drag that follows can round to it.
991    pub fn select_word_at(&mut self, scope: Key, point: Vec2) -> Option<(Key, usize, usize)> {
992        let at = self.selection_hit(scope, point)?;
993        let (from, to) = self.text.word_at(scope, at.node, at.byte, self.building)?;
994        let row = self.row_of(at.node);
995        self.set_selection(Selection::new(
996            scope,
997            Endpoint::new(at.node, from).in_row(row),
998            Endpoint::new(at.node, to).in_row(row),
999        ));
1000        Some((at.node, from, to))
1001    }
1002
1003    /// Selects the whole run under `point` — a triple click, which takes
1004    /// the line a label is. Answers the span, like `select_word_at`.
1005    pub fn select_run_at(&mut self, scope: Key, point: Vec2) -> Option<(Key, usize, usize)> {
1006        let at = self.selection_hit(scope, point)?;
1007        let len = self
1008            .text
1009            .scope_runs(scope, self.building)
1010            .into_iter()
1011            .find(|r| r.place.key == at.node)
1012            .map(|r| r.text.content().len())?;
1013        let row = self.row_of(at.node);
1014        self.set_selection(Selection::new(
1015            scope,
1016            Endpoint::new(at.node, 0).in_row(row),
1017            Endpoint::new(at.node, len).in_row(row),
1018        ));
1019        Some((at.node, 0, len))
1020    }
1021
1022    /// Arms a drag-select at a press inside `scope`, with what the click
1023    /// count says it moves by (`Grain::of_clicks`) and the span the press
1024    /// itself took, which both ends of the drag round outwards to. A grid
1025    /// selects in cells and a paragraph in bytes:
1026    /// this is where the two are told apart, once, and the anchor carries
1027    /// the answer for the drag. In a grid, Alt makes it the rectangular
1028    /// selection every terminal has. With `extend` — a Shift-press in
1029    /// the scope the selection is in — the anchor is kept and the press
1030    /// is the live end. Whether a drag was armed.
1031    pub(crate) fn arm_select_drag(
1032        &mut self,
1033        scope: Key,
1034        point: Vec2,
1035        clicks: u8,
1036        extend: bool,
1037    ) -> bool {
1038        let grain = if extend {
1039            Grain::Char
1040        } else {
1041            Grain::of_clicks(clicks)
1042        };
1043        let armed = if extend {
1044            // The anchor stays; the live end is the press, and the drag
1045            // goes on from there by characters, whatever the click count
1046            // — armed whether or not the press moved the end (one on the
1047            // focus itself moves nothing and still drags on). The
1048            // window's one selection is this one, so a focused editor's
1049            // collapses as `set_selection` would have it.
1050            let drag = SelectDrag {
1051                scope,
1052                grain,
1053                anchor: None,
1054            };
1055            self.extend_select_drag(drag, point);
1056            self.collapse_editor_selection();
1057            Some(None)
1058        } else if self.cells_id_of_ref(scope).is_some() {
1059            let block = self.interaction.modifiers().alt;
1060            match grain {
1061                Grain::Char => self
1062                    .begin_cell_selection(scope, point, block)
1063                    .then_some(None),
1064                Grain::Word => self
1065                    .select_word_in_cells(scope, point, block)
1066                    .map(|(l, f, t)| Some(DragAnchor::Cells(l, f, t))),
1067                Grain::Run => self
1068                    .select_line_in_cells(scope, point, block)
1069                    .map(|(l, f, t)| Some(DragAnchor::Cells(l, f, t))),
1070            }
1071        } else {
1072            match grain {
1073                Grain::Char => self.begin_selection(scope, point).then_some(None),
1074                Grain::Word => self
1075                    .select_word_at(scope, point)
1076                    .map(|(n, f, t)| Some(DragAnchor::Bytes(n, f, t))),
1077                Grain::Run => self
1078                    .select_run_at(scope, point)
1079                    .map(|(n, f, t)| Some(DragAnchor::Bytes(n, f, t))),
1080            }
1081        };
1082        if let Some(anchor) = armed {
1083            self.select_dragging = Some(SelectDrag {
1084                scope,
1085                grain,
1086                anchor,
1087            });
1088        }
1089        armed.is_some()
1090    }
1091
1092    /// Moves the live end of the drag [`Self::arm_select_drag`] started,
1093    /// in whichever geometry its scope has.
1094    pub(crate) fn extend_select_drag(&mut self, drag: SelectDrag, point: Vec2) -> bool {
1095        if self.cells_id_of_ref(drag.scope).is_some() {
1096            self.extend_cell_selection_grained(drag, point)
1097        } else {
1098            self.extend_selection_grained(drag, point)
1099        }
1100    }
1101
1102    /// Selects the word under `point` in `scope`, whichever way the scope
1103    /// addresses itself — what a double click takes, and what a force
1104    /// click takes before it asks for a definition. `false` when there
1105    /// was no word there.
1106    pub fn select_word_under(&mut self, scope: Key, point: Vec2) -> bool {
1107        if self.cells_id_of_ref(scope).is_some() {
1108            self.select_word_in_cells(scope, point, false).is_some()
1109        } else {
1110            self.select_word_at(scope, point).is_some()
1111        }
1112    }
1113
1114    /// Collapses the focused editor's selection, so a window never shows
1115    /// two. The caret stays where it was: the editor keeps its focus and
1116    /// its insertion point, and only the highlight goes.
1117    fn collapse_editor_selection(&mut self) {
1118        if let Some(key) = self.edit.focused() {
1119            self.edit.collapse_selection(key);
1120        }
1121    }
1122}