Skip to main content

kui_core/
cells.rs

1//! A cell grid: a terminal's screen as one node, `rows × cols` cells each
2//! with a character, a foreground, a background and attribute bits.
3//!
4//! Build a slice of [`Cell`]s, describe it with a [`CellGrid`] and hand it
5//! to `Ui::cells` with the node's own spec. A glyph is shaped once per
6//! character and style variant and then placed at `col × cell_w` without
7//! shaping, so a pane whose every cell is new every frame costs the same
8//! as one that never changes. The node's rows apply as on any node: an
9//! `on_key` makes it the terminal's sink, an `on_click` or `on_drag`
10//! carries `cell: {row, col}`, `selectable` selects in cells, and its
11//! access row is `terminal`.
12//!
13//! ```rust
14//! use kui_core::cells::{flags, Cell, CellGrid};
15//! use kui_core::{CellCursor, Color, Core, NodeSpec, Size, TextStyle};
16//!
17//! let (rows, cols) = (2, 4);
18//! let mut cells = vec![Cell::default(); rows * cols];
19//! for (i, ch) in "ab c".chars().enumerate() {
20//!     cells[i] = Cell::new(ch, 0xffffffff, 0x0000ffff); // white on blue
21//! }
22//! cells[6] = Cell::new('x', 0xff0000ff, 0).with(flags::BOLD);
23//!
24//! let mut core = Core::new();
25//! let mut ui = core.frame(Size::new(400.0, 200.0), 1.0);
26//! ui.cells(
27//!     &CellGrid {
28//!         rows,
29//!         cols,
30//!         cells: &cells,
31//!         style: TextStyle::new(14.0).mono().line_height(20.0),
32//!         cursor: Some((1, 2, CellCursor::Block, Color::WHITE)),
33//!         origin_line: 0,
34//!     },
35//!     NodeSpec::default(),
36//! );
37//! ui.finish();
38//! ```
39//!
40//! What it deliberately is not: shaped text. No ligatures, no kerning, no
41//! wrapping. A cell is one `char`: a precomposed character is one cell; a
42//! base with combining marks, a ZWJ emoji sequence or a flag is not
43//! representable, so the app precomposes what NFC can and drops the rest.
44//! A wide character is marked [`flags::WIDE`] and the cell after it is a
45//! spacer the app leaves blank. Box drawing, block elements and the
46//! Powerline separators are not shaped at all but rasterized from the cell
47//! box, so a TUI's frames are seamless in any font.
48
49use cosmic_text::{Attrs, Buffer, FontSystem, Shaping, Style as FontStyle};
50use rustc_hash::FxHashMap;
51
52use crate::atlas::GlyphAtlas;
53use crate::color::Color;
54use crate::display::{Clip, ClipId, Quad, QuadKind};
55use crate::geom::{Rect, Size, Vec2};
56use crate::key::Key;
57use crate::resources::Resources;
58use crate::spec::TextStyle;
59use crate::text::{Raster, glyph_kind, raster_glyph};
60
61mod boxdraw;
62
63/// Bits in [`Cell::flags`].
64pub mod flags {
65    pub const BOLD: u8 = 1;
66    pub const ITALIC: u8 = 2;
67    pub const UNDERLINE: u8 = 4;
68    pub const STRIKETHROUGH: u8 = 8;
69    /// The glyph is two cells wide; the app leaves the next cell blank.
70    pub const WIDE: u8 = 16;
71    /// The underline is a wave (SGR 4:3, a terminal's undercurl). Implies
72    /// `UNDERLINE`.
73    pub const WAVY: u8 = 32;
74    /// The underline is dotted (SGR 4:4). Implies `UNDERLINE`.
75    pub const DOTTED: u8 = 64;
76    /// The bits that make a line under or through a cell, and its shape:
77    /// what a run of cells has to agree on to share one.
78    pub const LINES: u8 = UNDERLINE | STRIKETHROUGH | WAVY | DOTTED;
79}
80
81/// One cell: a character, its colours as `0xRRGGBBAA` (a background of 0
82/// is none, an underline colour of 0 the foreground's), and attribute
83/// bits. Sixteen bytes, so a 200×50 pane is a 160 KB slice a frame.
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
85pub struct Cell {
86    pub ch: char,
87    pub fg: u32,
88    pub bg: u32,
89    pub flags: u8,
90    /// The underline's own colour (SGR 58), or 0 for `fg`.
91    pub ul: u32,
92}
93
94impl Cell {
95    pub const fn new(ch: char, fg: u32, bg: u32) -> Self {
96        Self {
97            ch,
98            fg,
99            bg,
100            flags: 0,
101            ul: 0,
102        }
103    }
104
105    /// An underline in its own colour; sets `UNDERLINE`.
106    pub const fn underline_color(mut self, ul: u32) -> Self {
107        self.flags |= flags::UNDERLINE;
108        self.ul = ul;
109        self
110    }
111
112    pub const fn with(mut self, flags: u8) -> Self {
113        self.flags |= flags;
114        self
115    }
116}
117
118/// How the grid's cursor is drawn, in the colour given with it.
119#[derive(Clone, Copy, Debug, PartialEq, Eq)]
120pub enum CursorShape {
121    /// The whole cell, painted under its glyph.
122    Block,
123    /// A two-pixel bar at the cell's left edge.
124    Bar,
125    /// A two-pixel line along the cell's bottom.
126    Underline,
127}
128
129impl CursorShape {
130    /// Wire order: the index every binding carries (`block`, `bar`,
131    /// `underline`; C's `KUI_CELL_CURSOR_*` is this plus one).
132    pub const NAMES: &[&str] = &["block", "bar", "underline"];
133
134    pub fn from_index(i: usize) -> Option<Self> {
135        match i {
136            0 => Some(Self::Block),
137            1 => Some(Self::Bar),
138            2 => Some(Self::Underline),
139            _ => None,
140        }
141    }
142
143    pub fn from_name(s: &str) -> Option<Self> {
144        Self::NAMES
145            .iter()
146            .position(|n| *n == s)
147            .and_then(Self::from_index)
148    }
149}
150
151/// A grid to draw: the cells in row-major order (`rows × cols` of them;
152/// fewer draw as blank), the style the glyphs are shaped in (`size`,
153/// `line_height` as the cell height, `family` / `font`), and the cursor.
154#[derive(Clone, Copy, Debug)]
155pub struct CellGrid<'a> {
156    pub rows: usize,
157    pub cols: usize,
158    pub cells: &'a [Cell],
159    pub style: TextStyle,
160    /// `(row, col, shape, colour)`.
161    pub cursor: Option<(usize, usize, CursorShape, Color)>,
162    /// The absolute line number of row 0: where this screenful sits in
163    /// the app's own history.
164    ///
165    /// A grid is one screenful and the scrollback behind it is the app's,
166    /// so a row number is not an address: it means a different line after
167    /// every scroll. Stamping this makes a selection's ends absolute, and
168    /// a terminal that scrolls under a selection keeps it. An app that
169    /// never sets it gets 0 and a selection that is correct only while it
170    /// does not scroll, which is the honest reading of saying nothing.
171    pub origin_line: u64,
172}
173
174/// Index into the frame's grid list.
175#[derive(Clone, Copy, Debug, PartialEq, Eq)]
176pub struct CellsId(pub u32);
177
178struct Entry {
179    /// The node that drew it, so a grid stays findable after the frame it
180    /// was built in — a view asking about the selection runs while the
181    /// next frame's tree is half-built, and the answer is last frame's.
182    key: Key,
183    rows: usize,
184    cols: usize,
185    cells: Vec<Cell>,
186    style: TextStyle,
187    cursor: Option<(usize, usize, CursorShape, Color)>,
188    origin_line: u64,
189}
190
191/// A glyph placed in a cell: where its raster goes, from the cell's
192/// top-left, physical px.
193#[derive(Clone, Copy)]
194struct CellGlyph {
195    x: f32,
196    y: f32,
197    w: f32,
198    h: f32,
199    uv: [u32; 4],
200    kind: QuadKind,
201}
202
203/// The glyphs of one style at one scale: ASCII by direct index in four
204/// variants (plain, bold, italic, both), everything else by map.
205struct StyleTable {
206    cell_w: f32,
207    cell_h: f32,
208    ascii: Vec<Option<Option<CellGlyph>>>,
209    other: FxHashMap<(char, u8), Option<CellGlyph>>,
210    /// The style's family by name: a glyph from a face of another family
211    /// is a fallback's (`shape_cell`). Read off the family itself, not off
212    /// the face its `M` shapes with: a symbols-only or a CJK-only family
213    /// has no `M`, and its own glyphs read as a fallback's, asked of a
214    /// monospaced face first and centred (backlog RG118).
215    family: String,
216    /// The atlas stamp the slots were looked up against.
217    epoch: u64,
218}
219
220const VARIANTS: usize = 4;
221
222fn variant(flags: u8) -> usize {
223    (flags & (flags::BOLD | flags::ITALIC)) as usize
224}
225
226/// The frame's grids and the glyph tables they draw from. The frame
227/// before it is kept too, the way the text store keeps its places: a host
228/// that reads the selection from inside its own `view` is asking about a
229/// frame that has not been built yet.
230pub struct CellStore {
231    /// This frame's grids and the frame before's — always kept, the way
232    /// the text store keeps its places (`retain::Kept`).
233    frame: crate::retain::Kept<Entry>,
234    tables: FxHashMap<u64, StyleTable>,
235    scale: f32,
236}
237
238impl Default for CellStore {
239    fn default() -> Self {
240        Self::new()
241    }
242}
243
244impl CellStore {
245    pub fn new() -> Self {
246        Self {
247            frame: Default::default(),
248            tables: FxHashMap::default(),
249            scale: 1.0,
250        }
251    }
252
253    /// Drops every style's table, to shape again on its next draw: the
254    /// weights a family is asked at changed under them.
255    pub(crate) fn forget_shaped(&mut self) {
256        self.tables.clear();
257    }
258
259    pub(crate) fn begin_frame(&mut self, scale: f32) {
260        if (scale - self.scale).abs() > f32::EPSILON {
261            self.tables.clear();
262        }
263        self.scale = scale;
264        self.frame.begin(true);
265    }
266
267    /// The grid `key` drew, in this frame or the one before it.
268    pub(crate) fn find(&self, key: Key, prev: bool) -> Option<CellsId> {
269        self.list(prev)
270            .iter()
271            .position(|e| e.key == key)
272            .map(|i| CellsId(i as u32))
273    }
274
275    fn list(&self, prev: bool) -> &[Entry] {
276        if prev { self.frame.prev() } else { &self.frame }
277    }
278
279    fn entry(&self, id: CellsId, prev: bool) -> &Entry {
280        &self.list(prev)[id.0 as usize]
281    }
282
283    pub(crate) fn add(&mut self, key: Key, grid: &CellGrid<'_>) -> CellsId {
284        let n = grid.rows * grid.cols;
285        let mut cells = Vec::with_capacity(n);
286        cells.extend_from_slice(&grid.cells[..grid.cells.len().min(n)]);
287        cells.resize(n, Cell::default());
288        self.frame.push(Entry {
289            key,
290            rows: grid.rows,
291            cols: grid.cols,
292            cells,
293            style: grid.style,
294            cursor: grid.cursor,
295            origin_line: grid.origin_line,
296        });
297        CellsId((self.frame.len() - 1) as u32)
298    }
299
300    fn table_key(style: &TextStyle, scale: f32) -> u64 {
301        crate::text::TextSystem::style_key("", style, scale)
302    }
303
304    /// The table for `style`, built if this is the first time: the cell
305    /// width is `M`'s advance and the cell height the style's line
306    /// height, both physical.
307    fn table(&mut self, style: &TextStyle, res: &Resources, fs: &mut FontSystem) -> u64 {
308        let key = Self::table_key(style, self.scale);
309        if !self.tables.contains_key(&key) {
310            let scale = self.scale;
311            let cell_w = shape_one(style, "M", 0, None, 1.0, res, fs, scale)
312                .map_or(style.size * scale * 0.6, |g| g.advance)
313                .round()
314                .max(1.0);
315            self.tables.insert(
316                key,
317                StyleTable {
318                    cell_w,
319                    // A pixel at least, as the shaper's (backlog RG136):
320                    // an infinite one put every row after the first at
321                    // the layout's limit.
322                    cell_h: match (style.line_height * scale).round() {
323                        h if h.is_finite() => h.max(1.0),
324                        _ => 1.0,
325                    },
326                    ascii: vec![None; VARIANTS * 128],
327                    other: FxHashMap::default(),
328                    family: fs
329                        .db()
330                        .family_name(&res.family_of(style.family))
331                        .to_string(),
332                    epoch: u64::MAX,
333                },
334            );
335        }
336        key
337    }
338
339    /// One cell's size, logical px.
340    pub(crate) fn cell_size(
341        &mut self,
342        id: CellsId,
343        prev: bool,
344        res: &Resources,
345        fs: &mut FontSystem,
346    ) -> Size {
347        let style = self.entry(id, prev).style;
348        let key = self.table(&style, res, fs);
349        let t = &self.tables[&key];
350        Size::new(t.cell_w / self.scale, t.cell_h / self.scale)
351    }
352
353    /// The absolute line the grid's row 0 is (`CellGrid::origin_line`).
354    pub(crate) fn origin_line(&self, id: CellsId, prev: bool) -> u64 {
355        self.entry(id, prev).origin_line
356    }
357
358    /// The character in one cell, and whether it is a spacer after a wide
359    /// glyph (which a copy skips rather than turning into a space).
360    ///
361    /// The flag lives on the *glyph*, so the spacer is recognised by the
362    /// cell before it — reading `WIDE` off the cell itself said the wide
363    /// character was the spacer, and copying a line of CJK gave back a
364    /// row of blanks.
365    pub(crate) fn cell_char(
366        &self,
367        id: CellsId,
368        row: usize,
369        col: usize,
370        prev: bool,
371    ) -> Option<(char, bool)> {
372        let e = self.entry(id, prev);
373        if row >= e.rows || col >= e.cols {
374            return None;
375        }
376        let c = e.cells[row * e.cols + col];
377        Some((c.ch, self.is_spacer(e, row, col)))
378    }
379
380    /// Whether this cell is the blank the app leaves after a wide glyph.
381    fn is_spacer(&self, e: &Entry, row: usize, col: usize) -> bool {
382        col > 0 && e.cells[row * e.cols + col - 1].flags & flags::WIDE != 0
383    }
384
385    /// The word around one cell, as a half-open column range on that row:
386    /// the run of like cells it sits in, classed the way a double click
387    /// classes text — word characters (alphanumeric or `_`), blanks, and
388    /// everything else. `'\0'` and the spacer after a wide glyph are the
389    /// glyph's own, so a double click on a wide character takes the pair.
390    pub(crate) fn word_at(
391        &self,
392        id: CellsId,
393        row: usize,
394        col: usize,
395        prev: bool,
396    ) -> Option<(usize, usize)> {
397        let e = self.entry(id, prev);
398        if row >= e.rows || col >= e.cols {
399            return None;
400        }
401        let class = |c: usize| -> u8 {
402            // A spacer belongs to the glyph in front of it, so a wide
403            // character and its blank are never two different words.
404            let c = if self.is_spacer(e, row, c) { c - 1 } else { c };
405            let ch = e.cells[row * e.cols + c].ch;
406            if ch == '\0' || ch.is_whitespace() {
407                1
408            } else if ch.is_alphanumeric() || ch == '_' {
409                0
410            } else {
411                2
412            }
413        };
414        let here = class(col);
415        let mut from = col;
416        while from > 0 && class(from - 1) == here {
417            from -= 1;
418        }
419        let mut to = col + 1;
420        while to < e.cols && class(to) == here {
421            to += 1;
422        }
423        Some((from, to))
424    }
425
426    pub(crate) fn dims(&self, id: CellsId, prev: bool) -> (usize, usize) {
427        let e = self.entry(id, prev);
428        (e.rows, e.cols)
429    }
430
431    /// The grid as text, rows joined by newlines with trailing blanks
432    /// trimmed — what a screen reader reads.
433    pub(crate) fn value(&self, id: CellsId) -> String {
434        let e = &self.frame[id.0 as usize];
435        let mut out = String::with_capacity(e.rows * (e.cols + 1));
436        for r in 0..e.rows {
437            let row = &e.cells[r * e.cols..(r + 1) * e.cols];
438            let end = row
439                .iter()
440                .rposition(|c| c.ch != ' ' && c.ch != '\0')
441                .map_or(0, |i| i + 1);
442            for c in &row[..end] {
443                out.push(if c.ch == '\0' { ' ' } else { c.ch });
444            }
445            if r + 1 < e.rows {
446                out.push('\n');
447            }
448        }
449        out
450    }
451
452    /// The grid's laid-out size, logical px.
453    pub(crate) fn size(&mut self, id: CellsId, res: &Resources, fs: &mut FontSystem) -> Size {
454        let (rows, cols, style) = {
455            let e = &self.frame[id.0 as usize];
456            (e.rows, e.cols, e.style)
457        };
458        let key = self.table(&style, res, fs);
459        let t = &self.tables[&key];
460        Size::new(
461            cols as f32 * t.cell_w / self.scale,
462            rows as f32 * t.cell_h / self.scale,
463        )
464    }
465
466    /// Emits the grid at `origin` (logical) inside `clip` (physical).
467    // The column index is the geometry (`col × cell_w`) as much as the
468    // subscript, so the range loops stay.
469    #[allow(clippy::too_many_arguments, clippy::needless_range_loop)]
470    pub(crate) fn emit(
471        &mut self,
472        id: CellsId,
473        origin: Vec2,
474        clip: Clip,
475        clip_id: ClipId,
476        res: &Resources,
477        fs: &mut FontSystem,
478        raster: &mut Raster,
479        atlas: &mut GlyphAtlas,
480        out: &mut Vec<Quad>,
481        // The window's selection when it is in *this* grid, and the tint
482        // to paint it under. Resolved by the
483        // caller, which is the only place that knows which grid is
484        // selected in.
485        sel: Option<(&crate::select::CellSelection, Color)>,
486    ) {
487        let scale = self.scale;
488        let style = self.frame[id.0 as usize].style;
489        let key = self.table(&style, res, fs);
490        let ox = crate::geom::snap_px(origin.x * scale);
491        let oy = crate::geom::snap_px(origin.y * scale);
492        let entry = &self.frame[id.0 as usize];
493        let table = self.tables.get_mut(&key).expect("just built");
494        if table.epoch != atlas.stamp {
495            // The page was replaced — reset, whose slots are gone, or
496            // resized, whose slots stayed — or the atlas is measuring
497            // what the frame uses: look every one up again.
498            table.ascii.iter_mut().for_each(|g| *g = None);
499            table.other.clear();
500            table.epoch = atlas.stamp;
501        }
502        let (cw, ch) = (table.cell_w, table.cell_h);
503        let quad = |rect: Rect, color: Color, kind: QuadKind, uv: [u32; 4]| Quad {
504            rect,
505            color,
506            border_color: Color::TRANSPARENT,
507            radius: [0.0; 4],
508            border_w: 0.0,
509            blur: 0.0,
510            kind,
511            clip: clip_id,
512            uv,
513        };
514        // Only the rows and columns the clip can show.
515        let r0 = (((clip.rect.y - oy) / ch).floor().max(0.0)) as usize;
516        let r1 = (((clip.rect.y + clip.rect.h - oy) / ch).ceil().max(0.0) as usize).min(entry.rows);
517        let c0 = (((clip.rect.x - ox) / cw).floor().max(0.0)) as usize;
518        let c1 = (((clip.rect.x + clip.rect.w - ox) / cw).ceil().max(0.0) as usize).min(entry.cols);
519        if r0 >= r1 || c0 >= c1 {
520            return;
521        }
522        let stroke = (scale).round().max(1.0);
523        // The selection, under everything the rows draw: one quad per
524        // run of selected columns on each visible row, so a linewise
525        // selection is one quad a line and a block selection is a
526        // rectangle of them.
527        if let Some((sel, tint)) = sel {
528            for r in r0..r1 {
529                let line = entry.origin_line + r as u64;
530                let Some((from, to)) = sel.cols_on(line, entry.cols) else {
531                    continue;
532                };
533                let (from, to) = (from.max(c0), to.min(c1));
534                if from >= to {
535                    continue;
536                }
537                out.push(quad(
538                    Rect::new(
539                        ox + from as f32 * cw,
540                        oy + r as f32 * ch,
541                        (to - from) as f32 * cw,
542                        ch,
543                    ),
544                    tint,
545                    QuadKind::Solid,
546                    [0; 4],
547                ));
548            }
549        }
550        for r in r0..r1 {
551            let row = &entry.cells[r * entry.cols..(r + 1) * entry.cols];
552            let cy = oy + r as f32 * ch;
553            // Backgrounds: one quad per run of one colour.
554            let mut run_start = c0;
555            let mut run_bg = row[c0].bg;
556            for c in c0..=c1 {
557                let bg = if c < c1 { row[c].bg } else { !run_bg };
558                if bg != run_bg {
559                    if run_bg & 0xff != 0 {
560                        out.push(quad(
561                            Rect::new(
562                                ox + run_start as f32 * cw,
563                                cy,
564                                (c - run_start) as f32 * cw,
565                                ch,
566                            ),
567                            Color::hex(run_bg),
568                            QuadKind::Solid,
569                            [0; 4],
570                        ));
571                    }
572                    run_start = c;
573                    run_bg = bg;
574                }
575            }
576            // The cursor, under the glyph it sits on.
577            if let Some((cr, cc, shape, color)) = entry.cursor
578                && cr == r
579                && cc >= c0
580                && cc < c1
581            {
582                let cx = ox + cc as f32 * cw;
583                let rect = match shape {
584                    CursorShape::Block => Rect::new(cx, cy, cw, ch),
585                    CursorShape::Bar => Rect::new(cx, cy, 2.0 * stroke, ch),
586                    CursorShape::Underline => {
587                        Rect::new(cx, cy + ch - 2.0 * stroke, cw, 2.0 * stroke)
588                    }
589                };
590                out.push(quad(rect, color, QuadKind::Solid, [0; 4]));
591            }
592            // Glyphs, and the lines through and under them per run: cells
593            // sharing the same line bits and colours share one line.
594            let mut line_run: Option<(usize, u8, u32, u32)> = None;
595            for c in c0..c1 {
596                let cell = &row[c];
597                let cx = ox + c as f32 * cw;
598                if cell.ch != ' ' && cell.ch != '\0' {
599                    let g = lookup(
600                        table, cell.ch, cell.flags, &style, res, fs, raster, atlas, scale,
601                    );
602                    if let Some(g) = g {
603                        out.push(quad(
604                            Rect::new(cx + g.x, cy + g.y, g.w, g.h),
605                            Color::hex(cell.fg),
606                            g.kind,
607                            g.uv,
608                        ));
609                    }
610                }
611                let lines = cell.flags & flags::LINES;
612                let same = line_run
613                    .is_some_and(|(_, f, fg, ul)| f == lines && fg == cell.fg && ul == cell.ul);
614                if !same {
615                    if let Some((start, f, fg, ul)) = line_run.take()
616                        && f != 0
617                    {
618                        push_lines(
619                            out, &quad, clip_id, ox, cy, cw, ch, stroke, start, c, f, fg, ul,
620                        );
621                    }
622                    line_run = Some((c, lines, cell.fg, cell.ul));
623                }
624            }
625            if let Some((start, f, fg, ul)) = line_run
626                && f != 0
627            {
628                push_lines(
629                    out, &quad, clip_id, ox, cy, cw, ch, stroke, start, c1, f, fg, ul,
630                );
631            }
632        }
633    }
634}
635
636#[allow(clippy::too_many_arguments)]
637fn push_lines(
638    out: &mut Vec<Quad>,
639    quad: &dyn Fn(Rect, Color, QuadKind, [u32; 4]) -> Quad,
640    clip_id: ClipId,
641    ox: f32,
642    cy: f32,
643    cw: f32,
644    ch: f32,
645    stroke: f32,
646    start: usize,
647    end: usize,
648    f: u8,
649    fg: u32,
650    ul: u32,
651) {
652    let x = ox + start as f32 * cw;
653    let w = (end - start) as f32 * cw;
654    if f & (flags::UNDERLINE | flags::WAVY | flags::DOTTED) != 0 {
655        // The shape bits imply the line; its colour is its own where the
656        // cell says (SGR 58), else the foreground's.
657        let style = if f & flags::WAVY != 0 {
658            crate::spec::UnderlineStyle::Wavy
659        } else if f & flags::DOTTED != 0 {
660            crate::spec::UnderlineStyle::Dotted
661        } else {
662            crate::spec::UnderlineStyle::Solid
663        };
664        let color = Color::hex(if ul != 0 { ul } else { fg });
665        crate::deco::push_line(
666            out,
667            style,
668            x,
669            cy + ch - 2.0 * stroke,
670            w,
671            stroke,
672            color,
673            clip_id,
674        );
675    }
676    if f & flags::STRIKETHROUGH != 0 {
677        out.push(quad(
678            Rect::new(x, (cy + ch * 0.55).round(), w, stroke),
679            Color::hex(fg),
680            QuadKind::Solid,
681            [0; 4],
682        ));
683    }
684}
685
686/// The glyph for `ch` in `flags`'s variant, from the table or shaped and
687/// rasterized now — once per character and variant for the life of the
688/// table.
689#[allow(clippy::too_many_arguments)]
690fn lookup(
691    table: &mut StyleTable,
692    ch: char,
693    flags: u8,
694    style: &TextStyle,
695    res: &Resources,
696    fs: &mut FontSystem,
697    raster: &mut Raster,
698    atlas: &mut GlyphAtlas,
699    scale: f32,
700) -> Option<CellGlyph> {
701    let v = variant(flags);
702    let known = if (ch as u32) < 128 {
703        table.ascii[v * 128 + ch as usize]
704    } else {
705        table.other.get(&(ch, v as u8)).copied()
706    };
707    if let Some(g) = known {
708        return g;
709    }
710    let g = shape_cell(
711        ch,
712        flags,
713        style,
714        &table.family,
715        res,
716        fs,
717        raster,
718        atlas,
719        scale,
720        (table.cell_w, table.cell_h),
721    );
722    if (ch as u32) < 128 {
723        table.ascii[v * 128 + ch as usize] = Some(g);
724    } else {
725        table.other.insert((ch, v as u8), g);
726    }
727    g
728}
729
730/// Shapes one cell's character and rasterizes its glyph into the atlas —
731/// or, for a character the cell box draws (`boxdraw`), rasterizes the
732/// cell-sized mask and skips the font.
733///
734/// A character the style's family has no glyph for is another family's
735/// (`own` is the style's), and a cell is still a cell (F120): a
736/// monospaced face that has it is asked before the platform's fallback
737/// list, whose first name on macOS is a proportional one; a glyph wider
738/// than its cells — two for a wide one — is shaped again at the size it
739/// fits at, on the baseline it had; and what room is left is shared either
740/// side of it. The private use area is left as it falls: an icon is drawn
741/// to run over the blank after it.
742#[allow(clippy::too_many_arguments)]
743fn shape_cell(
744    ch: char,
745    flags: u8,
746    style: &TextStyle,
747    own: &str,
748    res: &Resources,
749    fs: &mut FontSystem,
750    raster: &mut Raster,
751    atlas: &mut GlyphAtlas,
752    scale: f32,
753    cell: (f32, f32),
754) -> Option<CellGlyph> {
755    if boxdraw::draws(ch) {
756        let (w, h) = (cell.0 as u32, cell.1 as u32);
757        let slot = atlas.get_or_insert_synth(ch, w, h, || boxdraw::raster(ch, w, h))?;
758        return Some(CellGlyph {
759            x: 0.0,
760            y: 0.0,
761            w: w as f32,
762            h: h as f32,
763            uv: [slot.x, slot.y, slot.w, slot.h],
764            kind: QuadKind::GlyphMask,
765        });
766    }
767    let mut buf = [0u8; 4];
768    let text: &str = ch.encode_utf8(&mut buf);
769    let mut g = shape_one(style, text, flags, None, 1.0, res, fs, scale)?;
770    let mut dx = 0.0;
771    let theirs = fs
772        .db()
773        .face(g.font)
774        .is_some_and(|f| !f.families.iter().any(|(name, _)| name == own));
775    if theirs && !private_use(ch) {
776        let mut family = None;
777        // The app's own choice of fallback stands, whatever its pitch.
778        let chosen = fs.db().face(g.font).is_some_and(|f| {
779            f.families
780                .iter()
781                .any(|(name, _)| res.fallback.contains(name))
782        });
783        if !chosen
784            && !fs.is_monospace(g.font)
785            && let Some(m) = shape_one(style, text, flags, MONO, 1.0, res, fs, scale)
786            && m.glyph != 0
787            && fs.is_monospace(m.font)
788        {
789            family = MONO;
790            g = m;
791        }
792        let span = cell.0 * if flags & flags::WIDE != 0 { 2.0 } else { 1.0 };
793        if g.advance > span + 0.5 {
794            let fit = span / g.advance;
795            if let Some(f) = shape_one(style, text, flags, family, fit, res, fs, scale) {
796                g = Shaped {
797                    line_y: g.line_y,
798                    ..f
799                };
800            }
801        }
802        dx = ((span - g.advance) / 2.0).round().max(0.0);
803    }
804    let slot = raster_glyph(g.key, fs, raster, atlas)?;
805    Some(CellGlyph {
806        x: dx + g.x as f32 + slot.left as f32,
807        y: g.line_y.round() + g.y as f32 - slot.top as f32,
808        w: slot.w as f32,
809        h: slot.h as f32,
810        uv: [slot.x, slot.y, slot.w, slot.h],
811        kind: glyph_kind(&slot),
812    })
813}
814
815/// The generic monospaced family, in place of the style's.
816const MONO: Option<cosmic_text::Family<'static>> = Some(cosmic_text::Family::Monospace);
817
818/// Whether `ch` is in a private use area: an icon font's.
819fn private_use(ch: char) -> bool {
820    matches!(ch as u32, 0xE000..=0xF8FF | 0xF_0000..=0x10_FFFF)
821}
822
823/// One shaped glyph: its cache key, its face and its id there (0 where
824/// the face has none), its advance, and where it sits (physical x, y and
825/// the baseline).
826struct Shaped {
827    key: cosmic_text::CacheKey,
828    font: cosmic_text::fontdb::ID,
829    glyph: u16,
830    advance: f32,
831    x: i32,
832    y: i32,
833    line_y: f32,
834}
835
836/// Shapes `text` alone in `style` at `scale` and returns its first glyph
837/// — in `family` where one is given in place of the style's, at `fit`
838/// times the style's size.
839#[allow(clippy::too_many_arguments)]
840fn shape_one(
841    style: &TextStyle,
842    text: &str,
843    flags: u8,
844    family: Option<cosmic_text::Family<'_>>,
845    fit: f32,
846    res: &Resources,
847    fs: &mut FontSystem,
848    scale: f32,
849) -> Option<Shaped> {
850    let metrics = crate::text::shaper_metrics(style.size * scale * fit, style.line_height * scale);
851    let mut buffer = Buffer::new(fs, metrics);
852    buffer.set_size(None, None);
853    // Bold at a weight the family has a face for, never another family's.
854    let mut attrs = match family {
855        Some(family) => crate::weights::Weights::CSS
856            .apply(Attrs::new().family(family), flags & flags::BOLD != 0),
857        None => res.weights_of(style.family).apply(
858            Attrs::new().family(res.family_of(style.family)),
859            flags & flags::BOLD != 0,
860        ),
861    };
862    if flags & flags::ITALIC != 0 {
863        attrs = attrs.style(FontStyle::Italic);
864    }
865    buffer.set_text(text, &attrs, Shaping::Advanced, None);
866    buffer.shape_until_scroll(fs, false);
867    let run = buffer.layout_runs().next()?;
868    let glyph = run.glyphs.first()?;
869    let physical = glyph.physical((0.0, 0.0), 1.0);
870    Some(Shaped {
871        key: physical.cache_key,
872        font: glyph.font_id,
873        glyph: glyph.glyph_id,
874        advance: glyph.w,
875        x: physical.x,
876        y: physical.y,
877        line_y: run.line_y,
878    })
879}