kui_core/select.rs
1//! The window's text selection outside an editor: what a `selectable`
2//! node scopes, what a press-drag across it produces, and what a copy
3//! reads.
4//!
5//! A view opts text in with `NodeSpec::selectable` on a container; the
6//! core then handles the drag, Shift-arrows, double and triple clicks and
7//! Select All. An app reads the result with `Core::selection` (a
8//! [`Selection`]) or `Core::cell_selection` (a [`CellSelection`] inside a
9//! `cells` grid), and asks for the text with `Core::request_copy`, which
10//! answers a [`CopyRequest`]. There is one selection per window, and an
11//! editor's own selection is the other half of that: starting one clears
12//! the other.
13//!
14//! A selection is two addresses, a node key and a byte inside that node's
15//! own text, never an index into a frame's vectors. A frame that no longer
16//! builds the node resolves it to nothing; a frame that builds it again
17//! resolves it again.
18//!
19//! ```rust
20//! use kui_core::{CellEnd, CellSelection, Endpoint, Key, Selection};
21//!
22//! // A drag from byte 3 of one label back to byte 1 of an earlier one.
23//! let scope = Key::ROOT.str("card");
24//! let sel = Selection::new(
25//! scope,
26//! Endpoint::new(scope.str("second"), 3),
27//! Endpoint::new(scope.str("first"), 1),
28//! );
29//! assert!(!sel.is_empty());
30//!
31//! // Inside a terminal grid, ends are absolute lines and columns.
32//! let grid = CellSelection::new(Key::ROOT.str("term"), CellEnd::new(10, 3), CellEnd::new(12, 5));
33//! assert_eq!(grid.cols_on(11, 80), Some((0, 80))); // a middle line, edge to edge
34//! assert_eq!(grid.block(true).cols_on(11, 80), Some((3, 5))); // a block: same columns
35//! ```
36
37use crate::color::Color;
38use crate::key::Key;
39
40/// What a selection is painted under when nothing else says — the dark
41/// base's tint, and what kui painted before there were themes. The live
42/// value is `theme.selection`, which both a `selectable` scope and an
43/// editor read, because a selection over a label and one over a field
44/// sitting side by side must not be two different blues. This constant
45/// stays as the floor an [`crate::edit::EditOptions`] the core never
46/// stamped falls back to.
47pub const TINT: Color = Color {
48 r: 0x3b as f32 / 255.0,
49 g: 0x5b as f32 / 255.0,
50 b: 0xd4 as f32 / 255.0,
51 a: 0x66 as f32 / 255.0,
52};
53
54/// A `byte` that means "the end of the row, whatever its length": what a
55/// Select All puts on the last row of a `selectable` virtual list the
56/// frame did not build, since the core never laid that row out and cannot
57/// know where it ends. A `selectionrange` ask carries it as written, past
58/// any row's length, and the app cuts it to the row. `u32::MAX` rather
59/// than `usize::MAX` so it survives a wire that spells bytes as numbers.
60pub const ROW_END: usize = u32::MAX as usize;
61
62/// One end of a selection: the node whose text it lands in, and a byte
63/// offset into *that node's* content (not into the scope's).
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub struct Endpoint {
66 pub node: Key,
67 pub byte: usize,
68 /// The data index of the virtualised row this end is in, when it is in
69 /// one (`open_indexed`). Recorded when the end is made, and the only
70 /// thing that can place it once its row stops being built: a key says
71 /// *which* node, an index says *where in the data* — and a frame that
72 /// never built the node can still answer the second question.
73 pub row: Option<u64>,
74}
75
76impl Endpoint {
77 pub fn new(node: Key, byte: usize) -> Self {
78 Self {
79 node,
80 byte,
81 row: None,
82 }
83 }
84
85 /// The same end, in the virtualised row `row`.
86 pub fn in_row(mut self, row: Option<u64>) -> Self {
87 self.row = row;
88 self
89 }
90}
91
92/// The window's selection: a scope and two ends of it. `anchor` is where
93/// the press landed and `focus` is where the pointer is now, so the pair
94/// is *directed* — dragging back past the anchor selects the other way
95/// without the two swapping, which is what keeps a drag from feeling like
96/// it jumps when it crosses its own start.
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
98pub struct Selection {
99 /// The `selectable` node the selection lives inside.
100 pub scope: Key,
101 pub anchor: Endpoint,
102 pub focus: Endpoint,
103}
104
105impl Selection {
106 pub fn new(scope: Key, anchor: Endpoint, focus: Endpoint) -> Self {
107 Self {
108 scope,
109 anchor,
110 focus,
111 }
112 }
113
114 /// A selection of no text — a click that placed both ends together.
115 /// It still exists (the scope is where the next Shift-click or drag
116 /// extends from), and it copies nothing.
117 pub fn is_empty(&self) -> bool {
118 self.anchor == self.focus
119 }
120}
121
122/// What a drag-select moves by. A press sets it from the click count the
123/// driver counted, the way every text UI does: one click drags by
124/// characters, two by words, three by whole runs.
125///
126/// The unit is not just a rounding of the live end — the *anchor* rounds
127/// too, and outwards. A double-click-drag that turns back on itself keeps
128/// the word it started in whole, which is what makes the gesture feel
129/// like it is selecting words rather than snapping to them.
130#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
131pub enum Grain {
132 #[default]
133 Char,
134 Word,
135 /// One text node's whole content — a label, a paragraph. cosmic-text
136 /// calls this a line; here a run is the thing a triple click takes.
137 /// In a `cells` grid it is one whole row, edge to edge, which is what
138 /// a triple click takes in every terminal.
139 Run,
140}
141
142impl Grain {
143 /// The grain a press arms, from the click count the driver counted:
144 /// one click (or none counted) a character, two the word under it,
145 /// three or more the whole run.
146 pub(crate) fn of_clicks(clicks: u8) -> Self {
147 match clicks {
148 0 | 1 => Grain::Char,
149 2 => Grain::Word,
150 _ => Grain::Run,
151 }
152 }
153}
154
155/// One end of a selection in a cell grid: an *absolute* line (the grid's
156/// `origin_line` plus the row) and a column. Absolute because a grid is
157/// one screenful of an app's own history, so a row number means a
158/// different line after every scroll.
159#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
160pub struct CellEnd {
161 pub line: u64,
162 pub col: usize,
163}
164
165impl CellEnd {
166 pub fn new(line: u64, col: usize) -> Self {
167 Self { line, col }
168 }
169}
170
171/// A selection inside one `cells` grid.
172#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173pub struct CellSelection {
174 /// The grid it lives in.
175 pub node: Key,
176 pub anchor: CellEnd,
177 pub focus: CellEnd,
178 /// Rectangular rather than linewise: the column range is the same on
179 /// every line, which is how a terminal selects a column of output.
180 /// Held by a modifier while dragging, the way every terminal does it.
181 pub block: bool,
182}
183
184impl CellSelection {
185 pub fn new(node: Key, anchor: CellEnd, focus: CellEnd) -> Self {
186 Self {
187 node,
188 anchor,
189 focus,
190 block: false,
191 }
192 }
193
194 pub fn block(mut self, on: bool) -> Self {
195 self.block = on;
196 self
197 }
198
199 pub fn is_empty(&self) -> bool {
200 self.anchor == self.focus
201 }
202
203 /// The selection as data — `{node, anchor: {line, col}, focus: {line,
204 /// col}, block}`, the ends as the drag made them (directed, like
205 /// [`RangeEnd::to_value`]'s) and the lines absolute: the shape a
206 /// binding's `cell_selection` reads back, spelled once.
207 pub fn to_value(self, handles: crate::value::Handles) -> crate::value::Value {
208 use crate::value::Value;
209 let end = |e: CellEnd| {
210 Value::map([
211 ("line", Value::Int(e.line as i64)),
212 ("col", Value::Int(e.col as i64)),
213 ])
214 };
215 Value::map([
216 ("node", (handles.key)(self.node)),
217 ("anchor", end(self.anchor)),
218 ("focus", end(self.focus)),
219 ("block", Value::Bool(self.block)),
220 ])
221 }
222
223 /// The two ends in reading order.
224 pub fn ordered(&self) -> (CellEnd, CellEnd) {
225 if self.anchor <= self.focus {
226 (self.anchor, self.focus)
227 } else {
228 (self.focus, self.anchor)
229 }
230 }
231
232 /// The columns selected on `line`, as a half-open range, or `None`
233 /// when the line is outside the selection. Linewise by default — a
234 /// line in the middle runs edge to edge, which `cols` gives — and a
235 /// block selection is the same column range on every line it covers.
236 pub fn cols_on(&self, line: u64, cols: usize) -> Option<(usize, usize)> {
237 let (a, b) = self.ordered();
238 if self.block {
239 if line < a.line || line > b.line {
240 return None;
241 }
242 let (lo, hi) = (a.col.min(b.col), a.col.max(b.col));
243 return (lo < hi).then_some((lo.min(cols), hi.min(cols)));
244 }
245 clip_to_unit((a.line, a.col), (b.line, b.col), line, cols)
246 }
247}
248
249/// The part of one unit — a text node of `len` bytes, a grid row of `len`
250/// columns — that a selection running from `start` to `end` covers, as a
251/// half-open range in that unit's own offsets. `None` when the unit is
252/// outside the selection. The unit is named by whatever orders the
253/// scope's units (an ordinal, an absolute line); the arithmetic is the
254/// same for both geometries: the
255/// first unit runs from the start's offset, the last to the end's, and
256/// every unit between runs edge to edge.
257pub(crate) fn clip_to_unit<U: Ord + Copy>(
258 start: (U, usize),
259 end: (U, usize),
260 unit: U,
261 len: usize,
262) -> Option<(usize, usize)> {
263 if unit < start.0 || unit > end.0 {
264 return None;
265 }
266 let from = if unit == start.0 { start.1 } else { 0 };
267 let to = if unit == end.0 { end.1 } else { len };
268 let (from, to) = (from.min(len), to.min(len));
269 (from < to).then_some((from, to))
270}
271
272/// The edges a grained drag runs between: the anchor's unit and the live
273/// end's unit are each `(from, to)`, and which side of the anchor the
274/// live end is on decides which edge of each the selection takes —
275/// backwards, from the far edge of the anchor's unit to the near edge of
276/// the live one; forwards, the reverse. So the unit the press took stays
277/// whole however far back over itself the drag turns, in bytes or in
278/// cells alike. Answers `(anchor edge, live edge)`.
279pub(crate) fn grained_edges(
280 anchor: (usize, usize),
281 live: (usize, usize),
282 backwards: bool,
283) -> (usize, usize) {
284 if backwards {
285 (anchor.1, live.0)
286 } else {
287 (anchor.0, live.1)
288 }
289}
290
291/// Where a selection end in a virtualised row the frame did not build
292/// sits against the rows it did: after every built run of the scope iff
293/// its row is past the last built row that carries one (`last`), before
294/// them otherwise — below the first row, or in a hole, which a contiguous
295/// virtual window does not have. The one rule the highlight paints by
296/// (`resolve_selection`) and a `selectionrange` ask orders by
297/// (`selection_range`). With no built row to compare against, nothing is
298/// after: the start is the honest boundary.
299pub(crate) fn unbuilt_row_is_after(row: u64, last: Option<u64>) -> bool {
300 last.is_some_and(|hi| row > hi)
301}
302
303/// One end of the range an app is asked to fill in
304/// (`Core::selection_range`): the data index of the row it is in, and the
305/// byte inside that row's own text. An end outside every virtualised row
306/// has no index — it is text the core built and can answer for itself.
307#[derive(Clone, Copy, Debug, PartialEq, Eq)]
308pub struct RangeEnd {
309 pub row: Option<u64>,
310 pub byte: usize,
311}
312
313impl RangeEnd {
314 /// The end as data — `{index, byte}`, the index null outside every
315 /// virtualised row: the shape a `selectionrange` ask carries and a
316 /// binding's `selection_ends` reads back, spelled once.
317 pub fn to_value(self) -> crate::value::Value {
318 use crate::value::Value;
319 Value::map([
320 (
321 "index",
322 self.row.map_or(Value::Null, |r| Value::Int(r as i64)),
323 ),
324 ("byte", Value::Int(self.byte as i64)),
325 ])
326 }
327}
328
329/// What asking for a copy answered (`Core::request_copy`).
330///
331/// The third case is the one this type exists for: a selection can reach
332/// rows a virtual list never built, and the core will not invent them.
333/// It asks the app instead — a `selectionrange`
334/// event on the scope — and the answer arrives later as a clipboard
335/// action, so a copy over a gap is the app's own text rather than a
336/// silent hole in the middle of one.
337#[derive(Clone, Debug, PartialEq, Eq)]
338pub enum CopyRequest {
339 /// The core had all of it; here it is.
340 Ready(String),
341 /// The app was asked and has not answered yet
342 /// (`Core::answer_selection_range`).
343 Asked,
344 /// Nothing is selected.
345 Nothing,
346}
347
348/// The span the press itself selected — the word a double click took, the
349/// run or row a triple click took — which both ends of the drag round
350/// outwards to. Two shapes because the two kinds of scope address
351/// themselves differently, and a drag is only ever in one of them.
352#[derive(Clone, Copy, Debug, PartialEq, Eq)]
353pub(crate) enum DragAnchor {
354 /// `(node, from, to)`, in that node's own bytes.
355 Bytes(Key, usize, usize),
356 /// `(line, from, to)`: a half-open column range on one absolute line
357 /// of a `cells` grid.
358 Cells(u64, usize, usize),
359}
360
361/// A drag-select in flight: which scope it is in, what it moves by, and
362/// the span the press itself selected — the word a double click took, the
363/// run a triple click took — which both ends round outwards to.
364#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365pub(crate) struct SelectDrag {
366 pub scope: Key,
367 pub grain: Grain,
368 /// `None` for a character drag, which has nothing to round to.
369 pub anchor: Option<DragAnchor>,
370}
371
372/// Where one text node's content sits in a selection: the two ends
373/// resolved against *this* node, in its own bytes.
374///
375/// Resolved by ordinal rather than by a running byte offset, because an
376/// ordinal is knowable while the frame is still being emitted and a
377/// global offset is not — the runs after this one have not been placed
378/// yet, and the anchor may be one of them.
379#[derive(Clone, Copy, Debug, PartialEq, Eq)]
380pub(crate) struct Ends {
381 /// Ordinal of the earlier end's node within the scope, and the byte
382 /// inside it.
383 pub start: (u32, usize),
384 pub end: (u32, usize),
385}
386
387impl Ends {
388 /// The two ends in reading order: by ordinal, and by byte within one
389 /// node.
390 pub(crate) fn ordered(a: (u32, usize), b: (u32, usize)) -> Self {
391 if a <= b {
392 Self { start: a, end: b }
393 } else {
394 Self { start: b, end: a }
395 }
396 }
397
398 /// The byte range selected in the node at ordinal `ord`, whose own
399 /// content is `len` bytes. `None` when the node is outside the
400 /// selection entirely.
401 pub(crate) fn range_in(&self, ord: u32, len: usize) -> Option<(usize, usize)> {
402 clip_to_unit(self.start, self.end, ord, len)
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 fn cells(a: (u64, usize), b: (u64, usize)) -> CellSelection {
411 CellSelection::new(Key::ROOT, CellEnd::new(a.0, a.1), CellEnd::new(b.0, b.1))
412 }
413
414 #[test]
415 fn a_linewise_cell_selection_runs_edge_to_edge_in_the_middle() {
416 let s = cells((10, 3), (12, 5));
417 assert_eq!(s.cols_on(10, 80), Some((3, 80)));
418 assert_eq!(s.cols_on(11, 80), Some((0, 80)));
419 assert_eq!(s.cols_on(12, 80), Some((0, 5)));
420 assert_eq!(s.cols_on(13, 80), None);
421 assert_eq!(s.cols_on(9, 80), None);
422 }
423
424 #[test]
425 fn a_block_selection_is_the_same_columns_on_every_line() {
426 let s = cells((10, 6), (12, 2)).block(true);
427 for line in 10..=12 {
428 assert_eq!(s.cols_on(line, 80), Some((2, 6)));
429 }
430 assert_eq!(s.cols_on(13, 80), None);
431 }
432
433 #[test]
434 fn a_backwards_drag_selects_the_same_thing() {
435 assert_eq!(
436 cells((12, 5), (10, 3)).ordered(),
437 cells((10, 3), (12, 5)).ordered()
438 );
439 }
440
441 #[test]
442 fn ends_order_by_ordinal_then_byte() {
443 let e = Ends::ordered((2, 5), (0, 9));
444 assert_eq!(e.start, (0, 9));
445 assert_eq!(e.end, (2, 5));
446 let same = Ends::ordered((1, 7), (1, 2));
447 assert_eq!(same.start, (1, 2));
448 assert_eq!(same.end, (1, 7));
449 }
450
451 #[test]
452 fn a_middle_node_is_selected_whole() {
453 let e = Ends::ordered((0, 3), (2, 4));
454 assert_eq!(e.range_in(1, 10), Some((0, 10)));
455 assert_eq!(e.range_in(0, 10), Some((3, 10)));
456 assert_eq!(e.range_in(2, 10), Some((0, 4)));
457 assert_eq!(e.range_in(3, 10), None);
458 }
459
460 #[test]
461 fn an_empty_range_selects_nothing() {
462 // Both ends in one node, at the same byte: a click, not a drag.
463 let e = Ends::ordered((1, 4), (1, 4));
464 assert_eq!(e.range_in(1, 10), None);
465 }
466
467 #[test]
468 fn ends_clamp_to_the_content_they_land_in() {
469 // The node shrank since the address was taken.
470 let e = Ends::ordered((0, 2), (0, 99));
471 assert_eq!(e.range_in(0, 5), Some((2, 5)));
472 }
473}