use cosmic_text::{Attrs, Buffer, FontSystem, Shaping, Style as FontStyle};
use rustc_hash::FxHashMap;
use crate::atlas::GlyphAtlas;
use crate::color::Color;
use crate::display::{Clip, ClipId, Quad, QuadKind};
use crate::geom::{Rect, Size, Vec2};
use crate::key::Key;
use crate::resources::Resources;
use crate::spec::TextStyle;
use crate::text::{Raster, glyph_kind, raster_glyph};
mod boxdraw;
pub mod flags {
pub const BOLD: u8 = 1;
pub const ITALIC: u8 = 2;
pub const UNDERLINE: u8 = 4;
pub const STRIKETHROUGH: u8 = 8;
pub const WIDE: u8 = 16;
pub const WAVY: u8 = 32;
pub const DOTTED: u8 = 64;
pub const LINES: u8 = UNDERLINE | STRIKETHROUGH | WAVY | DOTTED;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct Cell {
pub ch: char,
pub fg: u32,
pub bg: u32,
pub flags: u8,
pub ul: u32,
}
impl Cell {
pub const fn new(ch: char, fg: u32, bg: u32) -> Self {
Self {
ch,
fg,
bg,
flags: 0,
ul: 0,
}
}
pub const fn underline_color(mut self, ul: u32) -> Self {
self.flags |= flags::UNDERLINE;
self.ul = ul;
self
}
pub const fn with(mut self, flags: u8) -> Self {
self.flags |= flags;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CursorShape {
Block,
Bar,
Underline,
}
impl CursorShape {
pub const NAMES: &[&str] = &["block", "bar", "underline"];
pub fn from_index(i: usize) -> Option<Self> {
match i {
0 => Some(Self::Block),
1 => Some(Self::Bar),
2 => Some(Self::Underline),
_ => None,
}
}
pub fn from_name(s: &str) -> Option<Self> {
Self::NAMES
.iter()
.position(|n| *n == s)
.and_then(Self::from_index)
}
}
#[derive(Clone, Copy, Debug)]
pub struct CellGrid<'a> {
pub rows: usize,
pub cols: usize,
pub cells: &'a [Cell],
pub style: TextStyle,
pub cursor: Option<(usize, usize, CursorShape, Color)>,
pub origin_line: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CellsId(pub u32);
struct Entry {
key: Key,
rows: usize,
cols: usize,
cells: Vec<Cell>,
style: TextStyle,
cursor: Option<(usize, usize, CursorShape, Color)>,
origin_line: u64,
}
#[derive(Clone, Copy)]
struct CellGlyph {
x: f32,
y: f32,
w: f32,
h: f32,
uv: [u32; 4],
kind: QuadKind,
}
struct StyleTable {
cell_w: f32,
cell_h: f32,
ascii: Vec<Option<Option<CellGlyph>>>,
other: FxHashMap<(char, u8), Option<CellGlyph>>,
family: String,
epoch: u64,
}
const VARIANTS: usize = 4;
fn variant(flags: u8) -> usize {
(flags & (flags::BOLD | flags::ITALIC)) as usize
}
pub struct CellStore {
frame: crate::retain::Kept<Entry>,
tables: FxHashMap<u64, StyleTable>,
scale: f32,
}
impl Default for CellStore {
fn default() -> Self {
Self::new()
}
}
impl CellStore {
pub fn new() -> Self {
Self {
frame: Default::default(),
tables: FxHashMap::default(),
scale: 1.0,
}
}
pub(crate) fn forget_shaped(&mut self) {
self.tables.clear();
}
pub(crate) fn begin_frame(&mut self, scale: f32) {
if (scale - self.scale).abs() > f32::EPSILON {
self.tables.clear();
}
self.scale = scale;
self.frame.begin(true);
}
pub(crate) fn find(&self, key: Key, prev: bool) -> Option<CellsId> {
self.list(prev)
.iter()
.position(|e| e.key == key)
.map(|i| CellsId(i as u32))
}
fn list(&self, prev: bool) -> &[Entry] {
if prev { self.frame.prev() } else { &self.frame }
}
fn entry(&self, id: CellsId, prev: bool) -> &Entry {
&self.list(prev)[id.0 as usize]
}
pub(crate) fn add(&mut self, key: Key, grid: &CellGrid<'_>) -> CellsId {
let n = grid.rows * grid.cols;
let mut cells = Vec::with_capacity(n);
cells.extend_from_slice(&grid.cells[..grid.cells.len().min(n)]);
cells.resize(n, Cell::default());
self.frame.push(Entry {
key,
rows: grid.rows,
cols: grid.cols,
cells,
style: grid.style,
cursor: grid.cursor,
origin_line: grid.origin_line,
});
CellsId((self.frame.len() - 1) as u32)
}
fn table_key(style: &TextStyle, scale: f32) -> u64 {
crate::text::TextSystem::style_key("", style, scale)
}
fn table(&mut self, style: &TextStyle, res: &Resources, fs: &mut FontSystem) -> u64 {
let key = Self::table_key(style, self.scale);
if !self.tables.contains_key(&key) {
let scale = self.scale;
let cell_w = shape_one(style, "M", 0, None, 1.0, res, fs, scale)
.map_or(style.size * scale * 0.6, |g| g.advance)
.round()
.max(1.0);
self.tables.insert(
key,
StyleTable {
cell_w,
cell_h: match (style.line_height * scale).round() {
h if h.is_finite() => h.max(1.0),
_ => 1.0,
},
ascii: vec![None; VARIANTS * 128],
other: FxHashMap::default(),
family: fs
.db()
.family_name(&res.family_of(style.family))
.to_string(),
epoch: u64::MAX,
},
);
}
key
}
pub(crate) fn cell_size(
&mut self,
id: CellsId,
prev: bool,
res: &Resources,
fs: &mut FontSystem,
) -> Size {
let style = self.entry(id, prev).style;
let key = self.table(&style, res, fs);
let t = &self.tables[&key];
Size::new(t.cell_w / self.scale, t.cell_h / self.scale)
}
pub(crate) fn origin_line(&self, id: CellsId, prev: bool) -> u64 {
self.entry(id, prev).origin_line
}
pub(crate) fn cell_char(
&self,
id: CellsId,
row: usize,
col: usize,
prev: bool,
) -> Option<(char, bool)> {
let e = self.entry(id, prev);
if row >= e.rows || col >= e.cols {
return None;
}
let c = e.cells[row * e.cols + col];
Some((c.ch, self.is_spacer(e, row, col)))
}
fn is_spacer(&self, e: &Entry, row: usize, col: usize) -> bool {
col > 0 && e.cells[row * e.cols + col - 1].flags & flags::WIDE != 0
}
pub(crate) fn word_at(
&self,
id: CellsId,
row: usize,
col: usize,
prev: bool,
) -> Option<(usize, usize)> {
let e = self.entry(id, prev);
if row >= e.rows || col >= e.cols {
return None;
}
let class = |c: usize| -> u8 {
let c = if self.is_spacer(e, row, c) { c - 1 } else { c };
let ch = e.cells[row * e.cols + c].ch;
if ch == '\0' || ch.is_whitespace() {
1
} else if ch.is_alphanumeric() || ch == '_' {
0
} else {
2
}
};
let here = class(col);
let mut from = col;
while from > 0 && class(from - 1) == here {
from -= 1;
}
let mut to = col + 1;
while to < e.cols && class(to) == here {
to += 1;
}
Some((from, to))
}
pub(crate) fn dims(&self, id: CellsId, prev: bool) -> (usize, usize) {
let e = self.entry(id, prev);
(e.rows, e.cols)
}
pub(crate) fn value(&self, id: CellsId) -> String {
let e = &self.frame[id.0 as usize];
let mut out = String::with_capacity(e.rows * (e.cols + 1));
for r in 0..e.rows {
let row = &e.cells[r * e.cols..(r + 1) * e.cols];
let end = row
.iter()
.rposition(|c| c.ch != ' ' && c.ch != '\0')
.map_or(0, |i| i + 1);
for c in &row[..end] {
out.push(if c.ch == '\0' { ' ' } else { c.ch });
}
if r + 1 < e.rows {
out.push('\n');
}
}
out
}
pub(crate) fn size(&mut self, id: CellsId, res: &Resources, fs: &mut FontSystem) -> Size {
let (rows, cols, style) = {
let e = &self.frame[id.0 as usize];
(e.rows, e.cols, e.style)
};
let key = self.table(&style, res, fs);
let t = &self.tables[&key];
Size::new(
cols as f32 * t.cell_w / self.scale,
rows as f32 * t.cell_h / self.scale,
)
}
#[allow(clippy::too_many_arguments, clippy::needless_range_loop)]
pub(crate) fn emit(
&mut self,
id: CellsId,
origin: Vec2,
clip: Clip,
clip_id: ClipId,
res: &Resources,
fs: &mut FontSystem,
raster: &mut Raster,
atlas: &mut GlyphAtlas,
out: &mut Vec<Quad>,
sel: Option<(&crate::select::CellSelection, Color)>,
) {
let scale = self.scale;
let style = self.frame[id.0 as usize].style;
let key = self.table(&style, res, fs);
let ox = crate::geom::snap_px(origin.x * scale);
let oy = crate::geom::snap_px(origin.y * scale);
let entry = &self.frame[id.0 as usize];
let table = self.tables.get_mut(&key).expect("just built");
if table.epoch != atlas.stamp {
table.ascii.iter_mut().for_each(|g| *g = None);
table.other.clear();
table.epoch = atlas.stamp;
}
let (cw, ch) = (table.cell_w, table.cell_h);
let quad = |rect: Rect, color: Color, kind: QuadKind, uv: [u32; 4]| Quad {
rect,
color,
border_color: Color::TRANSPARENT,
radius: [0.0; 4],
border_w: 0.0,
blur: 0.0,
kind,
clip: clip_id,
uv,
};
let r0 = (((clip.rect.y - oy) / ch).floor().max(0.0)) as usize;
let r1 = (((clip.rect.y + clip.rect.h - oy) / ch).ceil().max(0.0) as usize).min(entry.rows);
let c0 = (((clip.rect.x - ox) / cw).floor().max(0.0)) as usize;
let c1 = (((clip.rect.x + clip.rect.w - ox) / cw).ceil().max(0.0) as usize).min(entry.cols);
if r0 >= r1 || c0 >= c1 {
return;
}
let stroke = (scale).round().max(1.0);
if let Some((sel, tint)) = sel {
for r in r0..r1 {
let line = entry.origin_line + r as u64;
let Some((from, to)) = sel.cols_on(line, entry.cols) else {
continue;
};
let (from, to) = (from.max(c0), to.min(c1));
if from >= to {
continue;
}
out.push(quad(
Rect::new(
ox + from as f32 * cw,
oy + r as f32 * ch,
(to - from) as f32 * cw,
ch,
),
tint,
QuadKind::Solid,
[0; 4],
));
}
}
for r in r0..r1 {
let row = &entry.cells[r * entry.cols..(r + 1) * entry.cols];
let cy = oy + r as f32 * ch;
let mut run_start = c0;
let mut run_bg = row[c0].bg;
for c in c0..=c1 {
let bg = if c < c1 { row[c].bg } else { !run_bg };
if bg != run_bg {
if run_bg & 0xff != 0 {
out.push(quad(
Rect::new(
ox + run_start as f32 * cw,
cy,
(c - run_start) as f32 * cw,
ch,
),
Color::hex(run_bg),
QuadKind::Solid,
[0; 4],
));
}
run_start = c;
run_bg = bg;
}
}
if let Some((cr, cc, shape, color)) = entry.cursor
&& cr == r
&& cc >= c0
&& cc < c1
{
let cx = ox + cc as f32 * cw;
let rect = match shape {
CursorShape::Block => Rect::new(cx, cy, cw, ch),
CursorShape::Bar => Rect::new(cx, cy, 2.0 * stroke, ch),
CursorShape::Underline => {
Rect::new(cx, cy + ch - 2.0 * stroke, cw, 2.0 * stroke)
}
};
out.push(quad(rect, color, QuadKind::Solid, [0; 4]));
}
let mut line_run: Option<(usize, u8, u32, u32)> = None;
for c in c0..c1 {
let cell = &row[c];
let cx = ox + c as f32 * cw;
if cell.ch != ' ' && cell.ch != '\0' {
let g = lookup(
table, cell.ch, cell.flags, &style, res, fs, raster, atlas, scale,
);
if let Some(g) = g {
out.push(quad(
Rect::new(cx + g.x, cy + g.y, g.w, g.h),
Color::hex(cell.fg),
g.kind,
g.uv,
));
}
}
let lines = cell.flags & flags::LINES;
let same = line_run
.is_some_and(|(_, f, fg, ul)| f == lines && fg == cell.fg && ul == cell.ul);
if !same {
if let Some((start, f, fg, ul)) = line_run.take()
&& f != 0
{
push_lines(
out, &quad, clip_id, ox, cy, cw, ch, stroke, start, c, f, fg, ul,
);
}
line_run = Some((c, lines, cell.fg, cell.ul));
}
}
if let Some((start, f, fg, ul)) = line_run
&& f != 0
{
push_lines(
out, &quad, clip_id, ox, cy, cw, ch, stroke, start, c1, f, fg, ul,
);
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn push_lines(
out: &mut Vec<Quad>,
quad: &dyn Fn(Rect, Color, QuadKind, [u32; 4]) -> Quad,
clip_id: ClipId,
ox: f32,
cy: f32,
cw: f32,
ch: f32,
stroke: f32,
start: usize,
end: usize,
f: u8,
fg: u32,
ul: u32,
) {
let x = ox + start as f32 * cw;
let w = (end - start) as f32 * cw;
if f & (flags::UNDERLINE | flags::WAVY | flags::DOTTED) != 0 {
let style = if f & flags::WAVY != 0 {
crate::spec::UnderlineStyle::Wavy
} else if f & flags::DOTTED != 0 {
crate::spec::UnderlineStyle::Dotted
} else {
crate::spec::UnderlineStyle::Solid
};
let color = Color::hex(if ul != 0 { ul } else { fg });
crate::deco::push_line(
out,
style,
x,
cy + ch - 2.0 * stroke,
w,
stroke,
color,
clip_id,
);
}
if f & flags::STRIKETHROUGH != 0 {
out.push(quad(
Rect::new(x, (cy + ch * 0.55).round(), w, stroke),
Color::hex(fg),
QuadKind::Solid,
[0; 4],
));
}
}
#[allow(clippy::too_many_arguments)]
fn lookup(
table: &mut StyleTable,
ch: char,
flags: u8,
style: &TextStyle,
res: &Resources,
fs: &mut FontSystem,
raster: &mut Raster,
atlas: &mut GlyphAtlas,
scale: f32,
) -> Option<CellGlyph> {
let v = variant(flags);
let known = if (ch as u32) < 128 {
table.ascii[v * 128 + ch as usize]
} else {
table.other.get(&(ch, v as u8)).copied()
};
if let Some(g) = known {
return g;
}
let g = shape_cell(
ch,
flags,
style,
&table.family,
res,
fs,
raster,
atlas,
scale,
(table.cell_w, table.cell_h),
);
if (ch as u32) < 128 {
table.ascii[v * 128 + ch as usize] = Some(g);
} else {
table.other.insert((ch, v as u8), g);
}
g
}
#[allow(clippy::too_many_arguments)]
fn shape_cell(
ch: char,
flags: u8,
style: &TextStyle,
own: &str,
res: &Resources,
fs: &mut FontSystem,
raster: &mut Raster,
atlas: &mut GlyphAtlas,
scale: f32,
cell: (f32, f32),
) -> Option<CellGlyph> {
if boxdraw::draws(ch) {
let (w, h) = (cell.0 as u32, cell.1 as u32);
let slot = atlas.get_or_insert_synth(ch, w, h, || boxdraw::raster(ch, w, h))?;
return Some(CellGlyph {
x: 0.0,
y: 0.0,
w: w as f32,
h: h as f32,
uv: [slot.x, slot.y, slot.w, slot.h],
kind: QuadKind::GlyphMask,
});
}
let mut buf = [0u8; 4];
let text: &str = ch.encode_utf8(&mut buf);
let mut g = shape_one(style, text, flags, None, 1.0, res, fs, scale)?;
let mut dx = 0.0;
let theirs = fs
.db()
.face(g.font)
.is_some_and(|f| !f.families.iter().any(|(name, _)| name == own));
if theirs && !private_use(ch) {
let mut family = None;
let chosen = fs.db().face(g.font).is_some_and(|f| {
f.families
.iter()
.any(|(name, _)| res.fallback.contains(name))
});
if !chosen
&& !fs.is_monospace(g.font)
&& let Some(m) = shape_one(style, text, flags, MONO, 1.0, res, fs, scale)
&& m.glyph != 0
&& fs.is_monospace(m.font)
{
family = MONO;
g = m;
}
let span = cell.0 * if flags & flags::WIDE != 0 { 2.0 } else { 1.0 };
if g.advance > span + 0.5 {
let fit = span / g.advance;
if let Some(f) = shape_one(style, text, flags, family, fit, res, fs, scale) {
g = Shaped {
line_y: g.line_y,
..f
};
}
}
dx = ((span - g.advance) / 2.0).round().max(0.0);
}
let slot = raster_glyph(g.key, fs, raster, atlas)?;
Some(CellGlyph {
x: dx + g.x as f32 + slot.left as f32,
y: g.line_y.round() + g.y as f32 - slot.top as f32,
w: slot.w as f32,
h: slot.h as f32,
uv: [slot.x, slot.y, slot.w, slot.h],
kind: glyph_kind(&slot),
})
}
const MONO: Option<cosmic_text::Family<'static>> = Some(cosmic_text::Family::Monospace);
fn private_use(ch: char) -> bool {
matches!(ch as u32, 0xE000..=0xF8FF | 0xF_0000..=0x10_FFFF)
}
struct Shaped {
key: cosmic_text::CacheKey,
font: cosmic_text::fontdb::ID,
glyph: u16,
advance: f32,
x: i32,
y: i32,
line_y: f32,
}
#[allow(clippy::too_many_arguments)]
fn shape_one(
style: &TextStyle,
text: &str,
flags: u8,
family: Option<cosmic_text::Family<'_>>,
fit: f32,
res: &Resources,
fs: &mut FontSystem,
scale: f32,
) -> Option<Shaped> {
let metrics = crate::text::shaper_metrics(style.size * scale * fit, style.line_height * scale);
let mut buffer = Buffer::new(fs, metrics);
buffer.set_size(None, None);
let mut attrs = match family {
Some(family) => crate::weights::Weights::CSS
.apply(Attrs::new().family(family), flags & flags::BOLD != 0),
None => res.weights_of(style.family).apply(
Attrs::new().family(res.family_of(style.family)),
flags & flags::BOLD != 0,
),
};
if flags & flags::ITALIC != 0 {
attrs = attrs.style(FontStyle::Italic);
}
buffer.set_text(text, &attrs, Shaping::Advanced, None);
buffer.shape_until_scroll(fs, false);
let run = buffer.layout_runs().next()?;
let glyph = run.glyphs.first()?;
let physical = glyph.physical((0.0, 0.0), 1.0);
Some(Shaped {
key: physical.cache_key,
font: glyph.font_id,
glyph: glyph.glyph_id,
advance: glyph.w,
x: physical.x,
y: physical.y,
line_y: run.line_y,
})
}