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