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