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, Metrics, 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                    cell_h: (style.line_height * scale).round().max(1.0),
320                    ascii: vec![None; VARIANTS * 128],
321                    other: FxHashMap::default(),
322                    family: fs
323                        .db()
324                        .family_name(&res.family_of(style.family))
325                        .to_string(),
326                    epoch: u64::MAX,
327                },
328            );
329        }
330        key
331    }
332
333    /// One cell's size, logical px.
334    pub(crate) fn cell_size(
335        &mut self,
336        id: CellsId,
337        prev: bool,
338        res: &Resources,
339        fs: &mut FontSystem,
340    ) -> Size {
341        let style = self.entry(id, prev).style;
342        let key = self.table(&style, res, fs);
343        let t = &self.tables[&key];
344        Size::new(t.cell_w / self.scale, t.cell_h / self.scale)
345    }
346
347    /// The absolute line the grid's row 0 is (`CellGrid::origin_line`).
348    pub(crate) fn origin_line(&self, id: CellsId, prev: bool) -> u64 {
349        self.entry(id, prev).origin_line
350    }
351
352    /// The character in one cell, and whether it is a spacer after a wide
353    /// glyph (which a copy skips rather than turning into a space).
354    ///
355    /// The flag lives on the *glyph*, so the spacer is recognised by the
356    /// cell before it — reading `WIDE` off the cell itself said the wide
357    /// character was the spacer, and copying a line of CJK gave back a
358    /// row of blanks.
359    pub(crate) fn cell_char(
360        &self,
361        id: CellsId,
362        row: usize,
363        col: usize,
364        prev: bool,
365    ) -> Option<(char, bool)> {
366        let e = self.entry(id, prev);
367        if row >= e.rows || col >= e.cols {
368            return None;
369        }
370        let c = e.cells[row * e.cols + col];
371        Some((c.ch, self.is_spacer(e, row, col)))
372    }
373
374    /// Whether this cell is the blank the app leaves after a wide glyph.
375    fn is_spacer(&self, e: &Entry, row: usize, col: usize) -> bool {
376        col > 0 && e.cells[row * e.cols + col - 1].flags & flags::WIDE != 0
377    }
378
379    /// The word around one cell, as a half-open column range on that row:
380    /// the run of like cells it sits in, classed the way a double click
381    /// classes text — word characters (alphanumeric or `_`), blanks, and
382    /// everything else. `'\0'` and the spacer after a wide glyph are the
383    /// glyph's own, so a double click on a wide character takes the pair.
384    pub(crate) fn word_at(
385        &self,
386        id: CellsId,
387        row: usize,
388        col: usize,
389        prev: bool,
390    ) -> Option<(usize, usize)> {
391        let e = self.entry(id, prev);
392        if row >= e.rows || col >= e.cols {
393            return None;
394        }
395        let class = |c: usize| -> u8 {
396            // A spacer belongs to the glyph in front of it, so a wide
397            // character and its blank are never two different words.
398            let c = if self.is_spacer(e, row, c) { c - 1 } else { c };
399            let ch = e.cells[row * e.cols + c].ch;
400            if ch == '\0' || ch.is_whitespace() {
401                1
402            } else if ch.is_alphanumeric() || ch == '_' {
403                0
404            } else {
405                2
406            }
407        };
408        let here = class(col);
409        let mut from = col;
410        while from > 0 && class(from - 1) == here {
411            from -= 1;
412        }
413        let mut to = col + 1;
414        while to < e.cols && class(to) == here {
415            to += 1;
416        }
417        Some((from, to))
418    }
419
420    pub(crate) fn dims(&self, id: CellsId, prev: bool) -> (usize, usize) {
421        let e = self.entry(id, prev);
422        (e.rows, e.cols)
423    }
424
425    /// The grid as text, rows joined by newlines with trailing blanks
426    /// trimmed — what a screen reader reads.
427    pub(crate) fn value(&self, id: CellsId) -> String {
428        let e = &self.frame[id.0 as usize];
429        let mut out = String::with_capacity(e.rows * (e.cols + 1));
430        for r in 0..e.rows {
431            let row = &e.cells[r * e.cols..(r + 1) * e.cols];
432            let end = row
433                .iter()
434                .rposition(|c| c.ch != ' ' && c.ch != '\0')
435                .map_or(0, |i| i + 1);
436            for c in &row[..end] {
437                out.push(if c.ch == '\0' { ' ' } else { c.ch });
438            }
439            if r + 1 < e.rows {
440                out.push('\n');
441            }
442        }
443        out
444    }
445
446    /// The grid's laid-out size, logical px.
447    pub(crate) fn size(&mut self, id: CellsId, res: &Resources, fs: &mut FontSystem) -> Size {
448        let (rows, cols, style) = {
449            let e = &self.frame[id.0 as usize];
450            (e.rows, e.cols, e.style)
451        };
452        let key = self.table(&style, res, fs);
453        let t = &self.tables[&key];
454        Size::new(
455            cols as f32 * t.cell_w / self.scale,
456            rows as f32 * t.cell_h / self.scale,
457        )
458    }
459
460    /// Emits the grid at `origin` (logical) inside `clip` (physical).
461    // The column index is the geometry (`col × cell_w`) as much as the
462    // subscript, so the range loops stay.
463    #[allow(clippy::too_many_arguments, clippy::needless_range_loop)]
464    pub(crate) fn emit(
465        &mut self,
466        id: CellsId,
467        origin: Vec2,
468        clip: Clip,
469        clip_id: ClipId,
470        res: &Resources,
471        fs: &mut FontSystem,
472        raster: &mut Raster,
473        atlas: &mut GlyphAtlas,
474        out: &mut Vec<Quad>,
475        // The window's selection when it is in *this* grid, and the tint
476        // to paint it under. Resolved by the
477        // caller, which is the only place that knows which grid is
478        // selected in.
479        sel: Option<(&crate::select::CellSelection, Color)>,
480    ) {
481        let scale = self.scale;
482        let style = self.frame[id.0 as usize].style;
483        let key = self.table(&style, res, fs);
484        let ox = crate::geom::snap_px(origin.x * scale);
485        let oy = crate::geom::snap_px(origin.y * scale);
486        let entry = &self.frame[id.0 as usize];
487        let table = self.tables.get_mut(&key).expect("just built");
488        if table.epoch != atlas.stamp {
489            // The page was replaced — reset, whose slots are gone, or
490            // resized, whose slots stayed — or the atlas is measuring
491            // what the frame uses: look every one up again.
492            table.ascii.iter_mut().for_each(|g| *g = None);
493            table.other.clear();
494            table.epoch = atlas.stamp;
495        }
496        let (cw, ch) = (table.cell_w, table.cell_h);
497        let quad = |rect: Rect, color: Color, kind: QuadKind, uv: [u32; 4]| Quad {
498            rect,
499            color,
500            border_color: Color::TRANSPARENT,
501            radius: [0.0; 4],
502            border_w: 0.0,
503            blur: 0.0,
504            kind,
505            clip: clip_id,
506            uv,
507        };
508        // Only the rows and columns the clip can show.
509        let r0 = (((clip.rect.y - oy) / ch).floor().max(0.0)) as usize;
510        let r1 = (((clip.rect.y + clip.rect.h - oy) / ch).ceil().max(0.0) as usize).min(entry.rows);
511        let c0 = (((clip.rect.x - ox) / cw).floor().max(0.0)) as usize;
512        let c1 = (((clip.rect.x + clip.rect.w - ox) / cw).ceil().max(0.0) as usize).min(entry.cols);
513        if r0 >= r1 || c0 >= c1 {
514            return;
515        }
516        let stroke = (scale).round().max(1.0);
517        // The selection, under everything the rows draw: one quad per
518        // run of selected columns on each visible row, so a linewise
519        // selection is one quad a line and a block selection is a
520        // rectangle of them.
521        if let Some((sel, tint)) = sel {
522            for r in r0..r1 {
523                let line = entry.origin_line + r as u64;
524                let Some((from, to)) = sel.cols_on(line, entry.cols) else {
525                    continue;
526                };
527                let (from, to) = (from.max(c0), to.min(c1));
528                if from >= to {
529                    continue;
530                }
531                out.push(quad(
532                    Rect::new(
533                        ox + from as f32 * cw,
534                        oy + r as f32 * ch,
535                        (to - from) as f32 * cw,
536                        ch,
537                    ),
538                    tint,
539                    QuadKind::Solid,
540                    [0; 4],
541                ));
542            }
543        }
544        for r in r0..r1 {
545            let row = &entry.cells[r * entry.cols..(r + 1) * entry.cols];
546            let cy = oy + r as f32 * ch;
547            // Backgrounds: one quad per run of one colour.
548            let mut run_start = c0;
549            let mut run_bg = row[c0].bg;
550            for c in c0..=c1 {
551                let bg = if c < c1 { row[c].bg } else { !run_bg };
552                if bg != run_bg {
553                    if run_bg & 0xff != 0 {
554                        out.push(quad(
555                            Rect::new(
556                                ox + run_start as f32 * cw,
557                                cy,
558                                (c - run_start) as f32 * cw,
559                                ch,
560                            ),
561                            Color::hex(run_bg),
562                            QuadKind::Solid,
563                            [0; 4],
564                        ));
565                    }
566                    run_start = c;
567                    run_bg = bg;
568                }
569            }
570            // The cursor, under the glyph it sits on.
571            if let Some((cr, cc, shape, color)) = entry.cursor
572                && cr == r
573                && cc >= c0
574                && cc < c1
575            {
576                let cx = ox + cc as f32 * cw;
577                let rect = match shape {
578                    CursorShape::Block => Rect::new(cx, cy, cw, ch),
579                    CursorShape::Bar => Rect::new(cx, cy, 2.0 * stroke, ch),
580                    CursorShape::Underline => {
581                        Rect::new(cx, cy + ch - 2.0 * stroke, cw, 2.0 * stroke)
582                    }
583                };
584                out.push(quad(rect, color, QuadKind::Solid, [0; 4]));
585            }
586            // Glyphs, and the lines through and under them per run: cells
587            // sharing the same line bits and colours share one line.
588            let mut line_run: Option<(usize, u8, u32, u32)> = None;
589            for c in c0..c1 {
590                let cell = &row[c];
591                let cx = ox + c as f32 * cw;
592                if cell.ch != ' ' && cell.ch != '\0' {
593                    let g = lookup(
594                        table, cell.ch, cell.flags, &style, res, fs, raster, atlas, scale,
595                    );
596                    if let Some(g) = g {
597                        out.push(quad(
598                            Rect::new(cx + g.x, cy + g.y, g.w, g.h),
599                            Color::hex(cell.fg),
600                            g.kind,
601                            g.uv,
602                        ));
603                    }
604                }
605                let lines = cell.flags & flags::LINES;
606                let same = line_run
607                    .is_some_and(|(_, f, fg, ul)| f == lines && fg == cell.fg && ul == cell.ul);
608                if !same {
609                    if let Some((start, f, fg, ul)) = line_run.take()
610                        && f != 0
611                    {
612                        push_lines(
613                            out, &quad, clip_id, ox, cy, cw, ch, stroke, start, c, f, fg, ul,
614                        );
615                    }
616                    line_run = Some((c, lines, cell.fg, cell.ul));
617                }
618            }
619            if let Some((start, f, fg, ul)) = line_run
620                && f != 0
621            {
622                push_lines(
623                    out, &quad, clip_id, ox, cy, cw, ch, stroke, start, c1, f, fg, ul,
624                );
625            }
626        }
627    }
628}
629
630#[allow(clippy::too_many_arguments)]
631fn push_lines(
632    out: &mut Vec<Quad>,
633    quad: &dyn Fn(Rect, Color, QuadKind, [u32; 4]) -> Quad,
634    clip_id: ClipId,
635    ox: f32,
636    cy: f32,
637    cw: f32,
638    ch: f32,
639    stroke: f32,
640    start: usize,
641    end: usize,
642    f: u8,
643    fg: u32,
644    ul: u32,
645) {
646    let x = ox + start as f32 * cw;
647    let w = (end - start) as f32 * cw;
648    if f & (flags::UNDERLINE | flags::WAVY | flags::DOTTED) != 0 {
649        // The shape bits imply the line; its colour is its own where the
650        // cell says (SGR 58), else the foreground's.
651        let style = if f & flags::WAVY != 0 {
652            crate::spec::UnderlineStyle::Wavy
653        } else if f & flags::DOTTED != 0 {
654            crate::spec::UnderlineStyle::Dotted
655        } else {
656            crate::spec::UnderlineStyle::Solid
657        };
658        let color = Color::hex(if ul != 0 { ul } else { fg });
659        crate::deco::push_line(
660            out,
661            style,
662            x,
663            cy + ch - 2.0 * stroke,
664            w,
665            stroke,
666            color,
667            clip_id,
668        );
669    }
670    if f & flags::STRIKETHROUGH != 0 {
671        out.push(quad(
672            Rect::new(x, (cy + ch * 0.55).round(), w, stroke),
673            Color::hex(fg),
674            QuadKind::Solid,
675            [0; 4],
676        ));
677    }
678}
679
680/// The glyph for `ch` in `flags`'s variant, from the table or shaped and
681/// rasterized now — once per character and variant for the life of the
682/// table.
683#[allow(clippy::too_many_arguments)]
684fn lookup(
685    table: &mut StyleTable,
686    ch: char,
687    flags: u8,
688    style: &TextStyle,
689    res: &Resources,
690    fs: &mut FontSystem,
691    raster: &mut Raster,
692    atlas: &mut GlyphAtlas,
693    scale: f32,
694) -> Option<CellGlyph> {
695    let v = variant(flags);
696    let known = if (ch as u32) < 128 {
697        table.ascii[v * 128 + ch as usize]
698    } else {
699        table.other.get(&(ch, v as u8)).copied()
700    };
701    if let Some(g) = known {
702        return g;
703    }
704    let g = shape_cell(
705        ch,
706        flags,
707        style,
708        &table.family,
709        res,
710        fs,
711        raster,
712        atlas,
713        scale,
714        (table.cell_w, table.cell_h),
715    );
716    if (ch as u32) < 128 {
717        table.ascii[v * 128 + ch as usize] = Some(g);
718    } else {
719        table.other.insert((ch, v as u8), g);
720    }
721    g
722}
723
724/// Shapes one cell's character and rasterizes its glyph into the atlas —
725/// or, for a character the cell box draws (`boxdraw`), rasterizes the
726/// cell-sized mask and skips the font.
727///
728/// A character the style's family has no glyph for is another family's
729/// (`own` is the style's), and a cell is still a cell (F120): a
730/// monospaced face that has it is asked before the platform's fallback
731/// list, whose first name on macOS is a proportional one; a glyph wider
732/// than its cells — two for a wide one — is shaped again at the size it
733/// fits at, on the baseline it had; and what room is left is shared either
734/// side of it. The private use area is left as it falls: an icon is drawn
735/// to run over the blank after it.
736#[allow(clippy::too_many_arguments)]
737fn shape_cell(
738    ch: char,
739    flags: u8,
740    style: &TextStyle,
741    own: &str,
742    res: &Resources,
743    fs: &mut FontSystem,
744    raster: &mut Raster,
745    atlas: &mut GlyphAtlas,
746    scale: f32,
747    cell: (f32, f32),
748) -> Option<CellGlyph> {
749    if boxdraw::draws(ch) {
750        let (w, h) = (cell.0 as u32, cell.1 as u32);
751        let slot = atlas.get_or_insert_synth(ch, w, h, || boxdraw::raster(ch, w, h))?;
752        return Some(CellGlyph {
753            x: 0.0,
754            y: 0.0,
755            w: w as f32,
756            h: h as f32,
757            uv: [slot.x, slot.y, slot.w, slot.h],
758            kind: QuadKind::GlyphMask,
759        });
760    }
761    let mut buf = [0u8; 4];
762    let text: &str = ch.encode_utf8(&mut buf);
763    let mut g = shape_one(style, text, flags, None, 1.0, res, fs, scale)?;
764    let mut dx = 0.0;
765    let theirs = fs
766        .db()
767        .face(g.font)
768        .is_some_and(|f| !f.families.iter().any(|(name, _)| name == own));
769    if theirs && !private_use(ch) {
770        let mut family = None;
771        // The app's own choice of fallback stands, whatever its pitch.
772        let chosen = fs.db().face(g.font).is_some_and(|f| {
773            f.families
774                .iter()
775                .any(|(name, _)| res.fallback.contains(name))
776        });
777        if !chosen
778            && !fs.is_monospace(g.font)
779            && let Some(m) = shape_one(style, text, flags, MONO, 1.0, res, fs, scale)
780            && m.glyph != 0
781            && fs.is_monospace(m.font)
782        {
783            family = MONO;
784            g = m;
785        }
786        let span = cell.0 * if flags & flags::WIDE != 0 { 2.0 } else { 1.0 };
787        if g.advance > span + 0.5 {
788            let fit = span / g.advance;
789            if let Some(f) = shape_one(style, text, flags, family, fit, res, fs, scale) {
790                g = Shaped {
791                    line_y: g.line_y,
792                    ..f
793                };
794            }
795        }
796        dx = ((span - g.advance) / 2.0).round().max(0.0);
797    }
798    let slot = raster_glyph(g.key, fs, raster, atlas)?;
799    Some(CellGlyph {
800        x: dx + g.x as f32 + slot.left as f32,
801        y: g.line_y.round() + g.y as f32 - slot.top as f32,
802        w: slot.w as f32,
803        h: slot.h as f32,
804        uv: [slot.x, slot.y, slot.w, slot.h],
805        kind: glyph_kind(&slot),
806    })
807}
808
809/// The generic monospaced family, in place of the style's.
810const MONO: Option<cosmic_text::Family<'static>> = Some(cosmic_text::Family::Monospace);
811
812/// Whether `ch` is in a private use area: an icon font's.
813fn private_use(ch: char) -> bool {
814    matches!(ch as u32, 0xE000..=0xF8FF | 0xF_0000..=0x10_FFFF)
815}
816
817/// One shaped glyph: its cache key, its face and its id there (0 where
818/// the face has none), its advance, and where it sits (physical x, y and
819/// the baseline).
820struct Shaped {
821    key: cosmic_text::CacheKey,
822    font: cosmic_text::fontdb::ID,
823    glyph: u16,
824    advance: f32,
825    x: i32,
826    y: i32,
827    line_y: f32,
828}
829
830/// Shapes `text` alone in `style` at `scale` and returns its first glyph
831/// — in `family` where one is given in place of the style's, at `fit`
832/// times the style's size.
833#[allow(clippy::too_many_arguments)]
834fn shape_one(
835    style: &TextStyle,
836    text: &str,
837    flags: u8,
838    family: Option<cosmic_text::Family<'_>>,
839    fit: f32,
840    res: &Resources,
841    fs: &mut FontSystem,
842    scale: f32,
843) -> Option<Shaped> {
844    let metrics = Metrics::new(style.size * scale * fit, style.line_height * scale);
845    let mut buffer = Buffer::new(fs, metrics);
846    buffer.set_size(None, None);
847    // Bold at a weight the family has a face for, never another family's.
848    let mut attrs = match family {
849        Some(family) => crate::weights::Weights::CSS
850            .apply(Attrs::new().family(family), flags & flags::BOLD != 0),
851        None => res.weights_of(style.family).apply(
852            Attrs::new().family(res.family_of(style.family)),
853            flags & flags::BOLD != 0,
854        ),
855    };
856    if flags & flags::ITALIC != 0 {
857        attrs = attrs.style(FontStyle::Italic);
858    }
859    buffer.set_text(text, &attrs, Shaping::Advanced, None);
860    buffer.shape_until_scroll(fs, false);
861    let run = buffer.layout_runs().next()?;
862    let glyph = run.glyphs.first()?;
863    let physical = glyph.physical((0.0, 0.0), 1.0);
864    Some(Shaped {
865        key: physical.cache_key,
866        font: glyph.font_id,
867        glyph: glyph.glyph_id,
868        advance: glyph.w,
869        x: physical.x,
870        y: physical.y,
871        line_y: run.line_y,
872    })
873}