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}