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 (`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}