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