kui-core 0.1.0-alpha.35

kui contract: flat per-frame tree, clay-style flex layout, text stack, events as data, quad display list
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
//! The window's text selection outside an editor: what a `selectable`
//! node scopes, what a press-drag across it produces, and what a copy
//! reads.
//!
//! A view opts text in with `NodeSpec::selectable` on a container; the
//! core then handles the drag, Shift-arrows, double and triple clicks and
//! Select All. An app reads the result with `Core::selection` (a
//! [`Selection`]) or `Core::cell_selection` (a [`CellSelection`] inside a
//! `cells` grid), and asks for the text with `Core::request_copy`, which
//! answers a [`CopyRequest`]. There is one selection per window, and an
//! editor's own selection is the other half of that: starting one clears
//! the other.
//!
//! A selection is two addresses, a node key and a byte inside that node's
//! own text, never an index into a frame's vectors. A frame that no longer
//! builds the node resolves it to nothing; a frame that builds it again
//! resolves it again.
//!
//! ```rust
//! use kui_core::{CellEnd, CellSelection, Endpoint, Key, Selection};
//!
//! // A drag from byte 3 of one label back to byte 1 of an earlier one.
//! let scope = Key::ROOT.str("card");
//! let sel = Selection::new(
//!     scope,
//!     Endpoint::new(scope.str("second"), 3),
//!     Endpoint::new(scope.str("first"), 1),
//! );
//! assert!(!sel.is_empty());
//!
//! // Inside a terminal grid, ends are absolute lines and columns.
//! let grid = CellSelection::new(Key::ROOT.str("term"), CellEnd::new(10, 3), CellEnd::new(12, 5));
//! assert_eq!(grid.cols_on(11, 80), Some((0, 80))); // a middle line, edge to edge
//! assert_eq!(grid.block(true).cols_on(11, 80), Some((3, 5))); // a block: same columns
//! ```

use crate::color::Color;
use crate::key::Key;

/// What a selection is painted under when nothing else says — the dark
/// base's tint, and what kui painted before there were themes. The live
/// value is `theme.selection`, which both a `selectable` scope and an
/// editor read, because a selection over a label and one over a field
/// sitting side by side must not be two different blues. This constant
/// stays as the floor an [`crate::edit::EditOptions`] the core never
/// stamped falls back to.
pub const TINT: Color = Color {
    r: 0x3b as f32 / 255.0,
    g: 0x5b as f32 / 255.0,
    b: 0xd4 as f32 / 255.0,
    a: 0x66 as f32 / 255.0,
};

/// A `byte` that means "the end of the row, whatever its length": what a
/// Select All puts on the last row of a `selectable` virtual list the
/// frame did not build, since the core never laid that row out and cannot
/// know where it ends. A `selectionrange` ask carries it as written, past
/// any row's length, and the app cuts it to the row. `u32::MAX` rather
/// than `usize::MAX` so it survives a wire that spells bytes as numbers.
pub const ROW_END: usize = u32::MAX as usize;

/// One end of a selection: the node whose text it lands in, and a byte
/// offset into *that node's* content (not into the scope's).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Endpoint {
    pub node: Key,
    pub byte: usize,
    /// The data index of the virtualised row this end is in, when it is in
    /// one (`open_indexed`). Recorded when the end is made, and the only
    /// thing that can place it once its row stops being built: a key says
    /// *which* node, an index says *where in the data* — and a frame that
    /// never built the node can still answer the second question.
    pub row: Option<u64>,
}

impl Endpoint {
    pub fn new(node: Key, byte: usize) -> Self {
        Self {
            node,
            byte,
            row: None,
        }
    }

    /// The same end, in the virtualised row `row`.
    pub fn in_row(mut self, row: Option<u64>) -> Self {
        self.row = row;
        self
    }
}

/// The window's selection: a scope and two ends of it. `anchor` is where
/// the press landed and `focus` is where the pointer is now, so the pair
/// is *directed* — dragging back past the anchor selects the other way
/// without the two swapping, which is what keeps a drag from feeling like
/// it jumps when it crosses its own start.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Selection {
    /// The `selectable` node the selection lives inside.
    pub scope: Key,
    pub anchor: Endpoint,
    pub focus: Endpoint,
}

impl Selection {
    pub fn new(scope: Key, anchor: Endpoint, focus: Endpoint) -> Self {
        Self {
            scope,
            anchor,
            focus,
        }
    }

    /// A selection of no text — a click that placed both ends together.
    /// It still exists (the scope is where the next Shift-click or drag
    /// extends from), and it copies nothing.
    pub fn is_empty(&self) -> bool {
        self.anchor == self.focus
    }
}

/// What a drag-select moves by. A press sets it from the click count the
/// driver counted, the way every text UI does: one click drags by
/// characters, two by words, three by whole runs.
///
/// The unit is not just a rounding of the live end — the *anchor* rounds
/// too, and outwards. A double-click-drag that turns back on itself keeps
/// the word it started in whole, which is what makes the gesture feel
/// like it is selecting words rather than snapping to them.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Grain {
    #[default]
    Char,
    Word,
    /// One text node's whole content — a label, a paragraph. cosmic-text
    /// calls this a line; here a run is the thing a triple click takes.
    /// In a `cells` grid it is one whole row, edge to edge, which is what
    /// a triple click takes in every terminal.
    Run,
}

impl Grain {
    /// The grain a press arms, from the click count the driver counted:
    /// one click (or none counted) a character, two the word under it,
    /// three or more the whole run.
    pub(crate) fn of_clicks(clicks: u8) -> Self {
        match clicks {
            0 | 1 => Grain::Char,
            2 => Grain::Word,
            _ => Grain::Run,
        }
    }
}

/// One end of a selection in a cell grid: an *absolute* line (the grid's
/// `origin_line` plus the row) and a column. Absolute because a grid is
/// one screenful of an app's own history, so a row number means a
/// different line after every scroll.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct CellEnd {
    pub line: u64,
    pub col: usize,
}

impl CellEnd {
    pub fn new(line: u64, col: usize) -> Self {
        Self { line, col }
    }
}

/// A selection inside one `cells` grid.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CellSelection {
    /// The grid it lives in.
    pub node: Key,
    pub anchor: CellEnd,
    pub focus: CellEnd,
    /// Rectangular rather than linewise: the column range is the same on
    /// every line, which is how a terminal selects a column of output.
    /// Held by a modifier while dragging, the way every terminal does it.
    pub block: bool,
}

impl CellSelection {
    pub fn new(node: Key, anchor: CellEnd, focus: CellEnd) -> Self {
        Self {
            node,
            anchor,
            focus,
            block: false,
        }
    }

    pub fn block(mut self, on: bool) -> Self {
        self.block = on;
        self
    }

    pub fn is_empty(&self) -> bool {
        self.anchor == self.focus
    }

    /// The selection as data — `{node, anchor: {line, col}, focus: {line,
    /// col}, block}`, the ends as the drag made them (directed, like
    /// [`RangeEnd::to_value`]'s) and the lines absolute: the shape a
    /// binding's `cell_selection` reads back, spelled once.
    pub fn to_value(self, handles: crate::value::Handles) -> crate::value::Value {
        use crate::value::Value;
        let end = |e: CellEnd| {
            Value::map([
                ("line", Value::Int(e.line as i64)),
                ("col", Value::Int(e.col as i64)),
            ])
        };
        Value::map([
            ("node", (handles.key)(self.node)),
            ("anchor", end(self.anchor)),
            ("focus", end(self.focus)),
            ("block", Value::Bool(self.block)),
        ])
    }

    /// The two ends in reading order.
    pub fn ordered(&self) -> (CellEnd, CellEnd) {
        if self.anchor <= self.focus {
            (self.anchor, self.focus)
        } else {
            (self.focus, self.anchor)
        }
    }

    /// The columns selected on `line`, as a half-open range, or `None`
    /// when the line is outside the selection. Linewise by default — a
    /// line in the middle runs edge to edge, which `cols` gives — and a
    /// block selection is the same column range on every line it covers.
    pub fn cols_on(&self, line: u64, cols: usize) -> Option<(usize, usize)> {
        let (a, b) = self.ordered();
        if self.block {
            if line < a.line || line > b.line {
                return None;
            }
            let (lo, hi) = (a.col.min(b.col), a.col.max(b.col));
            return (lo < hi).then_some((lo.min(cols), hi.min(cols)));
        }
        clip_to_unit((a.line, a.col), (b.line, b.col), line, cols)
    }
}

/// The part of one unit — a text node of `len` bytes, a grid row of `len`
/// columns — that a selection running from `start` to `end` covers, as a
/// half-open range in that unit's own offsets. `None` when the unit is
/// outside the selection. The unit is named by whatever orders the
/// scope's units (an ordinal, an absolute line); the arithmetic is the
/// same for both geometries: the
/// first unit runs from the start's offset, the last to the end's, and
/// every unit between runs edge to edge.
pub(crate) fn clip_to_unit<U: Ord + Copy>(
    start: (U, usize),
    end: (U, usize),
    unit: U,
    len: usize,
) -> Option<(usize, usize)> {
    if unit < start.0 || unit > end.0 {
        return None;
    }
    let from = if unit == start.0 { start.1 } else { 0 };
    let to = if unit == end.0 { end.1 } else { len };
    let (from, to) = (from.min(len), to.min(len));
    (from < to).then_some((from, to))
}

/// The edges a grained drag runs between: the anchor's unit and the live
/// end's unit are each `(from, to)`, and which side of the anchor the
/// live end is on decides which edge of each the selection takes —
/// backwards, from the far edge of the anchor's unit to the near edge of
/// the live one; forwards, the reverse. So the unit the press took stays
/// whole however far back over itself the drag turns, in bytes or in
/// cells alike. Answers `(anchor edge, live edge)`.
pub(crate) fn grained_edges(
    anchor: (usize, usize),
    live: (usize, usize),
    backwards: bool,
) -> (usize, usize) {
    if backwards {
        (anchor.1, live.0)
    } else {
        (anchor.0, live.1)
    }
}

/// Where a selection end in a virtualised row the frame did not build
/// sits against the rows it did: after every built run of the scope iff
/// its row is past the last built row that carries one (`last`), before
/// them otherwise — below the first row, or in a hole, which a contiguous
/// virtual window does not have. The one rule the highlight paints by
/// (`resolve_selection`) and a `selectionrange` ask orders by
/// (`selection_range`). With no built row to compare against, nothing is
/// after: the start is the honest boundary.
pub(crate) fn unbuilt_row_is_after(row: u64, last: Option<u64>) -> bool {
    last.is_some_and(|hi| row > hi)
}

/// One end of the range an app is asked to fill in
/// (`Core::selection_range`): the data index of the row it is in, and the
/// byte inside that row's own text. An end outside every virtualised row
/// has no index — it is text the core built and can answer for itself.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RangeEnd {
    pub row: Option<u64>,
    pub byte: usize,
}

impl RangeEnd {
    /// The end as data — `{index, byte}`, the index null outside every
    /// virtualised row: the shape a `selectionrange` ask carries and a
    /// binding's `selection_ends` reads back, spelled once.
    pub fn to_value(self) -> crate::value::Value {
        use crate::value::Value;
        Value::map([
            (
                "index",
                self.row.map_or(Value::Null, |r| Value::Int(r as i64)),
            ),
            ("byte", Value::Int(self.byte as i64)),
        ])
    }
}

/// What asking for a copy answered (`Core::request_copy`).
///
/// The third case is the one this type exists for: a selection can reach
/// rows a virtual list never built, and the core will not invent them.
/// It asks the app instead — a `selectionrange`
/// event on the scope — and the answer arrives later as a clipboard
/// action, so a copy over a gap is the app's own text rather than a
/// silent hole in the middle of one.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CopyRequest {
    /// The core had all of it; here it is.
    Ready(String),
    /// The app was asked and has not answered yet
    /// (`Core::answer_selection_range`).
    Asked,
    /// Nothing is selected.
    Nothing,
}

/// The span the press itself selected — the word a double click took, the
/// run or row a triple click took — which both ends of the drag round
/// outwards to. Two shapes because the two kinds of scope address
/// themselves differently, and a drag is only ever in one of them.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum DragAnchor {
    /// `(node, from, to)`, in that node's own bytes.
    Bytes(Key, usize, usize),
    /// `(line, from, to)`: a half-open column range on one absolute line
    /// of a `cells` grid.
    Cells(u64, usize, usize),
}

/// A drag-select in flight: which scope it is in, what it moves by, and
/// the span the press itself selected — the word a double click took, the
/// run a triple click took — which both ends round outwards to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct SelectDrag {
    pub scope: Key,
    pub grain: Grain,
    /// `None` for a character drag, which has nothing to round to.
    pub anchor: Option<DragAnchor>,
}

/// Where one text node's content sits in a selection: the two ends
/// resolved against *this* node, in its own bytes.
///
/// Resolved by ordinal rather than by a running byte offset, because an
/// ordinal is knowable while the frame is still being emitted and a
/// global offset is not — the runs after this one have not been placed
/// yet, and the anchor may be one of them.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Ends {
    /// Ordinal of the earlier end's node within the scope, and the byte
    /// inside it.
    pub start: (u32, usize),
    pub end: (u32, usize),
}

impl Ends {
    /// The two ends in reading order: by ordinal, and by byte within one
    /// node.
    pub(crate) fn ordered(a: (u32, usize), b: (u32, usize)) -> Self {
        if a <= b {
            Self { start: a, end: b }
        } else {
            Self { start: b, end: a }
        }
    }

    /// The byte range selected in the node at ordinal `ord`, whose own
    /// content is `len` bytes. `None` when the node is outside the
    /// selection entirely.
    pub(crate) fn range_in(&self, ord: u32, len: usize) -> Option<(usize, usize)> {
        clip_to_unit(self.start, self.end, ord, len)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn cells(a: (u64, usize), b: (u64, usize)) -> CellSelection {
        CellSelection::new(Key::ROOT, CellEnd::new(a.0, a.1), CellEnd::new(b.0, b.1))
    }

    #[test]
    fn a_linewise_cell_selection_runs_edge_to_edge_in_the_middle() {
        let s = cells((10, 3), (12, 5));
        assert_eq!(s.cols_on(10, 80), Some((3, 80)));
        assert_eq!(s.cols_on(11, 80), Some((0, 80)));
        assert_eq!(s.cols_on(12, 80), Some((0, 5)));
        assert_eq!(s.cols_on(13, 80), None);
        assert_eq!(s.cols_on(9, 80), None);
    }

    #[test]
    fn a_block_selection_is_the_same_columns_on_every_line() {
        let s = cells((10, 6), (12, 2)).block(true);
        for line in 10..=12 {
            assert_eq!(s.cols_on(line, 80), Some((2, 6)));
        }
        assert_eq!(s.cols_on(13, 80), None);
    }

    #[test]
    fn a_backwards_drag_selects_the_same_thing() {
        assert_eq!(
            cells((12, 5), (10, 3)).ordered(),
            cells((10, 3), (12, 5)).ordered()
        );
    }

    #[test]
    fn ends_order_by_ordinal_then_byte() {
        let e = Ends::ordered((2, 5), (0, 9));
        assert_eq!(e.start, (0, 9));
        assert_eq!(e.end, (2, 5));
        let same = Ends::ordered((1, 7), (1, 2));
        assert_eq!(same.start, (1, 2));
        assert_eq!(same.end, (1, 7));
    }

    #[test]
    fn a_middle_node_is_selected_whole() {
        let e = Ends::ordered((0, 3), (2, 4));
        assert_eq!(e.range_in(1, 10), Some((0, 10)));
        assert_eq!(e.range_in(0, 10), Some((3, 10)));
        assert_eq!(e.range_in(2, 10), Some((0, 4)));
        assert_eq!(e.range_in(3, 10), None);
    }

    #[test]
    fn an_empty_range_selects_nothing() {
        // Both ends in one node, at the same byte: a click, not a drag.
        let e = Ends::ordered((1, 4), (1, 4));
        assert_eq!(e.range_in(1, 10), None);
    }

    #[test]
    fn ends_clamp_to_the_content_they_land_in() {
        // The node shrank since the address was taken.
        let e = Ends::ordered((0, 2), (0, 99));
        assert_eq!(e.range_in(0, 5), Some((2, 5)));
    }
}