slate-framework 1.0.1

GPU-accelerated Rust UI framework — umbrella crate
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
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
//! TextArea layout construction + pointer/selection geometry.
//!
//! [`build_layout`] shapes the document once and fits it to a fixed content
//! width, then floors the element height to `min_lines` rows so an empty or
//! short field still reserves space. The expensive shaping happens here;
//! `paint` reuses the returned [`MultilineLayout`] for glyph placement and
//! caret math — no re-shaping per frame.
//!
//! [`byte_at_point`] and [`selection_rects`] are the paint/handler-side
//! geometry that turns a window-relative pointer position into a caret byte and
//! a selection range into per-visual-line highlight rectangles. Both read the
//! cached [`MultilineLayout`] and the painted origin; neither re-shapes.

use std::rc::Rc;

use slate_text::{MultilineLayout, ShapedLine, TextError};

use crate::text_system::{PlatformFont, TextSystem};

/// Build the wrapped multi-line layout for `text` at `width_lpx`, returning the
/// shared layout plus the element height floored to `min_lines` rows.
///
/// `min_lines` of 0 is treated as 1 so the floor never collapses the field to
/// zero height (a layout always has ≥ 1 visual line).
pub(crate) fn build_layout(
    text_system: &TextSystem,
    font: &PlatformFont,
    text: &str,
    width_lpx: f32,
    min_lines: usize,
) -> Result<(Rc<MultilineLayout>, f32), TextError> {
    let doc = text_system.shape_document(font, text)?;
    let layout = slate_text::wrap_document(&doc, width_lpx);

    let floor_rows = min_lines.max(1) as f32;
    let height = layout
        .total_height_lpx
        .max(floor_rows * layout.line_height_lpx);

    Ok((Rc::new(layout), height))
}

/// Map a window-relative pointer `(x, y)` to an absolute caret byte offset.
///
/// `paint_origin_{x,y}` are the element's painted top-left (cached on
/// `ImeState` during paint). The y coordinate selects a visual line by the
/// uniform line height; the x coordinate is resolved within that line by
/// [`MultilineLayout::byte_at_line_x`] (clamped to the line's addressable
/// range). Clicks above the first line clamp to byte 0; clicks below the last
/// line clamp to the document end. Returns 0 for an empty layout.
pub(crate) fn byte_at_point(
    layout: &MultilineLayout,
    text: &str,
    paint_origin_x: f32,
    paint_origin_y: f32,
    x: f32,
    y: f32,
) -> usize {
    if layout.lines.is_empty() {
        return 0;
    }
    let local_y = y - paint_origin_y;
    if local_y < 0.0 {
        return 0; // above the first line → document start
    }
    if local_y >= layout.total_height_lpx {
        // below the last line → document end (last line's caret-addressable end)
        let last = layout.lines.len() - 1;
        return layout.line_caret_end(text, last);
    }

    let lh = layout.line_height_lpx;
    let line_idx = if lh > 0.0 {
        ((local_y / lh) as usize).min(layout.lines.len() - 1)
    } else {
        0
    };
    let local_x = x - paint_origin_x;
    layout.byte_at_line_x(text, line_idx, local_x)
}

/// One selection highlight rectangle, element-relative (add the paint origin at
/// draw time). `width_lpx` is already clamped to ≥ 0.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct SelectionRect {
    /// Left edge, relative to the element's painted x-origin.
    pub x_lpx: f32,
    /// Top edge, relative to the element's painted y-origin (the line's
    /// `y_offset_lpx`).
    pub y_lpx: f32,
    /// Rectangle width in logical pixels.
    pub width_lpx: f32,
}

/// Build one highlight rect per visual line intersecting the byte range
/// `[lo, hi)`.
///
/// - The line where the selection starts mid-way runs from the start byte's x
///   to the line's text end (`width_lpx`).
/// - A line fully covered (selection spans from at-or-before its `byte_start`
///   to at-or-after its `byte_end`) extends to `content_width` so the wrapped
///   break / newline reads as selected.
/// - The line where the selection ends mid-way runs from the line start to the
///   end byte's x.
///
/// Empty selections (`lo >= hi`) and zero-width rects produce nothing.
pub(crate) fn selection_rects(
    layout: &MultilineLayout,
    lo: usize,
    hi: usize,
    content_width: f32,
) -> Vec<SelectionRect> {
    let mut rects = Vec::new();
    if lo >= hi {
        return rects;
    }

    for vline in &layout.lines {
        // Skip lines entirely outside the selection. A line whose byte_start
        // equals hi (the start of the line after the selection's end) is
        // excluded — nothing on it is selected.
        if vline.byte_end <= lo || vline.byte_start >= hi {
            continue;
        }

        // Mixed / RTL line: the logical selection range maps to one contiguous
        // x-span per level-run, so emit a rect per run intersection. (The full-
        // width wrap signal below is an LTR-only nicety; run-aware lines paint
        // the precise glyph coverage instead.)
        if !vline.line.runs.is_empty() {
            let line_lo = lo.max(vline.byte_start);
            let line_hi = hi.min(vline.byte_end);
            for (x_start, width) in slate_text::run_selection_rects(&vline.line, line_lo, line_hi) {
                rects.push(SelectionRect {
                    x_lpx: x_start,
                    y_lpx: vline.line.y_offset_lpx,
                    width_lpx: width,
                });
            }
            continue;
        }

        let line_lo = lo.max(vline.byte_start);
        let x_start = line_x_at_byte(&vline.line, line_lo);

        let starts_at_head = lo <= vline.byte_start;
        let covers_to_end = hi >= vline.byte_end;
        let x_end = if starts_at_head && covers_to_end {
            // Fully-covered interior line → full content width signals the wrap.
            content_width
        } else {
            // Partial line: clamp the end byte to this line's coverage.
            line_x_at_byte(&vline.line, hi.min(vline.byte_end))
        };

        let width = (x_end - x_start).max(0.0);
        if width > 0.0 {
            rects.push(SelectionRect {
                x_lpx: x_start,
                y_lpx: vline.line.y_offset_lpx,
                width_lpx: width,
            });
        }
    }
    rects
}

/// Build the IME display string by splicing the active composition into the
/// committed text at the caret: `committed[..caret] + preedit + committed[caret..]`.
///
/// `caret` is clamped to `committed.len()` and floored to the nearest char
/// boundary at or below it, so a caret that momentarily indexes mid-codepoint
/// (or past the end) never panics the splice. An empty `preedit` short-circuits
/// to a clone of `committed` — paint uses the pre-shaped layout in that case.
pub(crate) fn compose_display(committed: &str, caret: usize, preedit: &str) -> String {
    if preedit.is_empty() {
        return committed.to_string();
    }
    let mut at = caret.min(committed.len());
    while at > 0 && !committed.is_char_boundary(at) {
        at -= 1;
    }
    let mut out = String::with_capacity(committed.len() + preedit.len());
    out.push_str(&committed[..at]);
    out.push_str(preedit);
    out.push_str(&committed[at..]);
    out
}

/// One preedit run on a single visual line: the line it sits on plus the
/// element-relative x and width of the composed bytes covered on that line.
/// Paint resolves the y (underline below baseline, highlight at line top) from
/// the line's own metrics, so this carries only the horizontal geometry.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct PreeditRun {
    /// Index into `layout.lines`.
    pub line_idx: usize,
    /// Left edge of the run, relative to the element's painted x-origin.
    pub x_lpx: f32,
    /// Run width in logical pixels (hugs the composed glyphs, never extends to
    /// the content edge the way a wrapped *selection* rect does).
    pub width_lpx: f32,
}

/// Split the composed byte range `[start, hi)` of a preedit run into one
/// per-visual-line run, hugging the glyphs actually on each line.
///
/// Unlike [`selection_rects`], a fully-covered interior line is **not** widened
/// to `content_width`: an IME underline / target highlight tracks the composed
/// text, not the wrap fill. Empty ranges and zero-width runs produce nothing.
pub(crate) fn preedit_runs(layout: &MultilineLayout, start: usize, end: usize) -> Vec<PreeditRun> {
    let mut runs = Vec::new();
    if start >= end {
        return runs;
    }
    for (idx, vline) in layout.lines.iter().enumerate() {
        if vline.byte_end <= start || vline.byte_start >= end {
            continue;
        }
        let lo = start.max(vline.byte_start);
        let hi = end.min(vline.byte_end);
        let x_start = line_x_at_byte(&vline.line, lo);
        let x_end = line_x_at_byte(&vline.line, hi);
        let width = (x_end - x_start).max(0.0);
        if width > 0.0 {
            runs.push(PreeditRun {
                line_idx: idx,
                x_lpx: x_start,
                width_lpx: width,
            });
        }
    }
    runs
}

/// Pen-x of the leading edge of the cluster at absolute `byte` on `line`:
/// the summed advance of every glyph whose (document-absolute) cluster is
/// strictly before `byte`. Returns the line width for a byte past the last
/// glyph. Mirrors `MultilineLayout::caret_position`'s pen walk.
fn line_x_at_byte(line: &ShapedLine, byte: usize) -> f32 {
    // Run-bearing line (mixed / RTL): defer to the run-aware caret math (`byte`
    // is a caret / selection boundary, so the grapheme snap is skipped).
    if !line.runs.is_empty() {
        return slate_text::run_caret_x_at(line, byte);
    }
    let mut pen = 0.0f32;
    for g in &line.glyphs {
        if g.cluster as usize >= byte {
            return pen;
        }
        pen += g.x_advance_lpx;
    }
    line.width_lpx
}

#[cfg(test)]
mod tests {
    use super::*;
    use slate_text::{Direction, FontHandle, FontId, ShapedGlyph, VisualLine};

    fn glyph(cluster: u32, adv: f32) -> ShapedGlyph {
        ShapedGlyph {
            glyph_id: 1,
            font_id: FontId::PRIMARY,
            font_handle: FontHandle::default(),
            x_advance_lpx: adv,
            position_lpx: [0.0, 0.0],
            cluster,
            direction: Direction::Ltr,
        }
    }

    fn vline(glyphs: Vec<ShapedGlyph>, byte_start: usize, byte_end: usize, y: f32) -> VisualLine {
        let width: f32 = glyphs.iter().map(|g| g.x_advance_lpx).sum();
        VisualLine {
            line: ShapedLine {
                glyphs,
                width_lpx: width,
                ascent_lpx: 10.0,
                descent_lpx: -2.0,
                y_offset_lpx: y,
                base_direction: Direction::Ltr,
                runs: Vec::new(),
            },
            byte_start,
            byte_end,
        }
    }

    /// "ab\ncd": line0 "ab" (clusters 0/1, adv 5/6, width 11, covers the '\n' so
    /// byte_end=3), line1 "cd" (clusters 3/4, adv 7/8, width 15). line_height 12.
    fn layout_abcd() -> MultilineLayout {
        MultilineLayout {
            lines: vec![
                vline(vec![glyph(0, 5.0), glyph(1, 6.0)], 0, 3, 0.0),
                vline(vec![glyph(3, 7.0), glyph(4, 8.0)], 3, 5, 12.0),
            ],
            total_height_lpx: 24.0,
            line_height_lpx: 12.0,
        }
    }

    /// Three lines of three/two/three clusters at advance 10, content gaps
    /// folded into each line's byte_end (so ranges tile 0..10):
    /// line0 "abc" 0..4 (covers '\n'), line1 "de" 4..7, line2 "fgh" 7..10.
    fn layout_3lines() -> MultilineLayout {
        MultilineLayout {
            lines: vec![
                vline(
                    vec![glyph(0, 10.0), glyph(1, 10.0), glyph(2, 10.0)],
                    0,
                    4,
                    0.0,
                ),
                vline(vec![glyph(4, 10.0), glyph(5, 10.0)], 4, 7, 12.0),
                vline(
                    vec![glyph(7, 10.0), glyph(8, 10.0), glyph(9, 10.0)],
                    7,
                    10,
                    24.0,
                ),
            ],
            total_height_lpx: 36.0,
            line_height_lpx: 12.0,
        }
    }

    #[test]
    fn byte_at_point_resolves_line_and_column() {
        let text = "ab\ncd";
        let l = layout_abcd();
        // Paint origin at (100, 50). A point in line0's y band, x near 'b' edge.
        // line0 'a' adv5 'b' adv6 → caret-x for byte 2 is 11; clicking past that
        // (local_x ≈ 11) snaps to line0 caret-end (byte 2).
        assert_eq!(
            byte_at_point(&l, text, 100.0, 50.0, 100.0 + 20.0, 50.0 + 3.0),
            2
        );
        // y in line0 band, far-left x → line0 start (byte 0).
        assert_eq!(
            byte_at_point(&l, text, 100.0, 50.0, 100.0 - 5.0, 50.0 + 3.0),
            0
        );
        // y in line1 band → a byte on line1 (≥ 3).
        let b = byte_at_point(&l, text, 100.0, 50.0, 100.0 + 1.0, 50.0 + 15.0);
        assert_eq!(b, 3, "left edge of line1 → its byte_start");
    }

    #[test]
    fn byte_at_point_clamps_y_out_of_band() {
        let text = "ab\ncd";
        let l = layout_abcd();
        // y past total height → document end (byte 5).
        assert_eq!(
            byte_at_point(&l, text, 100.0, 50.0, 100.0 + 100.0, 50.0 + 999.0),
            5
        );
        // Negative y (above first line) → 0.
        assert_eq!(
            byte_at_point(&l, text, 100.0, 50.0, 100.0 + 100.0, 50.0 - 999.0),
            0
        );
    }

    #[test]
    fn byte_at_point_line_boundary_y_picks_lower_line() {
        let text = "ab\ncd";
        let l = layout_abcd();
        // local_y exactly == line_height (12) is the top of line1 → resolve to
        // line1 (consistent with the single no-affinity rule). x far-left → 3.
        assert_eq!(byte_at_point(&l, text, 0.0, 0.0, -5.0, 12.0), 3);
    }

    #[test]
    fn selection_rects_three_line_span() {
        // Select bytes 1..9 across all three lines.
        let l = layout_3lines();
        let rects = selection_rects(&l, 1, 9, 100.0);
        assert_eq!(rects.len(), 3, "one rect per spanned line");
        // line0: starts mid-line at byte1 (x=10) → to line text end (width 30).
        assert_eq!(
            rects[0],
            SelectionRect {
                x_lpx: 10.0,
                y_lpx: 0.0,
                width_lpx: 20.0
            }
        );
        // line1: fully covered interior → full content width (100) from x=0.
        assert_eq!(
            rects[1],
            SelectionRect {
                x_lpx: 0.0,
                y_lpx: 12.0,
                width_lpx: 100.0
            }
        );
        // line2: from line start (x=0) to byte9 (x=20).
        assert_eq!(
            rects[2],
            SelectionRect {
                x_lpx: 0.0,
                y_lpx: 24.0,
                width_lpx: 20.0
            }
        );
    }

    #[test]
    fn selection_rects_single_line() {
        // "abc" single-line parity with the TextField path: one rect lo_x..hi_x.
        let l = MultilineLayout {
            lines: vec![vline(
                vec![
                    glyph(0, 10.0),
                    glyph(1, 10.0),
                    glyph(2, 10.0),
                    glyph(3, 10.0),
                ],
                0,
                4,
                0.0,
            )],
            total_height_lpx: 12.0,
            line_height_lpx: 12.0,
        };
        let rects = selection_rects(&l, 1, 3, 100.0);
        assert_eq!(rects.len(), 1);
        assert_eq!(
            rects[0],
            SelectionRect {
                x_lpx: 10.0,
                y_lpx: 0.0,
                width_lpx: 20.0
            }
        );
    }

    #[test]
    fn selection_rects_normalized_range_is_direction_agnostic() {
        // The caller normalizes to (lo, hi); a right-to-left drag yields the same
        // (lo, hi) and therefore identical rects.
        let l = layout_3lines();
        let ltr = selection_rects(&l, 1, 9, 100.0);
        let rtl = selection_rects(&l, 1, 9, 100.0);
        assert_eq!(ltr, rtl);
    }

    #[test]
    fn selection_rects_empty_selection_is_zero_rects() {
        let l = layout_3lines();
        assert!(selection_rects(&l, 4, 4, 100.0).is_empty());
        assert!(selection_rects(&l, 5, 2, 100.0).is_empty());
    }

    #[test]
    fn compose_display_inserts_at_caret() {
        assert_eq!(compose_display("ab", 0, "XY"), "XYab");
        assert_eq!(compose_display("ab", 1, "XY"), "aXYb");
        assert_eq!(compose_display("ab", 2, "XY"), "abXY");
    }

    #[test]
    fn compose_display_empty_preedit_is_clone() {
        assert_eq!(compose_display("hello", 3, ""), "hello");
        assert_eq!(compose_display("", 0, ""), "");
    }

    #[test]
    fn compose_display_clamps_and_floors_caret() {
        // Caret past end clamps to len.
        assert_eq!(compose_display("ab", 99, "X"), "abX");
        // Caret mid-codepoint floors to the char boundary below it ("é" = 2 bytes).
        assert_eq!(compose_display("é", 1, "X"), "");
    }

    #[test]
    fn compose_display_multibyte_preedit_and_text() {
        // Caret 2 == after "ab"; insert CJK composition.
        assert_eq!(compose_display("ab", 2, "你好"), "ab你好");
        assert_eq!(compose_display("ab", 0, "你好"), "你好ab");
    }

    #[test]
    fn preedit_runs_hug_glyphs_not_content_width() {
        // Select bytes 1..9 across all three lines (same layout selection uses).
        let l = layout_3lines();
        let runs = preedit_runs(&l, 1, 9);
        assert_eq!(runs.len(), 3, "one run per spanned line");
        // line0: byte1 (x=10) .. min(9, byte_end=4) → byte4 maps to width 30.
        assert_eq!(
            runs[0],
            PreeditRun {
                line_idx: 0,
                x_lpx: 10.0,
                width_lpx: 20.0
            }
        );
        // line1 fully covered: hugs glyphs (x=0 .. width 20), NOT content_width(100).
        assert_eq!(
            runs[1],
            PreeditRun {
                line_idx: 1,
                x_lpx: 0.0,
                width_lpx: 20.0
            }
        );
        // line2: x=0 .. byte9 (x=20).
        assert_eq!(
            runs[2],
            PreeditRun {
                line_idx: 2,
                x_lpx: 0.0,
                width_lpx: 20.0
            }
        );
    }

    #[test]
    fn preedit_runs_empty_range_is_none() {
        let l = layout_3lines();
        assert!(preedit_runs(&l, 4, 4).is_empty());
        assert!(preedit_runs(&l, 9, 2).is_empty());
    }

    #[test]
    fn selection_rects_to_line_break_is_full_width() {
        // Selecting "ab\n" (lo=0, hi=3) covers line0 head-to-end → full width.
        let l = layout_abcd();
        let rects = selection_rects(&l, 0, 3, 200.0);
        assert_eq!(rects.len(), 1);
        assert_eq!(
            rects[0],
            SelectionRect {
                x_lpx: 0.0,
                y_lpx: 0.0,
                width_lpx: 200.0
            }
        );
    }
}