1use cosmic_text::{Attrs, Buffer, FontSystem, 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 family: String,
216 epoch: u64,
218}
219
220const VARIANTS: usize = 4;
221
222fn variant(flags: u8) -> usize {
223 (flags & (flags::BOLD | flags::ITALIC)) as usize
224}
225
226pub struct CellStore {
231 frame: crate::retain::Kept<Entry>,
234 tables: FxHashMap<u64, StyleTable>,
235 scale: f32,
236}
237
238impl Default for CellStore {
239 fn default() -> Self {
240 Self::new()
241 }
242}
243
244impl CellStore {
245 pub fn new() -> Self {
246 Self {
247 frame: Default::default(),
248 tables: FxHashMap::default(),
249 scale: 1.0,
250 }
251 }
252
253 pub(crate) fn forget_shaped(&mut self) {
256 self.tables.clear();
257 }
258
259 pub(crate) fn begin_frame(&mut self, scale: f32) {
260 if (scale - self.scale).abs() > f32::EPSILON {
261 self.tables.clear();
262 }
263 self.scale = scale;
264 self.frame.begin(true);
265 }
266
267 pub(crate) fn find(&self, key: Key, prev: bool) -> Option<CellsId> {
269 self.list(prev)
270 .iter()
271 .position(|e| e.key == key)
272 .map(|i| CellsId(i as u32))
273 }
274
275 fn list(&self, prev: bool) -> &[Entry] {
276 if prev { self.frame.prev() } else { &self.frame }
277 }
278
279 fn entry(&self, id: CellsId, prev: bool) -> &Entry {
280 &self.list(prev)[id.0 as usize]
281 }
282
283 pub(crate) fn add(&mut self, key: Key, grid: &CellGrid<'_>) -> CellsId {
284 let n = grid.rows * grid.cols;
285 let mut cells = Vec::with_capacity(n);
286 cells.extend_from_slice(&grid.cells[..grid.cells.len().min(n)]);
287 cells.resize(n, Cell::default());
288 self.frame.push(Entry {
289 key,
290 rows: grid.rows,
291 cols: grid.cols,
292 cells,
293 style: grid.style,
294 cursor: grid.cursor,
295 origin_line: grid.origin_line,
296 });
297 CellsId((self.frame.len() - 1) as u32)
298 }
299
300 fn table_key(style: &TextStyle, scale: f32) -> u64 {
301 crate::text::TextSystem::style_key("", style, scale)
302 }
303
304 fn table(&mut self, style: &TextStyle, res: &Resources, fs: &mut FontSystem) -> u64 {
308 let key = Self::table_key(style, self.scale);
309 if !self.tables.contains_key(&key) {
310 let scale = self.scale;
311 let cell_w = shape_one(style, "M", 0, None, 1.0, res, fs, scale)
312 .map_or(style.size * scale * 0.6, |g| g.advance)
313 .round()
314 .max(1.0);
315 self.tables.insert(
316 key,
317 StyleTable {
318 cell_w,
319 cell_h: match (style.line_height * scale).round() {
323 h if h.is_finite() => h.max(1.0),
324 _ => 1.0,
325 },
326 ascii: vec![None; VARIANTS * 128],
327 other: FxHashMap::default(),
328 family: fs
329 .db()
330 .family_name(&res.family_of(style.family))
331 .to_string(),
332 epoch: u64::MAX,
333 },
334 );
335 }
336 key
337 }
338
339 pub(crate) fn cell_size(
341 &mut self,
342 id: CellsId,
343 prev: bool,
344 res: &Resources,
345 fs: &mut FontSystem,
346 ) -> Size {
347 let style = self.entry(id, prev).style;
348 let key = self.table(&style, res, fs);
349 let t = &self.tables[&key];
350 Size::new(t.cell_w / self.scale, t.cell_h / self.scale)
351 }
352
353 pub(crate) fn origin_line(&self, id: CellsId, prev: bool) -> u64 {
355 self.entry(id, prev).origin_line
356 }
357
358 pub(crate) fn cell_char(
366 &self,
367 id: CellsId,
368 row: usize,
369 col: usize,
370 prev: bool,
371 ) -> Option<(char, bool)> {
372 let e = self.entry(id, prev);
373 if row >= e.rows || col >= e.cols {
374 return None;
375 }
376 let c = e.cells[row * e.cols + col];
377 Some((c.ch, self.is_spacer(e, row, col)))
378 }
379
380 fn is_spacer(&self, e: &Entry, row: usize, col: usize) -> bool {
382 col > 0 && e.cells[row * e.cols + col - 1].flags & flags::WIDE != 0
383 }
384
385 pub(crate) fn word_at(
391 &self,
392 id: CellsId,
393 row: usize,
394 col: usize,
395 prev: bool,
396 ) -> Option<(usize, usize)> {
397 let e = self.entry(id, prev);
398 if row >= e.rows || col >= e.cols {
399 return None;
400 }
401 let class = |c: usize| -> u8 {
402 let c = if self.is_spacer(e, row, c) { c - 1 } else { c };
405 let ch = e.cells[row * e.cols + c].ch;
406 if ch == '\0' || ch.is_whitespace() {
407 1
408 } else if ch.is_alphanumeric() || ch == '_' {
409 0
410 } else {
411 2
412 }
413 };
414 let here = class(col);
415 let mut from = col;
416 while from > 0 && class(from - 1) == here {
417 from -= 1;
418 }
419 let mut to = col + 1;
420 while to < e.cols && class(to) == here {
421 to += 1;
422 }
423 Some((from, to))
424 }
425
426 pub(crate) fn dims(&self, id: CellsId, prev: bool) -> (usize, usize) {
427 let e = self.entry(id, prev);
428 (e.rows, e.cols)
429 }
430
431 pub(crate) fn value(&self, id: CellsId) -> String {
434 let e = &self.frame[id.0 as usize];
435 let mut out = String::with_capacity(e.rows * (e.cols + 1));
436 for r in 0..e.rows {
437 let row = &e.cells[r * e.cols..(r + 1) * e.cols];
438 let end = row
439 .iter()
440 .rposition(|c| c.ch != ' ' && c.ch != '\0')
441 .map_or(0, |i| i + 1);
442 for c in &row[..end] {
443 out.push(if c.ch == '\0' { ' ' } else { c.ch });
444 }
445 if r + 1 < e.rows {
446 out.push('\n');
447 }
448 }
449 out
450 }
451
452 pub(crate) fn size(&mut self, id: CellsId, res: &Resources, fs: &mut FontSystem) -> Size {
454 let (rows, cols, style) = {
455 let e = &self.frame[id.0 as usize];
456 (e.rows, e.cols, e.style)
457 };
458 let key = self.table(&style, res, fs);
459 let t = &self.tables[&key];
460 Size::new(
461 cols as f32 * t.cell_w / self.scale,
462 rows as f32 * t.cell_h / self.scale,
463 )
464 }
465
466 #[allow(clippy::too_many_arguments, clippy::needless_range_loop)]
470 pub(crate) fn emit(
471 &mut self,
472 id: CellsId,
473 origin: Vec2,
474 clip: Clip,
475 clip_id: ClipId,
476 res: &Resources,
477 fs: &mut FontSystem,
478 raster: &mut Raster,
479 atlas: &mut GlyphAtlas,
480 out: &mut Vec<Quad>,
481 sel: Option<(&crate::select::CellSelection, Color)>,
486 ) {
487 let scale = self.scale;
488 let style = self.frame[id.0 as usize].style;
489 let key = self.table(&style, res, fs);
490 let ox = crate::geom::snap_px(origin.x * scale);
491 let oy = crate::geom::snap_px(origin.y * scale);
492 let entry = &self.frame[id.0 as usize];
493 let table = self.tables.get_mut(&key).expect("just built");
494 if table.epoch != atlas.stamp {
495 table.ascii.iter_mut().for_each(|g| *g = None);
499 table.other.clear();
500 table.epoch = atlas.stamp;
501 }
502 let (cw, ch) = (table.cell_w, table.cell_h);
503 let quad = |rect: Rect, color: Color, kind: QuadKind, uv: [u32; 4]| Quad {
504 rect,
505 color,
506 border_color: Color::TRANSPARENT,
507 radius: [0.0; 4],
508 border_w: 0.0,
509 blur: 0.0,
510 kind,
511 clip: clip_id,
512 uv,
513 };
514 let vis = clip.visible();
517 let r0 = (((vis.y - oy) / ch).floor().max(0.0)) as usize;
518 let r1 = (((vis.y + vis.h - oy) / ch).ceil().max(0.0) as usize).min(entry.rows);
519 let c0 = (((vis.x - ox) / cw).floor().max(0.0)) as usize;
520 let c1 = (((vis.x + vis.w - ox) / cw).ceil().max(0.0) as usize).min(entry.cols);
521 if r0 >= r1 || c0 >= c1 {
522 return;
523 }
524 let stroke = (scale).round().max(1.0);
525 if let Some((sel, tint)) = sel {
530 for r in r0..r1 {
531 let line = entry.origin_line + r as u64;
532 let Some((from, to)) = sel.cols_on(line, entry.cols) else {
533 continue;
534 };
535 let (from, to) = (from.max(c0), to.min(c1));
536 if from >= to {
537 continue;
538 }
539 out.push(quad(
540 Rect::new(
541 ox + from as f32 * cw,
542 oy + r as f32 * ch,
543 (to - from) as f32 * cw,
544 ch,
545 ),
546 tint,
547 QuadKind::Solid,
548 [0; 4],
549 ));
550 }
551 }
552 for r in r0..r1 {
553 let row = &entry.cells[r * entry.cols..(r + 1) * entry.cols];
554 let cy = oy + r as f32 * ch;
555 let mut run_start = c0;
557 let mut run_bg = row[c0].bg;
558 for c in c0..=c1 {
559 let bg = if c < c1 { row[c].bg } else { !run_bg };
560 if bg != run_bg {
561 if run_bg & 0xff != 0 {
562 out.push(quad(
563 Rect::new(
564 ox + run_start as f32 * cw,
565 cy,
566 (c - run_start) as f32 * cw,
567 ch,
568 ),
569 Color::hex(run_bg),
570 QuadKind::Solid,
571 [0; 4],
572 ));
573 }
574 run_start = c;
575 run_bg = bg;
576 }
577 }
578 if let Some((cr, cc, shape, color)) = entry.cursor
580 && cr == r
581 && cc >= c0
582 && cc < c1
583 {
584 let cx = ox + cc as f32 * cw;
585 let rect = match shape {
586 CursorShape::Block => Rect::new(cx, cy, cw, ch),
587 CursorShape::Bar => Rect::new(cx, cy, 2.0 * stroke, ch),
588 CursorShape::Underline => {
589 Rect::new(cx, cy + ch - 2.0 * stroke, cw, 2.0 * stroke)
590 }
591 };
592 out.push(quad(rect, color, QuadKind::Solid, [0; 4]));
593 }
594 let mut line_run: Option<(usize, u8, u32, u32)> = None;
597 for c in c0..c1 {
598 let cell = &row[c];
599 let cx = ox + c as f32 * cw;
600 if cell.ch != ' ' && cell.ch != '\0' {
601 let g = lookup(
602 table, cell.ch, cell.flags, &style, res, fs, raster, atlas, scale,
603 );
604 if let Some(g) = g {
605 out.push(quad(
606 Rect::new(cx + g.x, cy + g.y, g.w, g.h),
607 Color::hex(cell.fg),
608 g.kind,
609 g.uv,
610 ));
611 }
612 }
613 let lines = cell.flags & flags::LINES;
614 let same = line_run
615 .is_some_and(|(_, f, fg, ul)| f == lines && fg == cell.fg && ul == cell.ul);
616 if !same {
617 if let Some((start, f, fg, ul)) = line_run.take()
618 && f != 0
619 {
620 push_lines(
621 out, &quad, clip_id, ox, cy, cw, ch, stroke, start, c, f, fg, ul,
622 );
623 }
624 line_run = Some((c, lines, cell.fg, cell.ul));
625 }
626 }
627 if let Some((start, f, fg, ul)) = line_run
628 && f != 0
629 {
630 push_lines(
631 out, &quad, clip_id, ox, cy, cw, ch, stroke, start, c1, f, fg, ul,
632 );
633 }
634 }
635 }
636}
637
638#[allow(clippy::too_many_arguments)]
639fn push_lines(
640 out: &mut Vec<Quad>,
641 quad: &dyn Fn(Rect, Color, QuadKind, [u32; 4]) -> Quad,
642 clip_id: ClipId,
643 ox: f32,
644 cy: f32,
645 cw: f32,
646 ch: f32,
647 stroke: f32,
648 start: usize,
649 end: usize,
650 f: u8,
651 fg: u32,
652 ul: u32,
653) {
654 let x = ox + start as f32 * cw;
655 let w = (end - start) as f32 * cw;
656 if f & (flags::UNDERLINE | flags::WAVY | flags::DOTTED) != 0 {
657 let style = if f & flags::WAVY != 0 {
660 crate::spec::UnderlineStyle::Wavy
661 } else if f & flags::DOTTED != 0 {
662 crate::spec::UnderlineStyle::Dotted
663 } else {
664 crate::spec::UnderlineStyle::Solid
665 };
666 let color = Color::hex(if ul != 0 { ul } else { fg });
667 crate::deco::push_line(
668 out,
669 style,
670 x,
671 cy + ch - 2.0 * stroke,
672 w,
673 stroke,
674 color,
675 clip_id,
676 );
677 }
678 if f & flags::STRIKETHROUGH != 0 {
679 out.push(quad(
680 Rect::new(x, (cy + ch * 0.55).round(), w, stroke),
681 Color::hex(fg),
682 QuadKind::Solid,
683 [0; 4],
684 ));
685 }
686}
687
688#[allow(clippy::too_many_arguments)]
692fn lookup(
693 table: &mut StyleTable,
694 ch: char,
695 flags: u8,
696 style: &TextStyle,
697 res: &Resources,
698 fs: &mut FontSystem,
699 raster: &mut Raster,
700 atlas: &mut GlyphAtlas,
701 scale: f32,
702) -> Option<CellGlyph> {
703 let v = variant(flags);
704 let known = if (ch as u32) < 128 {
705 table.ascii[v * 128 + ch as usize]
706 } else {
707 table.other.get(&(ch, v as u8)).copied()
708 };
709 if let Some(g) = known {
710 return g;
711 }
712 let g = shape_cell(
713 ch,
714 flags,
715 style,
716 &table.family,
717 res,
718 fs,
719 raster,
720 atlas,
721 scale,
722 (table.cell_w, table.cell_h),
723 );
724 if (ch as u32) < 128 {
725 table.ascii[v * 128 + ch as usize] = Some(g);
726 } else {
727 table.other.insert((ch, v as u8), g);
728 }
729 g
730}
731
732#[allow(clippy::too_many_arguments)]
745fn shape_cell(
746 ch: char,
747 flags: u8,
748 style: &TextStyle,
749 own: &str,
750 res: &Resources,
751 fs: &mut FontSystem,
752 raster: &mut Raster,
753 atlas: &mut GlyphAtlas,
754 scale: f32,
755 cell: (f32, f32),
756) -> Option<CellGlyph> {
757 if boxdraw::draws(ch) {
758 let (w, h) = (cell.0 as u32, cell.1 as u32);
759 let slot = atlas.get_or_insert_synth(ch, w, h, || boxdraw::raster(ch, w, h))?;
760 return Some(CellGlyph {
761 x: 0.0,
762 y: 0.0,
763 w: w as f32,
764 h: h as f32,
765 uv: [slot.x, slot.y, slot.w, slot.h],
766 kind: QuadKind::GlyphMask,
767 });
768 }
769 let mut buf = [0u8; 4];
770 let text: &str = ch.encode_utf8(&mut buf);
771 let mut g = shape_one(style, text, flags, None, 1.0, res, fs, scale)?;
772 let mut dx = 0.0;
773 let theirs = fs
774 .db()
775 .face(g.font)
776 .is_some_and(|f| !f.families.iter().any(|(name, _)| name == own));
777 if theirs && !private_use(ch) {
778 let mut family = None;
779 let chosen = fs.db().face(g.font).is_some_and(|f| {
781 f.families
782 .iter()
783 .any(|(name, _)| res.fallback.contains(name))
784 });
785 if !chosen
786 && !fs.is_monospace(g.font)
787 && let Some(m) = shape_one(style, text, flags, MONO, 1.0, res, fs, scale)
788 && m.glyph != 0
789 && fs.is_monospace(m.font)
790 {
791 family = MONO;
792 g = m;
793 }
794 let span = cell.0 * if flags & flags::WIDE != 0 { 2.0 } else { 1.0 };
795 if g.advance > span + 0.5 {
796 let fit = span / g.advance;
797 if let Some(f) = shape_one(style, text, flags, family, fit, res, fs, scale) {
798 g = Shaped {
799 line_y: g.line_y,
800 ..f
801 };
802 }
803 }
804 dx = ((span - g.advance) / 2.0).round().max(0.0);
805 }
806 let slot = raster_glyph(g.key, fs, raster, atlas)?;
807 Some(CellGlyph {
808 x: dx + g.x as f32 + slot.left as f32,
809 y: g.line_y.round() + g.y as f32 - slot.top as f32,
810 w: slot.w as f32,
811 h: slot.h as f32,
812 uv: [slot.x, slot.y, slot.w, slot.h],
813 kind: glyph_kind(&slot),
814 })
815}
816
817const MONO: Option<cosmic_text::Family<'static>> = Some(cosmic_text::Family::Monospace);
819
820fn private_use(ch: char) -> bool {
822 matches!(ch as u32, 0xE000..=0xF8FF | 0xF_0000..=0x10_FFFF)
823}
824
825struct Shaped {
829 key: cosmic_text::CacheKey,
830 font: cosmic_text::fontdb::ID,
831 glyph: u16,
832 advance: f32,
833 x: i32,
834 y: i32,
835 line_y: f32,
836}
837
838#[allow(clippy::too_many_arguments)]
842fn shape_one(
843 style: &TextStyle,
844 text: &str,
845 flags: u8,
846 family: Option<cosmic_text::Family<'_>>,
847 fit: f32,
848 res: &Resources,
849 fs: &mut FontSystem,
850 scale: f32,
851) -> Option<Shaped> {
852 let metrics = crate::text::shaper_metrics(style.size * scale * fit, style.line_height * scale);
853 let mut buffer = Buffer::new(fs, metrics);
854 buffer.set_size(None, None);
855 let mut attrs = match family {
857 Some(family) => crate::weights::Weights::CSS
858 .apply(Attrs::new().family(family), flags & flags::BOLD != 0),
859 None => res.weights_of(style.family).apply(
860 Attrs::new().family(res.family_of(style.family)),
861 flags & flags::BOLD != 0,
862 ),
863 };
864 if flags & flags::ITALIC != 0 {
865 attrs = attrs.style(FontStyle::Italic);
866 }
867 buffer.set_text(text, &attrs, Shaping::Advanced, None);
868 buffer.shape_until_scroll(fs, false);
869 let run = buffer.layout_runs().next()?;
870 let glyph = run.glyphs.first()?;
871 let physical = glyph.physical((0.0, 0.0), 1.0);
872 Some(Shaped {
873 key: physical.cache_key,
874 font: glyph.font_id,
875 glyph: glyph.glyph_id,
876 advance: glyph.w,
877 x: physical.x,
878 y: physical.y,
879 line_y: run.line_y,
880 })
881}