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