kui-core 0.1.0-alpha.45

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
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
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
//! A cell grid: a terminal's screen as one node, `rows × cols` cells each
//! with a character, a foreground, a background and attribute bits.
//!
//! Build a slice of [`Cell`]s, describe it with a [`CellGrid`] and hand it
//! to `Ui::cells` with the node's own spec. A glyph is shaped once per
//! character and style variant and then placed at `col × cell_w` without
//! shaping, so a pane whose every cell is new every frame costs the same
//! as one that never changes. The node's rows apply as on any node: an
//! `on_key` makes it the terminal's sink, an `on_click` or `on_drag`
//! carries `cell: {row, col}`, `selectable` selects in cells, and its
//! access row is `terminal`.
//!
//! ```rust
//! use kui_core::cells::{flags, Cell, CellGrid};
//! use kui_core::{CellCursor, Color, Core, NodeSpec, Size, TextStyle};
//!
//! let (rows, cols) = (2, 4);
//! let mut cells = vec![Cell::default(); rows * cols];
//! for (i, ch) in "ab c".chars().enumerate() {
//!     cells[i] = Cell::new(ch, 0xffffffff, 0x0000ffff); // white on blue
//! }
//! cells[6] = Cell::new('x', 0xff0000ff, 0).with(flags::BOLD);
//!
//! let mut core = Core::new();
//! let mut ui = core.frame(Size::new(400.0, 200.0), 1.0);
//! ui.cells(
//!     &CellGrid {
//!         rows,
//!         cols,
//!         cells: &cells,
//!         style: TextStyle::new(14.0).mono().line_height(20.0),
//!         cursor: Some((1, 2, CellCursor::Block, Color::WHITE)),
//!         origin_line: 0,
//!     },
//!     NodeSpec::default(),
//! );
//! ui.finish();
//! ```
//!
//! What it deliberately is not: shaped text. No ligatures, no kerning, no
//! wrapping. A cell is one `char`: a precomposed character is one cell; a
//! base with combining marks, a ZWJ emoji sequence or a flag is not
//! representable, so the app precomposes what NFC can and drops the rest.
//! A wide character is marked [`flags::WIDE`] and the cell after it is a
//! spacer the app leaves blank. Box drawing, block elements and the
//! Powerline separators are not shaped at all but rasterized from the cell
//! box, so a TUI's frames are seamless in any font.

use cosmic_text::{Attrs, Buffer, FontSystem, Shaping, Style as FontStyle};
use rustc_hash::FxHashMap;

use crate::atlas::GlyphAtlas;
use crate::color::Color;
use crate::display::{Clip, ClipId, Quad, QuadKind};
use crate::geom::{Rect, Size, Vec2};
use crate::key::Key;
use crate::resources::Resources;
use crate::spec::TextStyle;
use crate::text::{Raster, glyph_kind, raster_glyph};

mod boxdraw;

/// Bits in [`Cell::flags`].
pub mod flags {
    pub const BOLD: u8 = 1;
    pub const ITALIC: u8 = 2;
    pub const UNDERLINE: u8 = 4;
    pub const STRIKETHROUGH: u8 = 8;
    /// The glyph is two cells wide; the app leaves the next cell blank.
    pub const WIDE: u8 = 16;
    /// The underline is a wave (SGR 4:3, a terminal's undercurl). Implies
    /// `UNDERLINE`.
    pub const WAVY: u8 = 32;
    /// The underline is dotted (SGR 4:4). Implies `UNDERLINE`.
    pub const DOTTED: u8 = 64;
    /// The bits that make a line under or through a cell, and its shape:
    /// what a run of cells has to agree on to share one.
    pub const LINES: u8 = UNDERLINE | STRIKETHROUGH | WAVY | DOTTED;
}

/// One cell: a character, its colours as `0xRRGGBBAA` (a background of 0
/// is none, an underline colour of 0 the foreground's), and attribute
/// bits. Sixteen bytes, so a 200×50 pane is a 160 KB slice a frame.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct Cell {
    pub ch: char,
    pub fg: u32,
    pub bg: u32,
    pub flags: u8,
    /// The underline's own colour (SGR 58), or 0 for `fg`.
    pub ul: u32,
}

impl Cell {
    pub const fn new(ch: char, fg: u32, bg: u32) -> Self {
        Self {
            ch,
            fg,
            bg,
            flags: 0,
            ul: 0,
        }
    }

    /// An underline in its own colour; sets `UNDERLINE`.
    pub const fn underline_color(mut self, ul: u32) -> Self {
        self.flags |= flags::UNDERLINE;
        self.ul = ul;
        self
    }

    pub const fn with(mut self, flags: u8) -> Self {
        self.flags |= flags;
        self
    }
}

/// How the grid's cursor is drawn, in the colour given with it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CursorShape {
    /// The whole cell, painted under its glyph.
    Block,
    /// A two-pixel bar at the cell's left edge.
    Bar,
    /// A two-pixel line along the cell's bottom.
    Underline,
}

impl CursorShape {
    /// Wire order: the index every binding carries (`block`, `bar`,
    /// `underline`; C's `KUI_CELL_CURSOR_*` is this plus one).
    pub const NAMES: &[&str] = &["block", "bar", "underline"];

    pub fn from_index(i: usize) -> Option<Self> {
        match i {
            0 => Some(Self::Block),
            1 => Some(Self::Bar),
            2 => Some(Self::Underline),
            _ => None,
        }
    }

    pub fn from_name(s: &str) -> Option<Self> {
        Self::NAMES
            .iter()
            .position(|n| *n == s)
            .and_then(Self::from_index)
    }
}

/// A grid to draw: the cells in row-major order (`rows × cols` of them;
/// fewer draw as blank), the style the glyphs are shaped in (`size`,
/// `line_height` as the cell height, `family` / `font`), and the cursor.
#[derive(Clone, Copy, Debug)]
pub struct CellGrid<'a> {
    pub rows: usize,
    pub cols: usize,
    pub cells: &'a [Cell],
    pub style: TextStyle,
    /// `(row, col, shape, colour)`.
    pub cursor: Option<(usize, usize, CursorShape, Color)>,
    /// The absolute line number of row 0: where this screenful sits in
    /// the app's own history.
    ///
    /// A grid is one screenful and the scrollback behind it is the app's,
    /// so a row number is not an address: it means a different line after
    /// every scroll. Stamping this makes a selection's ends absolute, and
    /// a terminal that scrolls under a selection keeps it. An app that
    /// never sets it gets 0 and a selection that is correct only while it
    /// does not scroll, which is the honest reading of saying nothing.
    pub origin_line: u64,
}

/// Index into the frame's grid list.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CellsId(pub u32);

struct Entry {
    /// The node that drew it, so a grid stays findable after the frame it
    /// was built in — a view asking about the selection runs while the
    /// next frame's tree is half-built, and the answer is last frame's.
    key: Key,
    rows: usize,
    cols: usize,
    cells: Vec<Cell>,
    style: TextStyle,
    cursor: Option<(usize, usize, CursorShape, Color)>,
    origin_line: u64,
}

/// A glyph placed in a cell: where its raster goes, from the cell's
/// top-left, physical px.
#[derive(Clone, Copy)]
struct CellGlyph {
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    uv: [u32; 4],
    kind: QuadKind,
}

/// The glyphs of one style at one scale: ASCII by direct index in four
/// variants (plain, bold, italic, both), everything else by map.
struct StyleTable {
    cell_w: f32,
    cell_h: f32,
    ascii: Vec<Option<Option<CellGlyph>>>,
    other: FxHashMap<(char, u8), Option<CellGlyph>>,
    /// The style's family by name: a glyph from a face of another family
    /// is a fallback's (`shape_cell`). Read off the family itself, not off
    /// the face its `M` shapes with: a symbols-only or a CJK-only family
    /// has no `M`, and its own glyphs read as a fallback's, asked of a
    /// monospaced face first and centred (backlog RG118).
    family: String,
    /// The atlas stamp the slots were looked up against.
    epoch: u64,
}

const VARIANTS: usize = 4;

fn variant(flags: u8) -> usize {
    (flags & (flags::BOLD | flags::ITALIC)) as usize
}

/// The frame's grids and the glyph tables they draw from. The frame
/// before it is kept too, the way the text store keeps its places: a host
/// that reads the selection from inside its own `view` is asking about a
/// frame that has not been built yet.
pub struct CellStore {
    /// This frame's grids and the frame before's — always kept, the way
    /// the text store keeps its places (`retain::Kept`).
    frame: crate::retain::Kept<Entry>,
    tables: FxHashMap<u64, StyleTable>,
    scale: f32,
}

impl Default for CellStore {
    fn default() -> Self {
        Self::new()
    }
}

impl CellStore {
    pub fn new() -> Self {
        Self {
            frame: Default::default(),
            tables: FxHashMap::default(),
            scale: 1.0,
        }
    }

    /// Drops every style's table, to shape again on its next draw: the
    /// weights a family is asked at changed under them.
    pub(crate) fn forget_shaped(&mut self) {
        self.tables.clear();
    }

    pub(crate) fn begin_frame(&mut self, scale: f32) {
        if (scale - self.scale).abs() > f32::EPSILON {
            self.tables.clear();
        }
        self.scale = scale;
        self.frame.begin(true);
    }

    /// The grid `key` drew, in this frame or the one before it.
    pub(crate) fn find(&self, key: Key, prev: bool) -> Option<CellsId> {
        self.list(prev)
            .iter()
            .position(|e| e.key == key)
            .map(|i| CellsId(i as u32))
    }

    fn list(&self, prev: bool) -> &[Entry] {
        if prev { self.frame.prev() } else { &self.frame }
    }

    fn entry(&self, id: CellsId, prev: bool) -> &Entry {
        &self.list(prev)[id.0 as usize]
    }

    pub(crate) fn add(&mut self, key: Key, grid: &CellGrid<'_>) -> CellsId {
        let n = grid.rows * grid.cols;
        let mut cells = Vec::with_capacity(n);
        cells.extend_from_slice(&grid.cells[..grid.cells.len().min(n)]);
        cells.resize(n, Cell::default());
        self.frame.push(Entry {
            key,
            rows: grid.rows,
            cols: grid.cols,
            cells,
            style: grid.style,
            cursor: grid.cursor,
            origin_line: grid.origin_line,
        });
        CellsId((self.frame.len() - 1) as u32)
    }

    fn table_key(style: &TextStyle, scale: f32) -> u64 {
        crate::text::TextSystem::style_key("", style, scale)
    }

    /// The table for `style`, built if this is the first time: the cell
    /// width is `M`'s advance and the cell height the style's line
    /// height, both physical.
    fn table(&mut self, style: &TextStyle, res: &Resources, fs: &mut FontSystem) -> u64 {
        let key = Self::table_key(style, self.scale);
        if !self.tables.contains_key(&key) {
            let scale = self.scale;
            let cell_w = shape_one(style, "M", 0, None, 1.0, res, fs, scale)
                .map_or(style.size * scale * 0.6, |g| g.advance)
                .round()
                .max(1.0);
            self.tables.insert(
                key,
                StyleTable {
                    cell_w,
                    // A pixel at least, as the shaper's (backlog RG136):
                    // an infinite one put every row after the first at
                    // the layout's limit.
                    cell_h: match (style.line_height * scale).round() {
                        h if h.is_finite() => h.max(1.0),
                        _ => 1.0,
                    },
                    ascii: vec![None; VARIANTS * 128],
                    other: FxHashMap::default(),
                    family: fs
                        .db()
                        .family_name(&res.family_of(style.family))
                        .to_string(),
                    epoch: u64::MAX,
                },
            );
        }
        key
    }

    /// One cell's size, logical px.
    pub(crate) fn cell_size(
        &mut self,
        id: CellsId,
        prev: bool,
        res: &Resources,
        fs: &mut FontSystem,
    ) -> Size {
        let style = self.entry(id, prev).style;
        let key = self.table(&style, res, fs);
        let t = &self.tables[&key];
        Size::new(t.cell_w / self.scale, t.cell_h / self.scale)
    }

    /// The absolute line the grid's row 0 is (`CellGrid::origin_line`).
    pub(crate) fn origin_line(&self, id: CellsId, prev: bool) -> u64 {
        self.entry(id, prev).origin_line
    }

    /// The character in one cell, and whether it is a spacer after a wide
    /// glyph (which a copy skips rather than turning into a space).
    ///
    /// The flag lives on the *glyph*, so the spacer is recognised by the
    /// cell before it — reading `WIDE` off the cell itself said the wide
    /// character was the spacer, and copying a line of CJK gave back a
    /// row of blanks.
    pub(crate) fn cell_char(
        &self,
        id: CellsId,
        row: usize,
        col: usize,
        prev: bool,
    ) -> Option<(char, bool)> {
        let e = self.entry(id, prev);
        if row >= e.rows || col >= e.cols {
            return None;
        }
        let c = e.cells[row * e.cols + col];
        Some((c.ch, self.is_spacer(e, row, col)))
    }

    /// Whether this cell is the blank the app leaves after a wide glyph.
    fn is_spacer(&self, e: &Entry, row: usize, col: usize) -> bool {
        col > 0 && e.cells[row * e.cols + col - 1].flags & flags::WIDE != 0
    }

    /// The word around one cell, as a half-open column range on that row:
    /// the run of like cells it sits in, classed the way a double click
    /// classes text — word characters (alphanumeric or `_`), blanks, and
    /// everything else. `'\0'` and the spacer after a wide glyph are the
    /// glyph's own, so a double click on a wide character takes the pair.
    pub(crate) fn word_at(
        &self,
        id: CellsId,
        row: usize,
        col: usize,
        prev: bool,
    ) -> Option<(usize, usize)> {
        let e = self.entry(id, prev);
        if row >= e.rows || col >= e.cols {
            return None;
        }
        let class = |c: usize| -> u8 {
            // A spacer belongs to the glyph in front of it, so a wide
            // character and its blank are never two different words.
            let c = if self.is_spacer(e, row, c) { c - 1 } else { c };
            let ch = e.cells[row * e.cols + c].ch;
            if ch == '\0' || ch.is_whitespace() {
                1
            } else if ch.is_alphanumeric() || ch == '_' {
                0
            } else {
                2
            }
        };
        let here = class(col);
        let mut from = col;
        while from > 0 && class(from - 1) == here {
            from -= 1;
        }
        let mut to = col + 1;
        while to < e.cols && class(to) == here {
            to += 1;
        }
        Some((from, to))
    }

    pub(crate) fn dims(&self, id: CellsId, prev: bool) -> (usize, usize) {
        let e = self.entry(id, prev);
        (e.rows, e.cols)
    }

    /// The grid as text, rows joined by newlines with trailing blanks
    /// trimmed — what a screen reader reads.
    pub(crate) fn value(&self, id: CellsId) -> String {
        let e = &self.frame[id.0 as usize];
        let mut out = String::with_capacity(e.rows * (e.cols + 1));
        for r in 0..e.rows {
            let row = &e.cells[r * e.cols..(r + 1) * e.cols];
            let end = row
                .iter()
                .rposition(|c| c.ch != ' ' && c.ch != '\0')
                .map_or(0, |i| i + 1);
            for c in &row[..end] {
                out.push(if c.ch == '\0' { ' ' } else { c.ch });
            }
            if r + 1 < e.rows {
                out.push('\n');
            }
        }
        out
    }

    /// The grid's laid-out size, logical px.
    pub(crate) fn size(&mut self, id: CellsId, res: &Resources, fs: &mut FontSystem) -> Size {
        let (rows, cols, style) = {
            let e = &self.frame[id.0 as usize];
            (e.rows, e.cols, e.style)
        };
        let key = self.table(&style, res, fs);
        let t = &self.tables[&key];
        Size::new(
            cols as f32 * t.cell_w / self.scale,
            rows as f32 * t.cell_h / self.scale,
        )
    }

    /// Emits the grid at `origin` (logical) inside `clip` (physical).
    // The column index is the geometry (`col × cell_w`) as much as the
    // subscript, so the range loops stay.
    #[allow(clippy::too_many_arguments, clippy::needless_range_loop)]
    pub(crate) fn emit(
        &mut self,
        id: CellsId,
        origin: Vec2,
        clip: Clip,
        clip_id: ClipId,
        res: &Resources,
        fs: &mut FontSystem,
        raster: &mut Raster,
        atlas: &mut GlyphAtlas,
        out: &mut Vec<Quad>,
        // The window's selection when it is in *this* grid, and the tint
        // to paint it under. Resolved by the
        // caller, which is the only place that knows which grid is
        // selected in.
        sel: Option<(&crate::select::CellSelection, Color)>,
    ) {
        let scale = self.scale;
        let style = self.frame[id.0 as usize].style;
        let key = self.table(&style, res, fs);
        let ox = crate::geom::snap_px(origin.x * scale);
        let oy = crate::geom::snap_px(origin.y * scale);
        let entry = &self.frame[id.0 as usize];
        let table = self.tables.get_mut(&key).expect("just built");
        if table.epoch != atlas.stamp {
            // The page was replaced — reset, whose slots are gone, or
            // resized, whose slots stayed — or the atlas is measuring
            // what the frame uses: look every one up again.
            table.ascii.iter_mut().for_each(|g| *g = None);
            table.other.clear();
            table.epoch = atlas.stamp;
        }
        let (cw, ch) = (table.cell_w, table.cell_h);
        let quad = |rect: Rect, color: Color, kind: QuadKind, uv: [u32; 4]| Quad {
            rect,
            color,
            border_color: Color::TRANSPARENT,
            radius: [0.0; 4],
            border_w: 0.0,
            blur: 0.0,
            kind,
            clip: clip_id,
            uv,
        };
        // Only the rows and columns the clip can show.
        let r0 = (((clip.rect.y - oy) / ch).floor().max(0.0)) as usize;
        let r1 = (((clip.rect.y + clip.rect.h - oy) / ch).ceil().max(0.0) as usize).min(entry.rows);
        let c0 = (((clip.rect.x - ox) / cw).floor().max(0.0)) as usize;
        let c1 = (((clip.rect.x + clip.rect.w - ox) / cw).ceil().max(0.0) as usize).min(entry.cols);
        if r0 >= r1 || c0 >= c1 {
            return;
        }
        let stroke = (scale).round().max(1.0);
        // The selection, under everything the rows draw: one quad per
        // run of selected columns on each visible row, so a linewise
        // selection is one quad a line and a block selection is a
        // rectangle of them.
        if let Some((sel, tint)) = sel {
            for r in r0..r1 {
                let line = entry.origin_line + r as u64;
                let Some((from, to)) = sel.cols_on(line, entry.cols) else {
                    continue;
                };
                let (from, to) = (from.max(c0), to.min(c1));
                if from >= to {
                    continue;
                }
                out.push(quad(
                    Rect::new(
                        ox + from as f32 * cw,
                        oy + r as f32 * ch,
                        (to - from) as f32 * cw,
                        ch,
                    ),
                    tint,
                    QuadKind::Solid,
                    [0; 4],
                ));
            }
        }
        for r in r0..r1 {
            let row = &entry.cells[r * entry.cols..(r + 1) * entry.cols];
            let cy = oy + r as f32 * ch;
            // Backgrounds: one quad per run of one colour.
            let mut run_start = c0;
            let mut run_bg = row[c0].bg;
            for c in c0..=c1 {
                let bg = if c < c1 { row[c].bg } else { !run_bg };
                if bg != run_bg {
                    if run_bg & 0xff != 0 {
                        out.push(quad(
                            Rect::new(
                                ox + run_start as f32 * cw,
                                cy,
                                (c - run_start) as f32 * cw,
                                ch,
                            ),
                            Color::hex(run_bg),
                            QuadKind::Solid,
                            [0; 4],
                        ));
                    }
                    run_start = c;
                    run_bg = bg;
                }
            }
            // The cursor, under the glyph it sits on.
            if let Some((cr, cc, shape, color)) = entry.cursor
                && cr == r
                && cc >= c0
                && cc < c1
            {
                let cx = ox + cc as f32 * cw;
                let rect = match shape {
                    CursorShape::Block => Rect::new(cx, cy, cw, ch),
                    CursorShape::Bar => Rect::new(cx, cy, 2.0 * stroke, ch),
                    CursorShape::Underline => {
                        Rect::new(cx, cy + ch - 2.0 * stroke, cw, 2.0 * stroke)
                    }
                };
                out.push(quad(rect, color, QuadKind::Solid, [0; 4]));
            }
            // Glyphs, and the lines through and under them per run: cells
            // sharing the same line bits and colours share one line.
            let mut line_run: Option<(usize, u8, u32, u32)> = None;
            for c in c0..c1 {
                let cell = &row[c];
                let cx = ox + c as f32 * cw;
                if cell.ch != ' ' && cell.ch != '\0' {
                    let g = lookup(
                        table, cell.ch, cell.flags, &style, res, fs, raster, atlas, scale,
                    );
                    if let Some(g) = g {
                        out.push(quad(
                            Rect::new(cx + g.x, cy + g.y, g.w, g.h),
                            Color::hex(cell.fg),
                            g.kind,
                            g.uv,
                        ));
                    }
                }
                let lines = cell.flags & flags::LINES;
                let same = line_run
                    .is_some_and(|(_, f, fg, ul)| f == lines && fg == cell.fg && ul == cell.ul);
                if !same {
                    if let Some((start, f, fg, ul)) = line_run.take()
                        && f != 0
                    {
                        push_lines(
                            out, &quad, clip_id, ox, cy, cw, ch, stroke, start, c, f, fg, ul,
                        );
                    }
                    line_run = Some((c, lines, cell.fg, cell.ul));
                }
            }
            if let Some((start, f, fg, ul)) = line_run
                && f != 0
            {
                push_lines(
                    out, &quad, clip_id, ox, cy, cw, ch, stroke, start, c1, f, fg, ul,
                );
            }
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn push_lines(
    out: &mut Vec<Quad>,
    quad: &dyn Fn(Rect, Color, QuadKind, [u32; 4]) -> Quad,
    clip_id: ClipId,
    ox: f32,
    cy: f32,
    cw: f32,
    ch: f32,
    stroke: f32,
    start: usize,
    end: usize,
    f: u8,
    fg: u32,
    ul: u32,
) {
    let x = ox + start as f32 * cw;
    let w = (end - start) as f32 * cw;
    if f & (flags::UNDERLINE | flags::WAVY | flags::DOTTED) != 0 {
        // The shape bits imply the line; its colour is its own where the
        // cell says (SGR 58), else the foreground's.
        let style = if f & flags::WAVY != 0 {
            crate::spec::UnderlineStyle::Wavy
        } else if f & flags::DOTTED != 0 {
            crate::spec::UnderlineStyle::Dotted
        } else {
            crate::spec::UnderlineStyle::Solid
        };
        let color = Color::hex(if ul != 0 { ul } else { fg });
        crate::deco::push_line(
            out,
            style,
            x,
            cy + ch - 2.0 * stroke,
            w,
            stroke,
            color,
            clip_id,
        );
    }
    if f & flags::STRIKETHROUGH != 0 {
        out.push(quad(
            Rect::new(x, (cy + ch * 0.55).round(), w, stroke),
            Color::hex(fg),
            QuadKind::Solid,
            [0; 4],
        ));
    }
}

/// The glyph for `ch` in `flags`'s variant, from the table or shaped and
/// rasterized now — once per character and variant for the life of the
/// table.
#[allow(clippy::too_many_arguments)]
fn lookup(
    table: &mut StyleTable,
    ch: char,
    flags: u8,
    style: &TextStyle,
    res: &Resources,
    fs: &mut FontSystem,
    raster: &mut Raster,
    atlas: &mut GlyphAtlas,
    scale: f32,
) -> Option<CellGlyph> {
    let v = variant(flags);
    let known = if (ch as u32) < 128 {
        table.ascii[v * 128 + ch as usize]
    } else {
        table.other.get(&(ch, v as u8)).copied()
    };
    if let Some(g) = known {
        return g;
    }
    let g = shape_cell(
        ch,
        flags,
        style,
        &table.family,
        res,
        fs,
        raster,
        atlas,
        scale,
        (table.cell_w, table.cell_h),
    );
    if (ch as u32) < 128 {
        table.ascii[v * 128 + ch as usize] = Some(g);
    } else {
        table.other.insert((ch, v as u8), g);
    }
    g
}

/// Shapes one cell's character and rasterizes its glyph into the atlas —
/// or, for a character the cell box draws (`boxdraw`), rasterizes the
/// cell-sized mask and skips the font.
///
/// A character the style's family has no glyph for is another family's
/// (`own` is the style's), and a cell is still a cell (F120): a
/// monospaced face that has it is asked before the platform's fallback
/// list, whose first name on macOS is a proportional one; a glyph wider
/// than its cells — two for a wide one — is shaped again at the size it
/// fits at, on the baseline it had; and what room is left is shared either
/// side of it. The private use area is left as it falls: an icon is drawn
/// to run over the blank after it.
#[allow(clippy::too_many_arguments)]
fn shape_cell(
    ch: char,
    flags: u8,
    style: &TextStyle,
    own: &str,
    res: &Resources,
    fs: &mut FontSystem,
    raster: &mut Raster,
    atlas: &mut GlyphAtlas,
    scale: f32,
    cell: (f32, f32),
) -> Option<CellGlyph> {
    if boxdraw::draws(ch) {
        let (w, h) = (cell.0 as u32, cell.1 as u32);
        let slot = atlas.get_or_insert_synth(ch, w, h, || boxdraw::raster(ch, w, h))?;
        return Some(CellGlyph {
            x: 0.0,
            y: 0.0,
            w: w as f32,
            h: h as f32,
            uv: [slot.x, slot.y, slot.w, slot.h],
            kind: QuadKind::GlyphMask,
        });
    }
    let mut buf = [0u8; 4];
    let text: &str = ch.encode_utf8(&mut buf);
    let mut g = shape_one(style, text, flags, None, 1.0, res, fs, scale)?;
    let mut dx = 0.0;
    let theirs = fs
        .db()
        .face(g.font)
        .is_some_and(|f| !f.families.iter().any(|(name, _)| name == own));
    if theirs && !private_use(ch) {
        let mut family = None;
        // The app's own choice of fallback stands, whatever its pitch.
        let chosen = fs.db().face(g.font).is_some_and(|f| {
            f.families
                .iter()
                .any(|(name, _)| res.fallback.contains(name))
        });
        if !chosen
            && !fs.is_monospace(g.font)
            && let Some(m) = shape_one(style, text, flags, MONO, 1.0, res, fs, scale)
            && m.glyph != 0
            && fs.is_monospace(m.font)
        {
            family = MONO;
            g = m;
        }
        let span = cell.0 * if flags & flags::WIDE != 0 { 2.0 } else { 1.0 };
        if g.advance > span + 0.5 {
            let fit = span / g.advance;
            if let Some(f) = shape_one(style, text, flags, family, fit, res, fs, scale) {
                g = Shaped {
                    line_y: g.line_y,
                    ..f
                };
            }
        }
        dx = ((span - g.advance) / 2.0).round().max(0.0);
    }
    let slot = raster_glyph(g.key, fs, raster, atlas)?;
    Some(CellGlyph {
        x: dx + g.x as f32 + slot.left as f32,
        y: g.line_y.round() + g.y as f32 - slot.top as f32,
        w: slot.w as f32,
        h: slot.h as f32,
        uv: [slot.x, slot.y, slot.w, slot.h],
        kind: glyph_kind(&slot),
    })
}

/// The generic monospaced family, in place of the style's.
const MONO: Option<cosmic_text::Family<'static>> = Some(cosmic_text::Family::Monospace);

/// Whether `ch` is in a private use area: an icon font's.
fn private_use(ch: char) -> bool {
    matches!(ch as u32, 0xE000..=0xF8FF | 0xF_0000..=0x10_FFFF)
}

/// One shaped glyph: its cache key, its face and its id there (0 where
/// the face has none), its advance, and where it sits (physical x, y and
/// the baseline).
struct Shaped {
    key: cosmic_text::CacheKey,
    font: cosmic_text::fontdb::ID,
    glyph: u16,
    advance: f32,
    x: i32,
    y: i32,
    line_y: f32,
}

/// Shapes `text` alone in `style` at `scale` and returns its first glyph
/// — in `family` where one is given in place of the style's, at `fit`
/// times the style's size.
#[allow(clippy::too_many_arguments)]
fn shape_one(
    style: &TextStyle,
    text: &str,
    flags: u8,
    family: Option<cosmic_text::Family<'_>>,
    fit: f32,
    res: &Resources,
    fs: &mut FontSystem,
    scale: f32,
) -> Option<Shaped> {
    let metrics = crate::text::shaper_metrics(style.size * scale * fit, style.line_height * scale);
    let mut buffer = Buffer::new(fs, metrics);
    buffer.set_size(None, None);
    // Bold at a weight the family has a face for, never another family's.
    let mut attrs = match family {
        Some(family) => crate::weights::Weights::CSS
            .apply(Attrs::new().family(family), flags & flags::BOLD != 0),
        None => res.weights_of(style.family).apply(
            Attrs::new().family(res.family_of(style.family)),
            flags & flags::BOLD != 0,
        ),
    };
    if flags & flags::ITALIC != 0 {
        attrs = attrs.style(FontStyle::Italic);
    }
    buffer.set_text(text, &attrs, Shaping::Advanced, None);
    buffer.shape_until_scroll(fs, false);
    let run = buffer.layout_runs().next()?;
    let glyph = run.glyphs.first()?;
    let physical = glyph.physical((0.0, 0.0), 1.0);
    Some(Shaped {
        key: physical.cache_key,
        font: glyph.font_id,
        glyph: glyph.glyph_id,
        advance: glyph.w,
        x: physical.x,
        y: physical.y,
        line_y: run.line_y,
    })
}