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