1use 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
63pub 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 pub const WIDE: u8 = 16;
71 pub const WAVY: u8 = 32;
74 pub const DOTTED: u8 = 64;
76 pub const LINES: u8 = UNDERLINE | STRIKETHROUGH | WAVY | DOTTED;
79}
80
81#[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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
120pub enum CursorShape {
121 Block,
123 Bar,
125 Underline,
127}
128
129impl CursorShape {
130 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#[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 pub cursor: Option<(usize, usize, CursorShape, Color)>,
162 pub origin_line: u64,
172}
173
174#[derive(Clone, Copy, Debug, PartialEq, Eq)]
176pub struct CellsId(pub u32);
177
178struct Entry {
179 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#[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
203struct StyleTable {
206 cell_w: f32,
207 cell_h: f32,
208 ascii: Vec<Option<Option<CellGlyph>>>,
209 other: FxHashMap<(char, u8), Option<CellGlyph>>,
210 own: [Option<Option<cosmic_text::fontdb::ID>>; VARIANTS],
213 epoch: u64,
215}
216
217const VARIANTS: usize = 4;
218
219fn variant(flags: u8) -> usize {
220 (flags & (flags::BOLD | flags::ITALIC)) as usize
221}
222
223pub struct CellStore {
228 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 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 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 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 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 pub(crate) fn origin_line(&self, id: CellsId, prev: bool) -> u64 {
343 self.entry(id, prev).origin_line
344 }
345
346 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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#[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#[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 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
802const MONO: Option<cosmic_text::Family<'static>> = Some(cosmic_text::Family::Monospace);
804
805fn private_use(ch: char) -> bool {
807 matches!(ch as u32, 0xE000..=0xF8FF | 0xF_0000..=0x10_FFFF)
808}
809
810struct 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#[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 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}