1use std::{
7 collections::{HashMap, HashSet},
8 num::NonZeroU32,
9};
10
11use felis_grid::{AttrFlags, Grapheme, HAlign, ScreenBuffer, Sizing, VAlign};
12use felis_shaping::{
13 CellMetrics, FACE_ID_SATURATED, Font, FontStack, FontStyle, GlyphBitmap, GlyphId, GlyphPixels,
14 ShapeCache, ShapedGlyph, Shaper, SizingKey,
15};
16use foldhash::fast::RandomState;
17
18use crate::{
19 atlas::{Allocation, ShelfAtlas},
20 box_drawing,
21 display_list::AtlasKind,
22};
23
24pub const GLYPH_ATLAS_SIDE: u32 = 2048;
29
30pub const COLOR_GLYPH_ATLAS_SIDE: u32 = 1024;
34const _: () = assert!(COLOR_GLYPH_ATLAS_SIDE < GLYPH_ATLAS_SIDE);
35
36#[must_use]
39pub fn color_atlas_side(mono_side: NonZeroU32) -> NonZeroU32 {
40 NonZeroU32::new(mono_side.get().min(COLOR_GLYPH_ATLAS_SIDE)).unwrap_or(mono_side)
41}
42
43#[must_use]
48pub fn cluster_base_char(screen: &ScreenBuffer, id: NonZeroU32) -> Option<char> {
49 screen.cluster_str(id).and_then(|s| s.chars().next())
50}
51
52#[derive(Copy, Clone, Debug, PartialEq, Eq)]
53pub struct GlyphSlot {
54 pub atlas_px: [u32; 2],
56 pub size_px: [u32; 2],
58 pub offset_px: [i32; 2],
60 pub is_color: bool,
62}
63
64pub trait AtlasView {
65 fn slot(&self, glyph: char, cell_height_px: u32, sizing: SizingKey) -> Option<GlyphSlot>;
68
69 fn glyph_id_slot(
74 &self,
75 glyph_id: GlyphId,
76 cell_height_px: u32,
77 sizing: SizingKey,
78 ) -> Option<GlyphSlot>;
79
80 fn fitted_glyph_id_slot(
83 &self,
84 glyph_id: GlyphId,
85 px: u16,
86 cell_height_px: u32,
87 sizing: SizingKey,
88 ) -> Option<GlyphSlot>;
89
90 fn overlay_cluster(&self, text: &str) -> Option<&[ClusterGlyph]>;
93
94 fn sheet_side(&self, sheet: AtlasKind) -> u32;
96}
97
98#[must_use]
100pub const fn sizing_key_from(sizing: Sizing) -> SizingKey {
101 let valign = match sizing.valign() {
102 VAlign::Top => 0,
103 VAlign::Bottom => 1,
104 VAlign::Center => 2,
105 };
106 let halign = match sizing.halign() {
107 HAlign::Left => 0,
108 HAlign::Right => 1,
109 HAlign::Center => 2,
110 };
111 SizingKey::new(
112 sizing.scale(),
113 sizing.cell_width(),
114 sizing.frac_num(),
115 sizing.frac_den(),
116 valign,
117 halign,
118 )
119}
120
121#[must_use]
124pub const fn font_style_of(flags: AttrFlags) -> FontStyle {
125 FontStyle {
126 bold: flags.contains(AttrFlags::BOLD),
127 italic: flags.contains(AttrFlags::ITALIC),
128 }
129}
130
131#[must_use]
134pub fn cluster_block_cols(screen: &ScreenBuffer, row: u16, col: u16) -> u16 {
135 match screen.cell_sizing(row, col).map_or(0, |s| s.cell_width()) {
136 0 => {
137 let (first, last) = screen.char_span(row, col);
138 last - first + 1
139 }
140 w => u16::from(w),
141 }
142}
143
144#[derive(Debug)]
145pub struct PendingUpload {
146 pub alloc: Allocation,
147 pub pixels: GlyphPixels,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
157enum SlotKey {
158 Char(char),
159 Glyph(GlyphId),
160 FittedGlyph(GlyphId, u16),
163}
164
165pub struct GlyphIndex {
166 stack: FontStack,
167 font_size_physical_px: f32,
168 metrics: CellMetrics,
169 shape: ShapeCache,
170 atlas: ShelfAtlas,
171 color_atlas: ShelfAtlas,
174 slots: HashMap<(SlotKey, u32, SizingKey), Option<GlyphSlot>, RandomState>,
180 blank_slots: usize,
183 placed_slots: usize,
186 shaped_runs: [HashMap<String, Vec<ShapedGlyph>>; 4],
190 overlay_clusters: HashMap<String, Vec<ClusterGlyph>>,
194 cluster_fits: HashMap<(String, u16, SizingKey), u16>,
198 pending: Vec<PendingUpload>,
199 primed: HashSet<(char, SizingKey), RandomState>,
200 atlas_reset_during_walk: bool,
204 atlas_resets: u64,
205 walked: Option<WalkKey>,
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213struct WalkKey {
214 rows: u16,
215 cols: u16,
216 shaped: bool,
217}
218
219#[derive(Debug, Clone, Copy)]
221struct FitBlock {
222 w: f32,
223 h: f32,
224 slack: f32,
225}
226
227impl FitBlock {
228 fn holds(self, advance: f32) -> bool {
231 advance <= self.w + self.slack
232 }
233
234 fn ratio(self, ink_w: f32, ink_h: f32) -> f32 {
235 (self.w / ink_w).min(self.h / ink_h).min(1.0)
236 }
237}
238
239impl GlyphIndex {
240 #[must_use]
241 pub fn new(stack: FontStack, font_size_physical_px: f32, atlas_side: NonZeroU32) -> Self {
242 let metrics = stack.primary().cell_metrics(font_size_physical_px);
243 let color_side = color_atlas_side(atlas_side);
244 Self {
245 stack,
246 font_size_physical_px,
247 metrics,
248 shape: ShapeCache::default(),
249 atlas: ShelfAtlas::new(atlas_side, atlas_side),
250 color_atlas: ShelfAtlas::new(color_side, color_side),
251 slots: HashMap::default(),
252 blank_slots: 0,
253 placed_slots: 0,
254 shaped_runs: std::array::from_fn(|_| HashMap::new()),
255 overlay_clusters: HashMap::new(),
256 cluster_fits: HashMap::new(),
257 pending: Vec::new(),
258 primed: HashSet::default(),
259 atlas_reset_during_walk: false,
260 atlas_resets: 0,
261 walked: None,
262 }
263 }
264
265 pub const SHAPED_RUN_CACHE_MAX: usize = 4096;
269
270 pub const BLANK_SLOT_MAX: usize = 4096;
273
274 pub const PLACED_SLOT_MAX: usize = 65_536;
278
279 pub fn shape_run_cached(
285 &mut self,
286 shaper: &mut Shaper,
287 features: &[String],
288 style: FontStyle,
289 text: &str,
290 out: &mut Vec<ShapedGlyph>,
291 ) -> bool {
292 out.clear();
293 let map = &self.shaped_runs[style.index()];
294 if let Some(cached) = map.get(text) {
295 out.extend_from_slice(cached);
296 return false;
297 }
298 let primary = self.stack.styled_primary(style).clone();
299 let shaped = shaper.shape_run(&primary, self.font_size_physical_px, features, text);
300 out.extend_from_slice(&shaped);
301 let map = &mut self.shaped_runs[style.index()];
304 map.insert(text.to_owned(), shaped);
305 while map.len() > Self::SHAPED_RUN_CACHE_MAX {
306 let Some(evict_key) = map.keys().find(|k| k.as_str() != text).cloned() else {
307 break;
308 };
309 map.remove(&evict_key);
310 }
311 true
312 }
313
314 #[must_use]
315 pub fn shaped_run_cache_len(&self) -> usize {
316 self.shaped_runs.iter().map(HashMap::len).sum()
317 }
318
319 #[must_use]
320 pub const fn cell_metrics(&self) -> CellMetrics {
321 self.metrics
322 }
323
324 #[must_use]
325 pub const fn font_size_physical_px(&self) -> f32 {
326 self.font_size_physical_px
327 }
328
329 #[must_use]
330 pub const fn stack(&self) -> &FontStack {
331 &self.stack
332 }
333
334 #[must_use]
335 pub const fn atlas_side(&self) -> u32 {
336 self.atlas.width()
337 }
338
339 #[must_use]
340 pub fn contains(&self, glyph: char, cell_height: u32, sizing: SizingKey) -> bool {
341 self.contains_key(SlotKey::Char(glyph), cell_height, sizing)
342 }
343
344 fn contains_key(&self, key: SlotKey, cell_height: u32, sizing: SizingKey) -> bool {
345 self.slots.contains_key(&(key, cell_height, sizing))
346 }
347
348 fn lookup(&self, key: SlotKey, cell_height: u32, sizing: SizingKey) -> Option<GlyphSlot> {
349 self.slots
350 .get(&(key, cell_height, sizing))
351 .copied()
352 .flatten()
353 }
354
355 pub fn ensure(&mut self, glyph: char, cell_height: u32, sizing: SizingKey) -> bool {
360 let key = (SlotKey::Char(glyph), cell_height, sizing);
361 if self.slots.contains_key(&key) {
362 return false;
363 }
364 let scale = sizing.effective_scale();
365 let scaled = CellMetrics {
366 width: ((self.metrics.width as f32) * scale).round().max(1.0) as u32,
367 height: ((cell_height as f32) * scale).round().max(1.0) as u32,
368 ascent: ((self.metrics.ascent as f32) * scale).round() as u32,
369 };
370 if let Some(bitmap) = box_drawing::rasterize(glyph, scaled) {
371 let slot = self.allocate(bitmap);
372 self.insert_slot(key, slot);
373 return true;
374 }
375 let font = self.stack.resolve(glyph, sizing.style()).clone();
376 let bitmap = if let Some(bitmap) = self.fitted_bitmap(&font, glyph, cell_height, sizing) {
377 bitmap
378 } else {
379 let px = (self.font_size_physical_px * scale).round() as u32;
380 self.shape
381 .get_or_insert_sized(&font, glyph, px, sizing)
382 .clone()
383 };
384 let slot = self.allocate(bitmap);
385 self.insert_slot(key, slot);
386 true
387 }
388
389 fn fitted_bitmap(
393 &mut self,
394 font: &Font,
395 glyph: char,
396 cell_height: u32,
397 sizing: SizingKey,
398 ) -> Option<GlyphBitmap> {
399 let glyph_scale = sizing.effective_scale();
400 let base_px = self.font_size_physical_px * glyph_scale;
401 let cells = match sizing.cell_width() {
402 0 => felis_grid::char_cell_width(glyph).max(1),
403 w => w,
404 };
405 let block = self.fit_block(cells.into(), cell_height, sizing);
406 if block.holds(font.advance_px(glyph, base_px)?) {
407 return None;
408 }
409 let ink = self
410 .shape
411 .get_or_insert_sized(font, glyph, base_px.round() as u32, sizing);
412 if ink.is_blank() {
413 return None;
414 }
415 let ratio = block.ratio(ink.width() as f32, ink.height() as f32);
416 let px = (base_px * ratio).floor().max(1.0) as u32;
417 let mut bitmap = self
418 .shape
419 .get_or_insert_sized(font, glyph, px, sizing)
420 .clone();
421 bitmap.left = ((block.w - bitmap.width() as f32) / 2.0).round() as i32;
422 let ascent = self.metrics.ascent as f32 * glyph_scale;
423 bitmap.top = (ascent - (block.h - bitmap.height() as f32) / 2.0).round() as i32;
424 Some(bitmap)
425 }
426
427 fn fit_block(&self, cells: u16, cell_height: u32, sizing: SizingKey) -> FitBlock {
430 let layout_scale = f32::from(sizing.scale().max(1));
431 FitBlock {
432 w: (self.metrics.width * u32::from(cells)) as f32 * layout_scale,
433 h: cell_height as f32 * layout_scale,
434 slack: layout_scale,
435 }
436 }
437
438 pub fn ensure_glyph_id(
443 &mut self,
444 glyph_id: GlyphId,
445 cell_height: u32,
446 sizing: SizingKey,
447 ) -> bool {
448 let px = self.font_size_physical_px * sizing.effective_scale();
449 self.ensure_glyph_slot(SlotKey::Glyph(glyph_id), glyph_id, px, cell_height, sizing)
450 }
451
452 pub fn ensure_fitted_glyph_id(
455 &mut self,
456 glyph_id: GlyphId,
457 px: u16,
458 cell_height: u32,
459 sizing: SizingKey,
460 ) -> bool {
461 let key = SlotKey::FittedGlyph(glyph_id, px);
462 self.ensure_glyph_slot(key, glyph_id, f32::from(px), cell_height, sizing)
463 }
464
465 fn ensure_glyph_slot(
466 &mut self,
467 slot_key: SlotKey,
468 glyph_id: GlyphId,
469 px: f32,
470 cell_height: u32,
471 sizing: SizingKey,
472 ) -> bool {
473 let key = (slot_key, cell_height, sizing);
474 if self.slots.contains_key(&key) {
475 return false;
476 }
477 let font = self.stack.font_at(sizing.font_id(), sizing.style()).clone();
478 let bitmap = self.shape.rasterize_glyph_id(&font, glyph_id, px);
479 let slot = self.allocate(bitmap);
480 self.insert_slot(key, slot);
481 true
482 }
483
484 const CLUSTER_FIT_MAX: usize = 1024;
487
488 fn cluster_fit_px(
493 &mut self,
494 text: &str,
495 glyphs: &[ShapedGlyph],
496 cols: u16,
497 sizing: SizingKey,
498 ) -> Option<u16> {
499 let glyph_scale = sizing.effective_scale();
500 let block = self.fit_block(cols, self.metrics.height, sizing);
501 let advance: f32 = glyphs.iter().map(|g| g.advance_px).sum::<f32>() * glyph_scale;
502 if block.holds(advance) {
503 return None;
504 }
505 let key = (text.to_owned(), cols, sizing);
506 if let Some(&px) = self.cluster_fits.get(&key) {
507 return (px > 0).then_some(px);
508 }
509 let font = self.stack.font_at(sizing.font_id(), sizing.style()).clone();
510 let effective_px = self.font_size_physical_px * glyph_scale;
511 let mut pen = 0.0f32;
512 let mut ink: Option<[f32; 4]> = None;
513 for g in glyphs {
514 let bitmap = self
515 .shape
516 .rasterize_glyph_id(&font, g.glyph_id, effective_px);
517 if !bitmap.is_blank() {
518 let x0 = g.x_offset_px.mul_add(glyph_scale, pen) + bitmap.left as f32;
519 let y0 = g.y_offset_px.mul_add(-glyph_scale, -bitmap.top as f32);
520 let [x1, y1] = [x0 + bitmap.width() as f32, y0 + bitmap.height() as f32];
521 ink = Some(ink.map_or([x0, y0, x1, y1], |[a, b, c, d]| {
522 [a.min(x0), b.min(y0), c.max(x1), d.max(y1)]
523 }));
524 }
525 pen = g.advance_px.mul_add(glyph_scale, pen);
526 }
527 let px = ink.map_or(0, |[x0, y0, x1, y1]| {
529 (effective_px * block.ratio(x1 - x0, y1 - y0))
530 .floor()
531 .clamp(1.0, f32::from(u16::MAX)) as u16
532 });
533 if self.cluster_fits.len() >= Self::CLUSTER_FIT_MAX {
534 self.cluster_fits.clear();
535 }
536 self.cluster_fits.insert(key, px);
537 (px > 0).then_some(px)
538 }
539
540 const OVERLAY_CLUSTER_MAX: usize = 256;
543
544 pub fn trim_overlay_clusters(&mut self) {
547 if self.overlay_clusters.len() > Self::OVERLAY_CLUSTER_MAX {
548 self.overlay_clusters.clear();
549 }
550 }
551
552 pub fn ensure_overlay_cluster(&mut self, shaper: &mut Shaper, text: &str) {
555 if !self.overlay_clusters.contains_key(text) {
556 let shaping = shaper.shape_cluster(
557 &self.stack,
558 self.font_size_physical_px,
559 FontStyle::REGULAR,
560 text,
561 );
562 let font_id = saturated_face_id(shaping.font_id);
563 let glyphs = shaping
564 .glyphs
565 .iter()
566 .map(|g| ClusterGlyph {
567 glyph_id: g.glyph_id,
568 font_id,
569 advance_px: g.advance_px,
570 x_offset_px: g.x_offset_px,
571 y_offset_px: g.y_offset_px,
572 })
573 .collect();
574 self.overlay_clusters.insert(text.to_owned(), glyphs);
575 }
576 let cell_height = self.metrics.height;
577 let primes: Vec<(GlyphId, u16)> = self.overlay_clusters[text]
578 .iter()
579 .map(|g| (g.glyph_id, g.font_id))
580 .collect();
581 for (glyph_id, font_id) in primes {
582 let key = SizingKey::default().with_font_id(usize::from(font_id));
583 self.ensure_glyph_id(glyph_id, cell_height, key);
584 }
585 }
586
587 fn insert_slot(&mut self, key: (SlotKey, u32, SizingKey), slot: Option<GlyphSlot>) {
590 if slot.is_none() {
591 if self.blank_slots >= Self::BLANK_SLOT_MAX {
592 self.slots.retain(|_, s| s.is_some());
593 self.blank_slots = 0;
594 }
595 self.blank_slots += 1;
596 } else {
597 self.placed_slots += 1;
598 }
599 self.slots.insert(key, slot);
600 }
601
602 #[must_use]
603 pub fn contains_glyph_id(
604 &self,
605 glyph_id: GlyphId,
606 cell_height: u32,
607 sizing: SizingKey,
608 ) -> bool {
609 self.contains_key(SlotKey::Glyph(glyph_id), cell_height, sizing)
610 }
611
612 pub fn drain_pending(&mut self) -> std::vec::Drain<'_, PendingUpload> {
613 self.pending.drain(..)
614 }
615
616 fn begin_populate(&mut self) {
617 self.primed.clear();
618 self.atlas_reset_during_walk = false;
619 }
620
621 #[must_use]
624 pub const fn atlas_was_reset(&self) -> bool {
625 self.atlas_reset_during_walk
626 }
627
628 #[must_use]
630 pub const fn atlas_resets(&self) -> u64 {
631 self.atlas_resets
632 }
633
634 fn prime_char(&mut self, glyph: char, cell_height: u32, sizing: SizingKey) {
637 if self.primed.insert((glyph, sizing)) {
638 self.ensure(glyph, cell_height, sizing);
639 }
640 }
641
642 pub fn populate(&mut self, screen: &ScreenBuffer) {
643 self.begin_populate();
644 for r in 0..screen.rows() {
645 self.populate_row(screen, r);
646 }
647 }
648
649 fn populate_row(&mut self, screen: &ScreenBuffer, r: u16) {
650 let cell_height = self.metrics.height;
651 for c in 0..screen.cols() {
652 let Some(cell) = screen.cell(r, c) else {
653 continue;
654 };
655 let glyph = match &cell.grapheme {
659 Grapheme::Empty
660 | Grapheme::Spacer
661 | Grapheme::SizedSpacer
662 | Grapheme::Cluster(_) => continue,
663 Grapheme::Ascii(b) => *b as char,
664 Grapheme::Char(c) => *c,
665 };
666 let sizing = screen
667 .cell_sizing(r, c)
668 .map(|s| sizing_key_from(*s))
669 .unwrap_or_default()
670 .with_style(font_style_of(screen.style(cell.style).flags));
671 self.prime_char(glyph, cell_height, sizing);
672 }
673 }
674
675 fn allocate(&mut self, bitmap: GlyphBitmap) -> Option<GlyphSlot> {
679 match self.try_allocate(bitmap) {
680 AllocOutcome::Placed(slot) => Some(slot),
681 AllocOutcome::Blank => None,
682 AllocOutcome::Full(bitmap) => {
683 self.reset_atlas_full();
684 match self.try_allocate(bitmap) {
685 AllocOutcome::Placed(slot) => Some(slot),
686 AllocOutcome::Blank | AllocOutcome::Full(_) => None,
687 }
688 }
689 }
690 }
691
692 fn reset_atlas_full(&mut self) {
698 self.atlas.reset();
699 self.color_atlas.reset();
700 self.slots.clear();
701 self.blank_slots = 0;
702 self.placed_slots = 0;
703 self.pending.clear();
704 self.primed.clear();
705 self.atlas_reset_during_walk = true;
706 self.atlas_resets += 1;
707 self.walked = None;
708 }
709
710 fn try_allocate(&mut self, bitmap: GlyphBitmap) -> AllocOutcome {
711 if bitmap.is_blank() || bitmap.width() == 0 || bitmap.height() == 0 {
712 return AllocOutcome::Blank;
713 }
714 if self.placed_slots >= Self::PLACED_SLOT_MAX {
715 return AllocOutcome::Full(bitmap);
716 }
717 let (width, height) = (bitmap.width(), bitmap.height());
718 let offset_px = [bitmap.left, bitmap.top];
719 let is_color = bitmap.is_color();
720 let sheet = if is_color {
721 &mut self.color_atlas
722 } else {
723 &mut self.atlas
724 };
725 let Some(alloc) = sheet.alloc(width, height) else {
726 return AllocOutcome::Full(bitmap);
727 };
728 let atlas_px = [alloc.x, alloc.y];
729 self.pending.push(PendingUpload {
730 alloc,
731 pixels: bitmap.into_pixels(),
732 });
733 AllocOutcome::Placed(GlyphSlot {
734 atlas_px,
735 size_px: [width, height],
736 offset_px,
737 is_color,
738 })
739 }
740}
741
742enum AllocOutcome {
743 Placed(GlyphSlot),
744 Blank,
745 Full(GlyphBitmap),
747}
748
749struct ShapeRun {
752 start_col: u16,
753 cell_count: u16,
754 style: FontStyle,
755}
756
757fn next_shape_run<F>(
763 screen: &ScreenBuffer,
764 row: u16,
765 start_col: u16,
766 text: &mut String,
767 covers: F,
768) -> Option<ShapeRun>
769where
770 F: Fn(char, FontStyle) -> bool,
771{
772 text.clear();
773 let cols = screen.cols();
774 if start_col >= cols {
775 return None;
776 }
777 let first = screen.cell(row, start_col)?;
778 let (Grapheme::Ascii(b0), None) = (&first.grapheme, screen.cell_sizing(row, start_col)) else {
779 return None;
780 };
781 let style = font_style_of(screen.style(first.style).flags);
782 let c0 = *b0 as char;
783 if !covers(c0, style) {
784 return None;
785 }
786 text.push(c0);
787 let mut last_col = start_col;
788 for c in (start_col + 1)..cols {
789 let Some(cell) = screen.cell(row, c) else {
790 break;
791 };
792 let Grapheme::Ascii(b) = &cell.grapheme else {
793 break;
794 };
795 if screen.cell_sizing(row, c).is_some() {
796 break;
797 }
798 if font_style_of(screen.style(cell.style).flags) != style {
799 break;
800 }
801 let ch = *b as char;
802 if !covers(ch, style) {
803 break;
804 }
805 text.push(ch);
806 last_col = c;
807 }
808 let cell_count = last_col - start_col + 1;
809 Some(ShapeRun {
810 start_col,
811 cell_count,
812 style,
813 })
814}
815
816struct RunPlacement {
817 row: u16,
818 start_col: u16,
819 cell_count: u16,
820 style: FontStyle,
821}
822
823struct ClusterSite {
824 row: u16,
825 col: u16,
826 id: NonZeroU32,
827 style: FontStyle,
828 sizing: SizingKey,
829}
830
831fn apply_shaped_run(
832 frame_cells: &mut [ShapedCell],
833 index: &mut GlyphIndex,
834 cols: u16,
835 run: &RunPlacement,
836 shaped: &[ShapedGlyph],
837) {
838 let cell_height = index.cell_metrics().height;
839 let cell_width = index.cell_metrics().width as f32;
840 let sizing = SizingKey::default().with_style(run.style);
843 let row_base = usize::from(run.row) * usize::from(cols);
844 let run_start_col = run.start_col;
847 let run_end_col_exclusive = run_start_col + run.cell_count;
848 let mut i = 0;
852 while i < shaped.len() {
853 let src_start = shaped[i].source_byte_start;
854 let src_len = shaped[i].source_byte_len;
855 let mut j = i + 1;
856 while j < shaped.len()
857 && shaped[j].source_byte_start == src_start
858 && shaped[j].source_byte_len == src_len
859 {
860 j += 1;
861 }
862 let cluster = &shaped[i..j];
863 i = j;
864 let cluster_col = run_start_col + (src_start as u16);
865 if cluster_col >= run_end_col_exclusive {
866 continue;
867 }
868 let span_cols = u16::try_from(src_len).unwrap_or(1).max(1);
869 let last_col = (cluster_col + span_cols).min(run_end_col_exclusive);
872 let total_advance: f32 = cluster.iter().map(|g| g.advance_px).sum();
873 if total_advance > (cell_width + 1.0) * f32::from(span_cols) {
878 continue;
879 }
880 if let [g] = cluster {
881 index.ensure_glyph_id(g.glyph_id, cell_height, sizing);
882 if let Some(slot) = frame_cells.get_mut(row_base + usize::from(cluster_col)) {
883 *slot = ShapedCell::Primary {
884 glyph_id: g.glyph_id,
885 span_cols,
886 };
887 }
888 for col in (cluster_col + 1)..last_col {
889 if let Some(slot) = frame_cells.get_mut(row_base + usize::from(col)) {
890 *slot = ShapedCell::Trailing;
891 }
892 }
893 continue;
894 }
895 let per_cell_advance = total_advance / f32::from(span_cols);
898 for col in cluster_col..last_col {
901 if let Some(slot) = frame_cells.get_mut(row_base + usize::from(col)) {
902 *slot = ShapedCell::Trailing;
903 }
904 }
905 let mut pen = 0.0f32;
906 for g in cluster {
907 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
908 let cell_offset = if per_cell_advance > 0.0 {
909 ((pen / per_cell_advance).round().max(0.0) as u16).min(span_cols - 1)
910 } else {
911 0
912 };
913 pen += g.advance_px;
914 let col = cluster_col + cell_offset;
915 if col >= last_col {
916 continue;
917 }
918 index.ensure_glyph_id(g.glyph_id, cell_height, sizing);
919 if let Some(slot) = frame_cells.get_mut(row_base + usize::from(col)) {
920 *slot = ShapedCell::Primary {
921 glyph_id: g.glyph_id,
922 span_cols: 1,
923 };
924 }
925 }
926 }
927}
928
929impl AtlasView for GlyphIndex {
930 fn slot(&self, glyph: char, cell_height_px: u32, sizing: SizingKey) -> Option<GlyphSlot> {
931 self.lookup(SlotKey::Char(glyph), cell_height_px, sizing)
932 }
933
934 fn glyph_id_slot(
935 &self,
936 glyph_id: GlyphId,
937 cell_height_px: u32,
938 sizing: SizingKey,
939 ) -> Option<GlyphSlot> {
940 self.lookup(SlotKey::Glyph(glyph_id), cell_height_px, sizing)
941 }
942
943 fn fitted_glyph_id_slot(
944 &self,
945 glyph_id: GlyphId,
946 px: u16,
947 cell_height_px: u32,
948 sizing: SizingKey,
949 ) -> Option<GlyphSlot> {
950 self.lookup(SlotKey::FittedGlyph(glyph_id, px), cell_height_px, sizing)
951 }
952
953 fn overlay_cluster(&self, text: &str) -> Option<&[ClusterGlyph]> {
954 self.overlay_clusters.get(text).map(Vec::as_slice)
955 }
956
957 fn sheet_side(&self, sheet: AtlasKind) -> u32 {
958 match sheet {
959 AtlasKind::Mono => self.atlas.width(),
960 AtlasKind::Color => self.color_atlas.width(),
961 }
962 }
963}
964
965#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
966pub enum ShapedCell {
967 #[default]
969 None,
970 Primary { glyph_id: GlyphId, span_cols: u16 },
973 Trailing,
976 Cluster { start: u32, len: u16, fit_px: u16 },
982}
983
984fn saturated_face_id(font_id: usize) -> u16 {
985 u16::try_from(font_id.min(FACE_ID_SATURATED)).unwrap_or(u16::MAX)
986}
987
988#[derive(Debug, Clone, Copy, PartialEq)]
991pub struct ClusterGlyph {
992 pub glyph_id: GlyphId,
993 pub font_id: u16,
997 pub advance_px: f32,
999 pub x_offset_px: f32,
1000 pub y_offset_px: f32,
1002}
1003
1004#[derive(Debug, Clone, Default)]
1008pub struct ShapeFrame {
1009 cells: Vec<ShapedCell>,
1010 cols: u16,
1011 cluster_glyphs: Vec<ClusterGlyph>,
1012 live_cluster_glyphs: usize,
1015}
1016
1017impl ShapeFrame {
1018 #[must_use]
1019 pub const fn empty() -> Self {
1020 Self {
1021 cells: Vec::new(),
1022 cols: 0,
1023 cluster_glyphs: Vec::new(),
1024 live_cluster_glyphs: 0,
1025 }
1026 }
1027
1028 fn begin(&mut self, rows: u16, cols: u16) {
1029 let cell_count = usize::from(rows) * usize::from(cols);
1030 self.cells.clear();
1031 self.cells.resize(cell_count, ShapedCell::None);
1032 self.cols = cols;
1033 self.cluster_glyphs.clear();
1034 self.live_cluster_glyphs = 0;
1035 }
1036
1037 fn reset_empty(&mut self) {
1038 self.cells.clear();
1039 self.cols = 0;
1040 self.cluster_glyphs.clear();
1041 self.live_cluster_glyphs = 0;
1042 }
1043
1044 fn clear_row(&mut self, row: u16) {
1045 let cols = usize::from(self.cols);
1046 let start = usize::from(row) * cols;
1047 let Some(cells) = self.cells.get_mut(start..start + cols) else {
1048 return;
1049 };
1050 for cell in cells {
1051 if let ShapedCell::Cluster { len, .. } = *cell {
1052 self.live_cluster_glyphs -= usize::from(len);
1053 }
1054 *cell = ShapedCell::None;
1055 }
1056 }
1057
1058 fn compact(&mut self) {
1062 if self.cluster_glyphs.len() <= (self.live_cluster_glyphs * 2).max(4096) {
1063 return;
1064 }
1065 let mut pool = Vec::with_capacity(self.live_cluster_glyphs);
1066 for cell in &mut self.cells {
1067 if let ShapedCell::Cluster { start, len, .. } = cell {
1068 let from = *start as usize;
1069 *start = u32::try_from(pool.len()).unwrap_or(u32::MAX);
1070 pool.extend_from_slice(&self.cluster_glyphs[from..from + usize::from(*len)]);
1071 }
1072 }
1073 self.cluster_glyphs = pool;
1074 }
1075
1076 #[cfg(any(test, feature = "testing"))]
1079 #[doc(hidden)]
1080 #[must_use]
1081 pub const fn from_cells(cells: Vec<ShapedCell>, cols: u16) -> Self {
1082 Self {
1083 cells,
1084 cols,
1085 cluster_glyphs: Vec::new(),
1086 live_cluster_glyphs: 0,
1087 }
1088 }
1089
1090 #[cfg(any(test, feature = "testing"))]
1091 #[doc(hidden)]
1092 #[must_use]
1093 pub const fn from_cluster_cells(
1094 cells: Vec<ShapedCell>,
1095 cols: u16,
1096 cluster_glyphs: Vec<ClusterGlyph>,
1097 ) -> Self {
1098 let live_cluster_glyphs = cluster_glyphs.len();
1099 Self {
1100 cells,
1101 cols,
1102 cluster_glyphs,
1103 live_cluster_glyphs,
1104 }
1105 }
1106
1107 #[must_use]
1108 pub fn cluster_slice(&self, start: u32, len: u16) -> &[ClusterGlyph] {
1109 let s = start as usize;
1110 let e = s + len as usize;
1111 self.cluster_glyphs.get(s..e).unwrap_or(&[])
1112 }
1113
1114 #[must_use]
1115 pub fn cell_at(&self, row: u16, col: u16) -> ShapedCell {
1116 if self.cells.is_empty() || self.cols == 0 || col >= self.cols {
1117 return ShapedCell::None;
1118 }
1119 let idx = usize::from(row) * usize::from(self.cols) + usize::from(col);
1120 self.cells.get(idx).copied().unwrap_or(ShapedCell::None)
1121 }
1122
1123 #[must_use]
1124 pub fn cells(&self) -> &[ShapedCell] {
1125 &self.cells
1126 }
1127
1128 #[must_use]
1129 pub const fn cols(&self) -> u16 {
1130 self.cols
1131 }
1132}
1133
1134#[derive(Default)]
1137pub struct GridWalker {
1138 run_text: String,
1139 run_glyphs: Vec<ShapedGlyph>,
1140 rows: Vec<u16>,
1141}
1142
1143impl GridWalker {
1144 pub fn walk(
1150 &mut self,
1151 index: &mut GlyphIndex,
1152 screen: &ScreenBuffer,
1153 shaper: &mut Shaper,
1154 features: &[String],
1155 frame: &mut ShapeFrame,
1156 ) {
1157 let (rows, cols) = (screen.rows(), screen.cols());
1158 let shaped = !features.is_empty() || screen.cluster_count() > 0;
1161 let key = WalkKey { rows, cols, shaped };
1162 let full = index.walked != Some(key);
1163 self.rows.clear();
1164 if full {
1165 self.rows.extend(0..rows);
1166 } else {
1167 let dirty = screen.damage().dirty_rows();
1168 self.rows
1169 .extend(dirty.filter_map(|r| u16::try_from(r).ok()));
1170 }
1171 index.walked = None;
1172 index.begin_populate();
1173 let walk = std::mem::take(&mut self.rows);
1174 if shaped {
1175 if full {
1176 frame.begin(rows, cols);
1177 }
1178 let stack = index.stack().clone();
1179 for &r in &walk {
1180 frame.clear_row(r);
1181 self.walk_shaped_row(index, screen, shaper, features, &stack, r, frame);
1182 }
1183 frame.compact();
1184 } else {
1185 frame.reset_empty();
1186 for &r in &walk {
1187 index.populate_row(screen, r);
1188 }
1189 }
1190 self.rows = walk;
1191 if !index.atlas_was_reset() {
1192 index.walked = Some(key);
1193 }
1194 }
1195
1196 #[expect(
1197 clippy::too_many_arguments,
1198 reason = "the walk's borrowed inputs, split so the index and frame stay separately mutable"
1199 )]
1200 fn walk_shaped_row(
1201 &mut self,
1202 index: &mut GlyphIndex,
1203 screen: &ScreenBuffer,
1204 shaper: &mut Shaper,
1205 features: &[String],
1206 stack: &FontStack,
1207 r: u16,
1208 frame: &mut ShapeFrame,
1209 ) {
1210 let cols = screen.cols();
1211 let mut col = 0u16;
1212 while col < cols {
1213 if !features.is_empty()
1214 && let Some(run) = next_shape_run(screen, r, col, &mut self.run_text, |c, style| {
1215 stack.primary_covers(c, style)
1216 })
1217 {
1218 col = run.start_col + run.cell_count;
1219 self.shape_and_apply_run(
1220 index,
1221 shaper,
1222 features,
1223 cols,
1224 &RunPlacement {
1225 row: r,
1226 start_col: run.start_col,
1227 cell_count: run.cell_count,
1228 style: run.style,
1229 },
1230 frame,
1231 );
1232 continue;
1233 }
1234 shape_cell(index, screen, shaper, stack, r, col, frame);
1235 col += 1;
1236 }
1237 }
1238
1239 fn shape_and_apply_run(
1243 &mut self,
1244 index: &mut GlyphIndex,
1245 shaper: &mut Shaper,
1246 features: &[String],
1247 cols: u16,
1248 run: &RunPlacement,
1249 frame: &mut ShapeFrame,
1250 ) {
1251 if self.run_text.is_empty() {
1252 return;
1253 }
1254 let cell_height = index.cell_metrics().height;
1255 let sizing = SizingKey::default().with_style(run.style);
1256 for ch in self.run_text.chars() {
1257 index.prime_char(ch, cell_height, sizing);
1258 }
1259 index.shape_run_cached(
1260 shaper,
1261 features,
1262 run.style,
1263 &self.run_text,
1264 &mut self.run_glyphs,
1265 );
1266 apply_shaped_run(&mut frame.cells, index, cols, run, &self.run_glyphs);
1267 }
1268}
1269
1270fn shape_cell(
1271 index: &mut GlyphIndex,
1272 screen: &ScreenBuffer,
1273 shaper: &mut Shaper,
1274 stack: &FontStack,
1275 row: u16,
1276 col: u16,
1277 frame: &mut ShapeFrame,
1278) {
1279 let Some(cell) = screen.cell(row, col) else {
1280 return;
1281 };
1282 let glyph = match cell.grapheme {
1283 Grapheme::Empty | Grapheme::Spacer | Grapheme::SizedSpacer => return,
1284 Grapheme::Cluster(_) => None,
1285 Grapheme::Ascii(b) => Some(b as char),
1286 Grapheme::Char(c) => Some(c),
1287 };
1288 let style = font_style_of(screen.style(cell.style).flags);
1289 let sizing = screen
1290 .cell_sizing(row, col)
1291 .map(|s| sizing_key_from(*s))
1292 .unwrap_or_default()
1293 .with_style(style);
1294 let cell_height = index.cell_metrics().height;
1295 if let Some(glyph) = glyph {
1296 index.prime_char(glyph, cell_height, sizing);
1297 return;
1298 }
1299 let Grapheme::Cluster(id) = cell.grapheme else {
1300 return;
1301 };
1302 composite_cluster(
1303 index,
1304 screen,
1305 shaper,
1306 stack,
1307 &ClusterSite {
1308 row,
1309 col,
1310 id,
1311 style,
1312 sizing,
1313 },
1314 frame,
1315 );
1316}
1317
1318fn composite_cluster(
1323 index: &mut GlyphIndex,
1324 screen: &ScreenBuffer,
1325 shaper: &mut Shaper,
1326 stack: &FontStack,
1327 site: &ClusterSite,
1328 frame: &mut ShapeFrame,
1329) {
1330 let Some(text) = screen.cluster_str(site.id) else {
1331 return;
1332 };
1333 let base = text.chars().next();
1334 if base == Some(felis_protocol::kitty_graphics::placeholder::PLACEHOLDER) {
1337 return;
1338 }
1339 let cell_height = index.cell_metrics().height;
1340 let px = index.font_size_physical_px();
1341 let shaping = shaper.shape_cluster(stack, px, site.style, text);
1342 if shaping.glyphs.is_empty() {
1343 if let Some(bc) = base {
1344 index.ensure(bc, cell_height, site.sizing);
1345 }
1346 return;
1347 }
1348 let sized_key = site.sizing.with_font_id(shaping.font_id);
1349 let font_id = saturated_face_id(shaping.font_id);
1350 let cols = cluster_block_cols(screen, site.row, site.col);
1351 let fit_px = index.cluster_fit_px(text, &shaping.glyphs, cols, sized_key);
1352 let pen_ratio = fit_px.map_or(1.0, |px| {
1355 f32::from(px) / (index.font_size_physical_px() * sized_key.effective_scale())
1356 });
1357 let start = u32::try_from(frame.cluster_glyphs.len()).unwrap_or(u32::MAX);
1358 let mut len = 0u16;
1359 for g in &shaping.glyphs {
1360 match fit_px {
1361 Some(px) => index.ensure_fitted_glyph_id(g.glyph_id, px, cell_height, sized_key),
1362 None => index.ensure_glyph_id(g.glyph_id, cell_height, sized_key),
1363 };
1364 frame.cluster_glyphs.push(ClusterGlyph {
1365 glyph_id: g.glyph_id,
1366 font_id,
1367 advance_px: g.advance_px * pen_ratio,
1368 x_offset_px: g.x_offset_px * pen_ratio,
1369 y_offset_px: g.y_offset_px * pen_ratio,
1370 });
1371 len = len.saturating_add(1);
1372 }
1373 let idx = usize::from(site.row) * usize::from(frame.cols) + usize::from(site.col);
1374 if let Some(slot) = frame.cells.get_mut(idx) {
1375 *slot = ShapedCell::Cluster {
1376 start,
1377 len,
1378 fit_px: fit_px.unwrap_or(0),
1379 };
1380 frame.live_cluster_glyphs += usize::from(len);
1381 }
1382}
1383
1384#[cfg(test)]
1385mod tests {
1386 #![allow(clippy::float_cmp)]
1387 #![cfg_attr(
1388 windows,
1389 allow(
1390 dead_code,
1391 reason = "the pinned-font tests are compiled out on Windows"
1392 )
1393 )]
1394 use felis_grid::Grid;
1395
1396 use std::sync::Arc;
1397
1398 use felis_shaping::Font;
1399 use felis_vt::Parser;
1400
1401 use super::*;
1402
1403 fn nz(v: u32) -> NonZeroU32 {
1404 NonZeroU32::new(v).unwrap()
1405 }
1406
1407 fn stack() -> FontStack {
1408 FontStack::new(Arc::new(Font::load_test_font()))
1409 }
1410
1411 #[cfg(not(windows))]
1412 #[test]
1413 fn an_overlay_cluster_is_shaped_and_its_glyphs_primed() {
1414 let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1415 let mut shaper = Shaper::new();
1416 assert!(idx.overlay_cluster("e\u{301}").is_none());
1417 idx.ensure_overlay_cluster(&mut shaper, "e\u{301}");
1418 let glyphs = idx.overlay_cluster("e\u{301}").expect("shaped").to_vec();
1419 assert_ne!(glyphs.len(), 0);
1420 let h = idx.cell_metrics().height;
1421 let base = &glyphs[0];
1422 let key = SizingKey::default().with_font_id(usize::from(base.font_id));
1423 assert!(
1424 idx.glyph_id_slot(base.glyph_id, h, key).is_some(),
1425 "base primed"
1426 );
1427 }
1428
1429 #[cfg(not(windows))]
1430 #[test]
1431 fn overlay_shapes_survive_the_frame_that_primed_them() {
1432 let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1433 let mut shaper = Shaper::new();
1434 let clusters: Vec<String> = ('a'..='z')
1435 .flat_map(|base| ('\u{300}'..='\u{30F}').map(move |mark| format!("{base}{mark}")))
1436 .take(GlyphIndex::OVERLAY_CLUSTER_MAX + 50)
1437 .collect();
1438 idx.trim_overlay_clusters();
1439 for cluster in &clusters {
1440 idx.ensure_overlay_cluster(&mut shaper, cluster);
1441 }
1442 assert!(
1443 clusters.iter().all(|c| idx.overlay_cluster(c).is_some()),
1444 "a label past the bound keeps every cluster for the frame it paints"
1445 );
1446 idx.trim_overlay_clusters();
1447 assert!(
1448 idx.overlay_cluster(&clusters[0]).is_none(),
1449 "bounded between frames"
1450 );
1451 }
1452
1453 #[cfg(not(windows))]
1454 #[test]
1455 fn ensure_records_a_slot_and_pending_upload_for_a_printable_glyph() {
1456 let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1457 let h = idx.cell_metrics().height;
1458 let sk = SizingKey::default();
1459 let did_work = idx.ensure('A', h, sk);
1460 assert!(did_work, "first ensure must rasterize + allocate");
1461 assert!(idx.contains('A', h, sk));
1462 let slot = idx.slot('A', h, sk).expect("A produced a slot");
1463 assert!(slot.atlas_px[0] + slot.size_px[0] <= idx.sheet_side(AtlasKind::Mono));
1464 assert!(slot.atlas_px[1] + slot.size_px[1] <= idx.sheet_side(AtlasKind::Mono));
1465 assert!(slot.size_px[0] > 0 && slot.size_px[1] > 0);
1466 let mut iter = idx.drain_pending();
1467 let first = iter.next().expect("one upload queued");
1468 assert!(iter.next().is_none(), "exactly one upload queued");
1469 assert_eq!(
1470 first.pixels.as_bytes().len(),
1471 (first.alloc.width * first.alloc.height) as usize,
1472 "pixel buffer length matches the allocation"
1473 );
1474 }
1475
1476 #[cfg(not(windows))]
1477 #[test]
1478 fn ensure_is_idempotent_for_the_same_key() {
1479 let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1480 let h = idx.cell_metrics().height;
1481 let sk = SizingKey::default();
1482 assert!(idx.ensure('B', h, sk));
1483 assert!(!idx.ensure('B', h, sk), "second ensure is a hit");
1484 assert_eq!(
1485 idx.drain_pending().count(),
1486 1,
1487 "second ensure must not enqueue another upload"
1488 );
1489 }
1490
1491 #[cfg(not(windows))]
1492 #[test]
1493 fn whitespace_caches_a_negative_entry_with_no_upload() {
1494 let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1495 let h = idx.cell_metrics().height;
1496 let sk = SizingKey::default();
1497 idx.ensure(' ', h, sk);
1498 assert!(idx.contains(' ', h, sk));
1499 assert_eq!(idx.slot(' ', h, sk), None, "space resolves to no slot");
1500 assert!(
1501 idx.drain_pending().next().is_none(),
1502 "blank glyphs do not queue uploads"
1503 );
1504 }
1505
1506 #[cfg(not(windows))]
1507 #[test]
1508 fn populate_walks_the_grid_and_caches_unique_glyphs_once() {
1509 let mut screen = Grid::new(2, 5);
1510 let mut parser = Parser::new();
1511 parser.advance(&mut screen, b"hello\r\nhello");
1512 let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1513 idx.populate(screen.screen());
1514 let h = idx.cell_metrics().height;
1515 let sk = SizingKey::default();
1516 for c in ['h', 'e', 'l', 'o'] {
1517 assert!(idx.contains(c, h, sk), "missing cache entry for {c:?}");
1518 assert!(idx.slot(c, h, sk).is_some(), "missing slot for {c:?}");
1519 }
1520 let pending_count = idx.drain_pending().count();
1521 assert_eq!(pending_count, 4);
1522 }
1523
1524 #[cfg(not(windows))]
1525 #[test]
1526 fn sized_variant_takes_a_distinct_atlas_slot_from_default() {
1527 let mut idx = GlyphIndex::new(stack(), 14.0, nz(512));
1529 let h = idx.cell_metrics().height;
1530 let default_sk = SizingKey::default();
1531 let scale_2_sk = SizingKey::new(2, 0, 0, 0, 0, 0);
1532 assert!(idx.ensure('A', h, default_sk), "first ensure must work");
1533 assert!(
1534 idx.ensure('A', h, scale_2_sk),
1535 "sized variant must rasterize separately"
1536 );
1537 let small = idx.slot('A', h, default_sk).expect("default slot");
1538 let large = idx.slot('A', h, scale_2_sk).expect("scaled slot");
1539 assert_ne!(
1540 (small.atlas_px, small.size_px),
1541 (large.atlas_px, large.size_px),
1542 "sized variant must occupy a different atlas rect"
1543 );
1544 assert!(
1546 large.size_px[1] > small.size_px[1],
1547 "scaled bitmap height ({}) must exceed default ({})",
1548 large.size_px[1],
1549 small.size_px[1]
1550 );
1551 }
1552
1553 #[cfg(not(windows))]
1554 #[test]
1555 fn a_full_atlas_resets_and_keeps_placing_glyphs() {
1556 let mut idx = GlyphIndex::new(stack(), 14.0, nz(48));
1559 let h = idx.cell_metrics().height;
1560 let sk = SizingKey::default();
1561 let mut reset_seen = false;
1562 let mut placed_after_reset = false;
1563 let mut prev_len = idx.slots.len();
1564 for c in ('!'..='~').chain('\u{a1}'..='\u{17f}') {
1565 idx.ensure(c, h, sk);
1566 let len = idx.slots.len();
1567 if len < prev_len {
1568 reset_seen = true;
1569 assert!(
1570 idx.slot(c, h, sk).is_some(),
1571 "post-reset glyph {c:?} must get a real slot",
1572 );
1573 placed_after_reset = true;
1574 }
1575 prev_len = len;
1576 }
1577 assert!(
1578 reset_seen,
1579 "a page of glyphs must overflow the tiny atlas and reset it",
1580 );
1581 assert!(
1582 placed_after_reset,
1583 "allocations must resume after a reset instead of turning into permanent misses",
1584 );
1585 }
1586
1587 fn osc66_sizings() -> impl Iterator<Item = SizingKey> {
1589 (1..=7u8).flat_map(|s| {
1590 (0..=7u8).flat_map(move |w| {
1591 (0..=15u8).flat_map(move |d| {
1592 (0..d.max(1)).flat_map(move |n| {
1593 (0..=2u8).flat_map(move |v| {
1594 (0..=2u8).map(move |h| SizingKey::new(s, w, n, d, v, h))
1595 })
1596 })
1597 })
1598 })
1599 })
1600 }
1601
1602 #[cfg(not(windows))]
1603 #[test]
1604 fn distinct_blank_keys_keep_the_slot_map_under_the_blank_cap() {
1605 let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1606 let h = idx.cell_metrics().height;
1607 for sk in osc66_sizings().take(3 * GlyphIndex::BLANK_SLOT_MAX) {
1608 idx.ensure(' ', h, sk);
1609 assert_eq!(idx.slot(' ', h, sk), None, "a sized space stays blank");
1610 assert!(
1611 idx.slots.len() <= GlyphIndex::BLANK_SLOT_MAX,
1612 "{} slot entries after blank key {sk:?}",
1613 idx.slots.len(),
1614 );
1615 }
1616 }
1617
1618 #[cfg(not(windows))]
1619 #[test]
1620 fn dropping_blank_entries_keeps_placed_slots_and_the_atlas() {
1621 let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1622 let h = idx.cell_metrics().height;
1623 let sk = SizingKey::default();
1624 idx.ensure('A', h, sk);
1625 let placed = idx.slot('A', h, sk).expect("A is placed");
1626 let _ = idx.drain_pending().count();
1627 for blank in osc66_sizings().take(2 * GlyphIndex::BLANK_SLOT_MAX) {
1628 idx.ensure(' ', h, blank);
1629 }
1630 assert_eq!(
1631 idx.atlas_resets(),
1632 0,
1633 "blank entries never recycle the atlas"
1634 );
1635 assert!(!idx.ensure('A', h, sk), "A is still resident");
1636 assert_eq!(idx.slot('A', h, sk), Some(placed));
1637 assert!(idx.drain_pending().next().is_none(), "nothing re-uploads");
1638 }
1639
1640 fn placed_slot_count(idx: &GlyphIndex) -> usize {
1641 idx.slots.values().filter(|s| s.is_some()).count()
1642 }
1643
1644 #[cfg(not(windows))]
1645 #[test]
1646 fn distinct_tiny_placed_glyphs_recycle_the_atlas_at_the_placed_cap() {
1647 let mut idx = GlyphIndex::new(stack(), 14.0, nz(GLYPH_ATLAS_SIDE));
1648 let h = idx.cell_metrics().height;
1649 let fractional = osc66_sizings()
1650 .filter(|sk| sk.scale() == 1 && sk.frac_num() > 0 && sk.frac_num() < sk.frac_den());
1651 let keys = fractional.flat_map(|sk| ('A'..='Z').map(move |c| (c, sk)));
1652 let mut placed = 0;
1653 let mut last = None;
1654 for (c, sk) in keys {
1655 let before = idx.placed_slots;
1656 let resets = idx.atlas_resets();
1657 idx.ensure(c, h, sk);
1658 if idx.atlas_resets() != resets {
1659 assert_eq!(
1660 before,
1661 GlyphIndex::PLACED_SLOT_MAX,
1662 "the cap, not the sheet, recycles tiny glyphs",
1663 );
1664 }
1665 assert!(idx.placed_slots <= GlyphIndex::PLACED_SLOT_MAX);
1666 if let Some(slot) = idx.slot(c, h, sk) {
1667 placed += 1;
1668 last = Some((c, sk, slot));
1669 if placed == GlyphIndex::PLACED_SLOT_MAX + 1 {
1670 break;
1671 }
1672 }
1673 }
1674 assert_eq!(placed, GlyphIndex::PLACED_SLOT_MAX + 1);
1675 assert_eq!(idx.atlas_resets(), 1);
1676 assert_eq!(placed_slot_count(&idx), idx.placed_slots);
1677 let (c, sk, slot) = last.expect("a glyph was placed");
1678 assert!(
1679 !idx.ensure(c, h, sk),
1680 "the newest glyph survives the recycle"
1681 );
1682 assert_eq!(idx.slot(c, h, sk), Some(slot));
1683 }
1684
1685 #[cfg(not(windows))]
1689 #[test]
1690 fn an_overlay_char_that_overflows_after_the_walk_reports_the_reset() {
1691 let mut idx = GlyphIndex::new(stack(), 14.0, nz(32));
1692 let h = idx.cell_metrics().height;
1693 let sk = SizingKey::default();
1694
1695 let text = "uvwxyz";
1696 let mut screen = Grid::new(1, 8);
1697 Parser::new().advance(&mut screen, text.as_bytes());
1698 idx.populate(screen.screen());
1699 assert!(
1700 !idx.atlas_was_reset(),
1701 "the walk of one short row must fit, isolating the overlay overflow",
1702 );
1703
1704 for c in '\u{a1}'..='\u{2ff}' {
1707 if idx.atlas_was_reset() {
1708 break;
1709 }
1710 idx.ensure(c, h, sk);
1711 }
1712 assert!(
1713 idx.atlas_was_reset(),
1714 "priming overlay text after the walk must report the reset",
1715 );
1716 assert!(
1717 text.chars().any(|c| idx.slot(c, h, sk).is_none()),
1718 "the grid slots handed out before the overlay's reset are gone",
1719 );
1720 }
1721
1722 #[cfg(not(windows))]
1726 #[test]
1727 fn a_mid_walk_reset_is_reported_and_a_second_walk_reslots_the_grid() {
1728 let mut idx = GlyphIndex::new(stack(), 14.0, nz(48));
1729 let h = idx.cell_metrics().height;
1730 let sk = SizingKey::default();
1731
1732 let capacity = {
1735 let mut probe = GlyphIndex::new(stack(), 14.0, nz(48));
1736 ('!'..='~')
1737 .take_while(|c| {
1738 let before = probe.slots.len();
1739 probe.ensure(*c, h, sk);
1740 probe.slots.len() > before
1741 })
1742 .count()
1743 };
1744 let text = "uvwxyz";
1745 assert!(
1746 capacity > text.len() + 2,
1747 "the sheet must hold the screen's own set with room to pre-fill",
1748 );
1749
1750 for c in ('!'..='~').take(capacity - 2) {
1753 idx.ensure(c, h, sk);
1754 }
1755
1756 let mut screen = Grid::new(1, 8);
1757 Parser::new().advance(&mut screen, text.as_bytes());
1758
1759 idx.populate(screen.screen());
1760 assert!(
1761 idx.atlas_was_reset(),
1762 "the pre-filled sheet must overflow during the walk",
1763 );
1764 assert!(
1765 text.chars().any(|c| idx.slot(c, h, sk).is_none()),
1766 "the glyphs primed before the reset lost their slots",
1767 );
1768
1769 idx.populate(screen.screen());
1770 assert!(
1771 !idx.atlas_was_reset(),
1772 "the reclaimed sheet holds the screen's own set in one walk",
1773 );
1774 for c in text.chars() {
1775 assert!(
1776 idx.slot(c, h, sk).is_some(),
1777 "second walk must re-slot {c:?} so its cell paints this frame",
1778 );
1779 }
1780 }
1781
1782 #[cfg(not(windows))]
1783 #[test]
1784 fn a_glyph_too_large_for_the_atlas_degrades_without_looping() {
1785 let mut idx = GlyphIndex::new(stack(), 14.0, nz(2));
1788 let h = idx.cell_metrics().height;
1789 let sk = SizingKey::default();
1790 idx.ensure('W', h, sk);
1791 assert_eq!(
1792 idx.slot('W', h, sk),
1793 None,
1794 "a glyph larger than the whole atlas degrades to no slot",
1795 );
1796 }
1797
1798 #[cfg(not(windows))]
1799 #[test]
1800 fn fractional_scale_rasterizes_below_default_for_sub_cell_shrink() {
1801 let mut idx = GlyphIndex::new(stack(), 14.0, nz(512));
1804 let h = idx.cell_metrics().height;
1805 let default_sk = SizingKey::default();
1806 let frac_half_sk = SizingKey::new(1, 0, 1, 2, 0, 0);
1807 let scale_2_sk = SizingKey::new(2, 0, 0, 0, 0, 0);
1808 assert!(idx.ensure('A', h, default_sk));
1809 assert!(idx.ensure('A', h, frac_half_sk));
1810 assert!(idx.ensure('A', h, scale_2_sk));
1811 let small = idx.slot('A', h, frac_half_sk).expect("fractional slot");
1812 let mid = idx.slot('A', h, default_sk).expect("default slot");
1813 let large = idx.slot('A', h, scale_2_sk).expect("integer scale=2 slot");
1814 assert!(
1817 small.size_px[1] < mid.size_px[1],
1818 "0.5× bitmap height ({}) must be below default ({})",
1819 small.size_px[1],
1820 mid.size_px[1],
1821 );
1822 assert!(
1823 large.size_px[1] > mid.size_px[1],
1824 "2× bitmap height ({}) must exceed default ({})",
1825 large.size_px[1],
1826 mid.size_px[1],
1827 );
1828 }
1829
1830 #[cfg(not(windows))]
1831 #[test]
1832 fn primary_face_ascii_is_never_fitted_at_any_osc66_scale() {
1833 let font = Arc::new(Font::load_test_font());
1834 for px in [13.0, 18.67, 24.0, 29.33] {
1835 let mut idx = GlyphIndex::new(FontStack::new(font.clone()), px, nz(512));
1836 let h = idx.cell_metrics().height;
1837 for s in 1..=7 {
1838 let sk = SizingKey::new(s, 0, 0, 0, 0, 0);
1839 for c in '!'..='~' {
1840 assert!(
1841 idx.fitted_bitmap(&font, c, h, sk).is_none(),
1842 "{c:?} at {px} px, s={s}"
1843 );
1844 }
1845 }
1846 }
1847 }
1848
1849 const OVERWIDE: [char; 6] = [
1852 '\u{263A}',
1853 '\u{2764}',
1854 '\u{2654}',
1855 '\u{26F5}',
1856 '\u{2B50}',
1857 '\u{1F30D}',
1858 ];
1859
1860 #[cfg(not(windows))]
1863 #[test]
1864 fn a_fallback_glyph_wider_than_its_cell_fills_and_centres_in_it() {
1865 let stack = FontStack::pinned_test_stack();
1866 let mut idx = GlyphIndex::new(stack, 18.0, nz(1024));
1867 let m = idx.cell_metrics();
1868 let (cw, ch, ascent) = (i64::from(m.width), i64::from(m.height), i64::from(m.ascent));
1869 let mut fitted = 0;
1870 for c in OVERWIDE
1871 .into_iter()
1872 .filter(|&c| felis_grid::char_cell_width(c) == 1)
1873 {
1874 let font = idx.stack().resolve(c, FontStyle::REGULAR).clone();
1875 let Some(bitmap) = idx.fitted_bitmap(&font, c, m.height, SizingKey::default()) else {
1876 continue;
1877 };
1878 fitted += 1;
1879 let (w, h) = (i64::from(bitmap.width()), i64::from(bitmap.height()));
1880 let (left, top) = (i64::from(bitmap.left), i64::from(bitmap.top));
1881 let right = cw - left - w;
1882 let (above, below) = (ascent - top, ch - (ascent - top) - h);
1883 assert!(
1884 left >= 0 && right >= -1,
1885 "{c:?} inside the cell: left {left}, right {right}"
1886 );
1887 assert!(
1888 above >= -1 && below >= -1,
1889 "{c:?} inside the cell: above {above}, below {below}"
1890 );
1891 assert!(
1892 (left - right).abs() <= 1,
1893 "{c:?} centred: left {left}, right {right}"
1894 );
1895 assert!(
1896 (above - below).abs() <= 1,
1897 "{c:?} centred: above {above}, below {below}"
1898 );
1899 assert!(
1900 w >= cw - 2 || h >= ch - 2,
1901 "{c:?} fills an axis: {w}x{h} in {cw}x{ch}"
1902 );
1903 }
1904 assert!(
1905 fitted > 0,
1906 "no candidate overruns its cell in the pinned set"
1907 );
1908 }
1909
1910 #[cfg(not(windows))]
1914 #[test]
1915 fn osc66_width_and_fractional_scale_set_the_fitted_block() {
1916 let stack = FontStack::pinned_test_stack();
1917 let px = 18.0;
1918 let mut idx = GlyphIndex::new(stack, px, nz(1024));
1919 let m = idx.cell_metrics();
1920 let cw = m.width as f32;
1921 let (c, font) = OVERWIDE
1922 .into_iter()
1923 .filter(|&c| felis_grid::char_cell_width(c) == 1)
1924 .map(|c| (c, idx.stack().resolve(c, FontStyle::REGULAR).clone()))
1925 .find(|(c, font)| {
1926 font.advance_px(*c, px)
1927 .is_some_and(|a| a > cw + 1.0 && a < 2.0 * cw)
1928 })
1929 .expect("the pinned set has a width-1 glyph between one and two cells wide");
1930 let w1 = SizingKey::new(1, 1, 0, 0, 0, 0);
1931 let w2 = SizingKey::new(1, 2, 0, 0, 0, 0);
1932 let half_in_s2 = SizingKey::new(2, 0, 1, 2, 0, 0);
1933 assert!(
1934 idx.fitted_bitmap(&font, c, m.height, w1).is_some(),
1935 "w=1 fits {c:?}"
1936 );
1937 assert!(
1938 idx.fitted_bitmap(&font, c, m.height, w2).is_none(),
1939 "w=2 holds {c:?}"
1940 );
1941 assert!(
1942 idx.fitted_bitmap(&font, c, m.height, half_in_s2).is_none(),
1943 "s=2 n=1 d=2 holds {c:?}"
1944 );
1945 }
1946
1947 #[cfg(not(windows))]
1950 #[test]
1951 fn an_emoji_presentation_char_rasterizes_in_color() {
1952 let stack = FontStack::pinned_test_stack();
1953 let mut idx = GlyphIndex::new(stack, 18.0, nz(1024));
1954 let h = idx.cell_metrics().height;
1955 idx.ensure('\u{2B50}', h, SizingKey::default());
1956 let slot = idx
1957 .slot('\u{2B50}', h, SizingKey::default())
1958 .expect("a star slot");
1959 assert!(slot.is_color);
1960 }
1961
1962 struct Walked {
1964 index: GlyphIndex,
1965 frame: ShapeFrame,
1966 grid: Grid,
1967 }
1968
1969 fn walk_pinned(bytes: &[u8], cols: u16) -> Walked {
1970 let stack = FontStack::pinned_test_stack();
1971 let mut grid = Grid::new(1, cols);
1972 Parser::new().advance(&mut grid, bytes);
1973 let mut index = GlyphIndex::new(stack, 18.0, nz(1024));
1974 let mut frame = ShapeFrame::empty();
1975 GridWalker::default().walk(
1976 &mut index,
1977 grid.screen(),
1978 &mut Shaper::new(),
1979 &[],
1980 &mut frame,
1981 );
1982 Walked { index, frame, grid }
1983 }
1984
1985 fn cluster_fit_px_at(p: &Walked, col: u16) -> u16 {
1986 match p.frame.cell_at(0, col) {
1987 ShapedCell::Cluster { fit_px, .. } => fit_px,
1988 other => panic!("col {col} is not a cluster: {other:?}"),
1989 }
1990 }
1991
1992 #[cfg(not(windows))]
1995 #[test]
1996 fn a_cluster_within_its_cells_is_not_fitted() {
1997 for text in [
1998 "\u{1F1EF}\u{1F1F5}",
1999 "\u{2764}\u{FE0F}",
2000 "#\u{FE0F}\u{20E3}",
2001 "e\u{301}",
2002 ] {
2003 let p = walk_pinned(text.as_bytes(), 4);
2004 assert_eq!(cluster_fit_px_at(&p, 0), 0, "{text:?}");
2005 }
2006 }
2007
2008 #[cfg(not(windows))]
2011 #[test]
2012 fn a_fitted_cluster_is_measured_once() {
2013 let mut p = walk_pinned("\u{1F408}\u{200D}\u{1F408}".as_bytes(), 4);
2014 assert_eq!(p.index.cluster_fits.len(), 1);
2015 let first = cluster_fit_px_at(&p, 0);
2016 p.index.cluster_fits.values_mut().for_each(|px| *px += 1);
2017 let mut frame = ShapeFrame::empty();
2018 p.index.walked = None;
2019 GridWalker::default().walk(
2020 &mut p.index,
2021 p.grid.screen(),
2022 &mut Shaper::new(),
2023 &[],
2024 &mut frame,
2025 );
2026 p.frame = frame;
2027 assert_eq!(
2028 cluster_fit_px_at(&p, 0),
2029 first + 1,
2030 "the walk read the memo"
2031 );
2032 }
2033
2034 #[cfg(not(windows))]
2038 #[test]
2039 fn vertical_box_drawing_bitmap_exactly_fills_the_cell() {
2040 let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
2041 let metrics = idx.cell_metrics();
2042 let sk = SizingKey::default();
2043 assert!(idx.ensure('│', metrics.height, sk));
2044 let slot = idx.slot('│', metrics.height, sk).expect("│ slot");
2045 assert_eq!(
2046 slot.size_px,
2047 [metrics.width, metrics.height],
2048 "synthetic │ must fill the cell so adjacent rows connect",
2049 );
2050 let ascent_i32 = i32::try_from(metrics.ascent).expect("ascent fits in i32");
2051 assert_eq!(
2052 slot.offset_px,
2053 [0, ascent_i32],
2054 "left=0 + top=ascent pins the bitmap at the cell's top-left",
2055 );
2056 }
2057
2058 #[cfg(not(windows))]
2059 #[test]
2060 fn allocations_within_the_atlas_never_overlap() {
2061 let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
2062 let h = idx.cell_metrics().height;
2063 let sk = SizingKey::default();
2064 let mut rects: Vec<(u32, u32, u32, u32)> = Vec::new();
2065 for c in 0x21u8..=0x7Eu8 {
2066 idx.ensure(c as char, h, sk);
2067 if let Some(s) = idx.slot(c as char, h, sk) {
2068 let [x, y] = s.atlas_px;
2069 rects.push((x, y, x + s.size_px[0], y + s.size_px[1]));
2070 }
2071 }
2072 for (i, a) in rects.iter().enumerate() {
2073 for b in &rects[i + 1..] {
2074 let separated = a.2 <= b.0 || b.2 <= a.0 || a.3 <= b.1 || b.3 <= a.1;
2075 assert!(separated, "overlap between {a:?} and {b:?}");
2076 }
2077 }
2078 }
2079
2080 #[cfg(not(windows))]
2083 #[test]
2084 fn ensure_glyph_id_is_idempotent_and_caches_a_slot() {
2085 use felis_shaping::Shaper;
2086 let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
2087 let h = idx.cell_metrics().height;
2088 let sk = SizingKey::default();
2089 let primary = idx.stack().primary().clone();
2090 let mut shaper = Shaper::new();
2091 let shaped = shaper.shape_run(&primary, 14.0, &[], "A");
2092 let glyph_id = shaped[0].glyph_id;
2093 assert_ne!(glyph_id, 0);
2094 let first = idx.ensure_glyph_id(glyph_id, h, sk);
2095 assert!(first, "first ensure must rasterize + allocate");
2096 assert!(idx.contains_glyph_id(glyph_id, h, sk));
2097 assert!(idx.glyph_id_slot(glyph_id, h, sk).is_some());
2098 let second = idx.ensure_glyph_id(glyph_id, h, sk);
2099 assert!(!second, "second ensure is a cache hit");
2100 }
2101
2102 #[cfg(not(windows))]
2104 #[test]
2105 fn ensure_glyph_id_zero_lands_a_negative_slot() {
2106 let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
2107 let h = idx.cell_metrics().height;
2108 let sk = SizingKey::default();
2109 idx.ensure_glyph_id(0, h, sk);
2110 assert!(idx.contains_glyph_id(0, h, sk));
2111 assert_eq!(idx.glyph_id_slot(0, h, sk), None);
2112 assert_eq!(idx.drain_pending().count(), 0);
2113 }
2114
2115 #[test]
2116 fn empty_shape_frame_reports_none_for_every_cell() {
2117 let frame = ShapeFrame::empty();
2118 assert_eq!(frame.cell_at(0, 0), ShapedCell::None);
2119 assert_eq!(frame.cell_at(100, 100), ShapedCell::None);
2120 assert_eq!(frame.cols(), 0);
2121 assert_eq!(frame.cells(), []);
2122 }
2123
2124 #[cfg(not(windows))]
2127 #[test]
2128 fn shape_run_stops_at_non_ascii_grapheme() {
2129 let mut screen = Grid::new(1, 5);
2130 let mut parser = Parser::new();
2131 parser.advance(&mut screen, "aあb".as_bytes());
2132 let stack = stack();
2133 let primary = stack.primary().clone();
2134 let mut text = String::new();
2135 let run = next_shape_run(screen.screen(), 0, 0, &mut text, |c, _style| {
2136 primary.has_glyph(c)
2137 })
2138 .expect("first cell must start a run");
2139 assert_eq!(text, "a");
2140 assert_eq!(run.cell_count, 1, "non-ASCII grapheme stops the sweep");
2141 }
2142
2143 #[cfg(not(windows))]
2147 #[test]
2148 fn glyph_id_slots_key_on_font_id_so_faces_do_not_collide() {
2149 let mut idx = GlyphIndex::new(stack(), 14.0, nz(512));
2150 let h = idx.cell_metrics().height;
2151 let face0 = SizingKey::default();
2152 let face2 = SizingKey::default().with_font_id(2);
2153 assert_ne!(face0, face2, "font-id must change the key");
2154 assert!(
2155 idx.ensure_glyph_id(7, h, face0),
2156 "first face-0 insert works"
2157 );
2158 assert!(
2159 idx.ensure_glyph_id(7, h, face2),
2160 "same id under a different font-id is a distinct slot, not a hit",
2161 );
2162 assert!(
2163 !idx.ensure_glyph_id(7, h, face0),
2164 "re-inserting the face-0 key is a cache hit",
2165 );
2166 assert!(idx.contains_glyph_id(7, h, face0));
2167 assert!(idx.contains_glyph_id(7, h, face2));
2168 }
2169
2170 #[cfg(not(windows))]
2174 #[test]
2175 fn single_glyph_ligature_cluster_marks_primary_then_trailing() {
2176 let stack = stack();
2177 let mut idx = GlyphIndex::new(stack, 14.0, nz(512));
2178 let mut frame_cells = vec![ShapedCell::None; 4];
2179 let shaped = [ShapedGlyph {
2180 glyph_id: 42,
2181 source_byte_start: 0,
2182 source_byte_len: 2,
2183 advance_px: 16.0,
2184 x_offset_px: 0.0,
2185 y_offset_px: 0.0,
2186 from_ligature: true,
2187 }];
2188 apply_shaped_run(
2189 &mut frame_cells,
2190 &mut idx,
2191 4,
2192 &RunPlacement {
2193 row: 0,
2194 start_col: 0,
2195 cell_count: 2,
2196 style: FontStyle::REGULAR,
2197 },
2198 &shaped,
2199 );
2200 assert_eq!(
2201 frame_cells[0],
2202 ShapedCell::Primary {
2203 glyph_id: 42,
2204 span_cols: 2,
2205 },
2206 );
2207 assert_eq!(frame_cells[1], ShapedCell::Trailing);
2208 assert_eq!(frame_cells[2], ShapedCell::None);
2209 }
2210
2211 #[cfg(not(windows))]
2216 #[test]
2217 fn multi_glyph_ligature_cluster_places_pieces_by_pen_position() {
2218 let stack = stack();
2219 let mut idx = GlyphIndex::new(stack, 14.0, nz(512));
2220 let mut frame_cells = vec![ShapedCell::None; 4];
2221 let piece = |glyph_id| ShapedGlyph {
2222 glyph_id,
2223 source_byte_start: 0,
2224 source_byte_len: 2,
2225 advance_px: 8.0,
2226 x_offset_px: 0.0,
2227 y_offset_px: 0.0,
2228 from_ligature: true,
2229 };
2230 let shaped = [piece(100), piece(200)];
2231 apply_shaped_run(
2232 &mut frame_cells,
2233 &mut idx,
2234 4,
2235 &RunPlacement {
2236 row: 0,
2237 start_col: 0,
2238 cell_count: 2,
2239 style: FontStyle::REGULAR,
2240 },
2241 &shaped,
2242 );
2243 assert_eq!(
2244 frame_cells[0],
2245 ShapedCell::Primary {
2246 glyph_id: 100,
2247 span_cols: 1,
2248 },
2249 "lead piece (pen 0) anchors at the cluster's first cell",
2250 );
2251 assert_eq!(
2252 frame_cells[1],
2253 ShapedCell::Primary {
2254 glyph_id: 200,
2255 span_cols: 1,
2256 },
2257 "body piece (pen = one advance) anchors at the second cell",
2258 );
2259 assert_eq!(frame_cells[2], ShapedCell::None);
2260 }
2261
2262 #[cfg(not(windows))]
2265 #[test]
2266 fn a_ligature_wider_than_its_cells_leaves_them_unshaped() {
2267 let mut idx = GlyphIndex::new(stack(), 14.0, nz(512));
2268 let cw = idx.cell_metrics().width as f32;
2269 let run = RunPlacement {
2270 row: 0,
2271 start_col: 0,
2272 cell_count: 2,
2273 style: FontStyle::REGULAR,
2274 };
2275 let ligature = |advance_px| ShapedGlyph {
2276 glyph_id: 42,
2277 source_byte_start: 0,
2278 source_byte_len: 2,
2279 advance_px,
2280 x_offset_px: 0.0,
2281 y_offset_px: 0.0,
2282 from_ligature: true,
2283 };
2284 let mut wide = vec![ShapedCell::None; 2];
2285 apply_shaped_run(
2286 &mut wide,
2287 &mut idx,
2288 2,
2289 &run,
2290 &[ligature(cw.mul_add(2.0, 3.0))],
2291 );
2292 assert_eq!(wide, [ShapedCell::None, ShapedCell::None]);
2293 let mut fitting = vec![ShapedCell::None; 2];
2294 apply_shaped_run(
2295 &mut fitting,
2296 &mut idx,
2297 2,
2298 &run,
2299 &[ligature(cw.mul_add(2.0, 1.0))],
2300 );
2301 assert_eq!(
2302 fitting,
2303 [
2304 ShapedCell::Primary {
2305 glyph_id: 42,
2306 span_cols: 2,
2307 },
2308 ShapedCell::Trailing,
2309 ],
2310 );
2311 }
2312
2313 #[cfg(not(windows))]
2317 #[test]
2318 fn ligature_run_places_monaspace_arrow_pieces() {
2319 use felis_shaping::{Font, FontStack, Shaper};
2320 let font = Font::load_test_font();
2321 let features = vec!["ss03".to_owned()];
2322 let mut screen = Grid::new(1, 6);
2323 let mut parser = Parser::new();
2324 parser.advance(&mut screen, b"-> ab");
2325 let primary = Arc::new(font);
2326 let stack = FontStack::with_primary_features(primary, features.clone());
2327 let mut idx = GlyphIndex::new(stack, 14.0, nz(512));
2328 let cell_height = idx.cell_metrics().height;
2329 let cols = screen.cols();
2330 let cell_count = usize::from(screen.rows()) * usize::from(cols);
2331 let mut frame_cells = vec![ShapedCell::None; cell_count];
2332 let mut shaper = Shaper::new();
2333 let primary_face = idx.stack().primary().clone();
2334 let mut col = 0u16;
2335 let mut run_text = String::new();
2336 while col < cols {
2337 let Some(run) = next_shape_run(screen.screen(), 0, col, &mut run_text, |c, _style| {
2338 primary_face.has_glyph(c)
2339 }) else {
2340 col += 1;
2341 continue;
2342 };
2343 let run_start = run.start_col;
2344 let run_cell_count = run.cell_count;
2345 col = run_start + run_cell_count;
2346 if run_text.is_empty() {
2347 continue;
2348 }
2349 let shaped = shaper.shape_run(&primary_face, 14.0, &features, &run_text);
2350 apply_shaped_run(
2351 &mut frame_cells,
2352 &mut idx,
2353 cols,
2354 &RunPlacement {
2355 row: 0,
2356 start_col: run_start,
2357 cell_count: run_cell_count,
2358 style: FontStyle::REGULAR,
2359 },
2360 &shaped,
2361 );
2362 }
2363 let plain_gid = |shaper: &mut Shaper, c: &str| {
2366 let out = shaper.shape_run(&primary_face, 14.0, &[], c);
2367 assert_eq!(out.len(), 1);
2368 out[0].glyph_id
2369 };
2370 let plain_hyphen = plain_gid(&mut shaper, "-");
2371 let plain_greater = plain_gid(&mut shaper, ">");
2372 match frame_cells[1] {
2373 ShapedCell::Primary {
2374 glyph_id,
2375 span_cols,
2376 } => {
2377 assert_ne!(glyph_id, 0, "arrow body glyph id must be non-zero");
2378 assert_ne!(
2379 glyph_id, plain_greater,
2380 "cell 1 must carry the substituted arrow piece, not `>`",
2381 );
2382 assert_eq!(span_cols, 1, "piece glyphs claim one cell each");
2383 assert!(idx.contains_glyph_id(glyph_id, cell_height, SizingKey::default()));
2384 }
2385 other => panic!("expected the arrow body Primary at (0,1), got {other:?}"),
2386 }
2387 match frame_cells[0] {
2388 ShapedCell::Primary { glyph_id, .. } => {
2389 assert_ne!(
2390 glyph_id, plain_hyphen,
2391 "cell 0 must carry the blank lead piece, not `-`",
2392 );
2393 }
2394 ShapedCell::Trailing => {}
2395 ShapedCell::None | ShapedCell::Cluster { .. } => {
2396 panic!("cell 0 must be consumed by the shaped ligature cluster")
2397 }
2398 }
2399 }
2400
2401 #[cfg(not(windows))]
2404 #[test]
2405 fn shape_run_cached_matches_direct_output_and_hits_on_repeat() {
2406 let stack = stack();
2407 let primary = stack.primary().clone();
2408 let features = vec!["calt".to_owned()];
2409 let mut idx = GlyphIndex::new(stack, 14.0, nz(256));
2410 let mut shaper = Shaper::new();
2411 let direct = shaper.shape_run(&primary, 14.0, &features, "Hello");
2412 let mut out = Vec::new();
2413 let first = idx.shape_run_cached(
2414 &mut shaper,
2415 &features,
2416 FontStyle::REGULAR,
2417 "Hello",
2418 &mut out,
2419 );
2420 assert!(first, "first call must shape (miss)");
2421 assert_eq!(out, direct, "memoized output must match a direct shape");
2422 let second = idx.shape_run_cached(
2423 &mut shaper,
2424 &features,
2425 FontStyle::REGULAR,
2426 "Hello",
2427 &mut out,
2428 );
2429 assert!(!second, "second call must be a cache hit");
2430 assert_eq!(out, direct, "hit must replay the identical output");
2431 }
2432
2433 #[cfg(not(windows))]
2436 #[test]
2437 fn shape_run_cache_stays_bounded_under_unique_text_pressure() {
2438 let stack = stack();
2439 let mut idx = GlyphIndex::new(stack, 14.0, nz(256));
2440 let mut shaper = Shaper::new();
2441 let mut out = Vec::new();
2442 let cap = GlyphIndex::SHAPED_RUN_CACHE_MAX;
2443 for i in 0..(cap + 16) {
2444 let text = format!("line {i}");
2445 idx.shape_run_cached(&mut shaper, &[], FontStyle::REGULAR, &text, &mut out);
2446 }
2447 assert!(
2448 idx.shaped_run_cache_len() <= cap,
2449 "cache len {} must not exceed cap {cap}",
2450 idx.shaped_run_cache_len(),
2451 );
2452 let kept = format!("line {}", cap + 15);
2453 assert!(
2454 !idx.shape_run_cached(&mut shaper, &[], FontStyle::REGULAR, &kept, &mut out),
2455 "most-recent insert must survive the eviction sweep",
2456 );
2457 }
2458
2459 #[cfg(not(windows))]
2463 #[test]
2464 fn shape_run_continues_across_attribute_boundary() {
2465 use felis_grid::{Attributes, Cell, Color};
2466 let mut screen = Grid::new(1, 4);
2467 let default_style = screen.style_table_mut().intern(Attributes::default());
2468 screen.set_cell(
2469 0,
2470 0,
2471 Cell {
2472 grapheme: Grapheme::Ascii(b'='),
2473 style: default_style,
2474 link: None,
2475 sizing: None,
2476 },
2477 );
2478 let differ = screen.style_table_mut().intern(Attributes {
2479 fg: Color::Indexed(1),
2480 ..Attributes::default()
2481 });
2482 screen.set_cell(
2483 0,
2484 1,
2485 Cell {
2486 grapheme: Grapheme::Ascii(b'>'),
2487 style: differ,
2488 link: None,
2489 sizing: None,
2490 },
2491 );
2492 let stack = stack();
2493 let primary = stack.primary().clone();
2494 let mut text = String::new();
2495 let run0 = next_shape_run(screen.screen(), 0, 0, &mut text, |c, _style| {
2496 primary.has_glyph(c)
2497 })
2498 .expect("first cell must start a run");
2499 assert_eq!(run0.cell_count, 2, "color boundary must not end the run");
2500 assert_eq!(text, "=>");
2501 }
2502
2503 #[cfg(not(windows))]
2505 #[test]
2506 fn shape_run_breaks_at_bold_italic_boundary() {
2507 use felis_grid::{AttrFlags, Attributes, Cell};
2508 let mut screen = Grid::new(1, 4);
2509 let default_style = screen.style_table_mut().intern(Attributes::default());
2510 screen.set_cell(
2511 0,
2512 0,
2513 Cell {
2514 grapheme: Grapheme::Ascii(b'='),
2515 style: default_style,
2516 link: None,
2517 sizing: None,
2518 },
2519 );
2520 let bold = screen.style_table_mut().intern(Attributes {
2521 flags: AttrFlags::BOLD,
2522 ..Attributes::default()
2523 });
2524 screen.set_cell(
2525 0,
2526 1,
2527 Cell {
2528 grapheme: Grapheme::Ascii(b'>'),
2529 style: bold,
2530 link: None,
2531 sizing: None,
2532 },
2533 );
2534 let stack = stack();
2535 let primary = stack.primary().clone();
2536 let mut text = String::new();
2537 let run0 = next_shape_run(screen.screen(), 0, 0, &mut text, |c, _style| {
2538 primary.has_glyph(c)
2539 })
2540 .expect("first cell must start a run");
2541 assert_eq!(run0.cell_count, 1, "bold boundary must end the run");
2542 assert_eq!(text, "=");
2543 assert_eq!(run0.style, FontStyle::REGULAR);
2544 let run1 = next_shape_run(screen.screen(), 0, 1, &mut text, |c, _style| {
2545 primary.has_glyph(c)
2546 })
2547 .expect("second cell starts a fresh run");
2548 assert_eq!(text, ">");
2549 assert_eq!(
2550 run1.style,
2551 FontStyle {
2552 bold: true,
2553 italic: false
2554 },
2555 );
2556 }
2557
2558 #[cfg(not(windows))]
2561 #[test]
2562 fn styled_cells_get_distinct_atlas_slots() {
2563 let stack = stack();
2564 let mut idx = GlyphIndex::new(stack, 14.0, nz(512));
2565 let h = idx.cell_metrics().height;
2566 let regular = SizingKey::default();
2567 let bold = SizingKey::default().with_style(FontStyle {
2568 bold: true,
2569 italic: false,
2570 });
2571 assert!(idx.ensure('A', h, regular), "regular 'A' is a miss");
2572 assert!(
2573 idx.ensure('A', h, bold),
2574 "bold 'A' must be a separate miss, not a hit on the regular slot",
2575 );
2576 assert!(idx.contains('A', h, regular));
2577 assert!(idx.contains('A', h, bold));
2578 }
2579}
2580
2581#[cfg(test)]
2582#[cfg(not(windows))]
2583mod walk_tests;