Skip to main content

kui_core/
cells.rs

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