Skip to main content

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}