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