1use super::{CellRenderer, GlyphInfo};
2
3pub(crate) struct RasterizedGlyph {
4 pub width: u32,
5 pub height: u32,
6 pub bearing_x: f32,
7 pub bearing_y: f32,
8 pub pixels: Vec<u8>,
9 pub is_colored: bool,
10}
11
12pub mod symbol_ranges {
17 pub const DINGBATS_START: u32 = 0x2700;
20 pub const DINGBATS_END: u32 = 0x27BF;
21
22 pub const MISC_SYMBOLS_START: u32 = 0x2600;
25 pub const MISC_SYMBOLS_END: u32 = 0x26FF;
26
27 pub const MISC_TECHNICAL_START: u32 = 0x2300;
30 pub const MISC_TECHNICAL_END: u32 = 0x23FF;
31
32 pub const MISC_SYMBOLS_ARROWS_START: u32 = 0x2B00;
35 pub const MISC_SYMBOLS_ARROWS_END: u32 = 0x2BFF;
36}
37
38pub fn should_render_as_symbol(ch: char) -> bool {
47 let code = ch as u32;
48
49 if (symbol_ranges::MISC_TECHNICAL_START..=symbol_ranges::MISC_TECHNICAL_END).contains(&code) {
51 return true;
52 }
53
54 if (symbol_ranges::MISC_SYMBOLS_START..=symbol_ranges::MISC_SYMBOLS_END).contains(&code) {
56 return true;
57 }
58
59 if (symbol_ranges::DINGBATS_START..=symbol_ranges::DINGBATS_END).contains(&code) {
61 return true;
62 }
63
64 if (symbol_ranges::MISC_SYMBOLS_ARROWS_START..=symbol_ranges::MISC_SYMBOLS_ARROWS_END)
66 .contains(&code)
67 {
68 return true;
69 }
70
71 false
72}
73
74impl CellRenderer {
75 pub fn clear_glyph_cache(&mut self) {
76 self.atlas.glyph_cache.clear();
77 self.atlas.lru_head = None;
78 self.atlas.lru_tail = None;
79 self.atlas.atlas_next_x = 0;
80 self.atlas.atlas_next_y = 0;
81 self.atlas.atlas_row_height = 0;
82 self.dirty_rows.fill(true);
83 self.upload_solid_pixel();
85 }
86
87 pub(crate) fn lru_remove(&mut self, key: u64) {
88 let info = self
89 .atlas
90 .glyph_cache
91 .get(&key)
92 .expect("Glyph cache entry must exist before calling lru_remove");
93 let prev = info.prev;
94 let next = info.next;
95
96 if let Some(p) = prev {
97 self.atlas
98 .glyph_cache
99 .get_mut(&p)
100 .expect("Glyph cache LRU prev entry must exist")
101 .next = next;
102 } else {
103 self.atlas.lru_head = next;
104 }
105
106 if let Some(n) = next {
107 self.atlas
108 .glyph_cache
109 .get_mut(&n)
110 .expect("Glyph cache LRU next entry must exist")
111 .prev = prev;
112 } else {
113 self.atlas.lru_tail = prev;
114 }
115 }
116
117 pub(crate) fn lru_push_front(&mut self, key: u64) {
118 let next = self.atlas.lru_head;
119 if let Some(n) = next {
120 self.atlas
121 .glyph_cache
122 .get_mut(&n)
123 .expect("Glyph cache LRU head entry must exist")
124 .prev = Some(key);
125 } else {
126 self.atlas.lru_tail = Some(key);
127 }
128
129 let info = self
130 .atlas
131 .glyph_cache
132 .get_mut(&key)
133 .expect("Glyph cache entry must exist before calling lru_push_front");
134 info.prev = None;
135 info.next = next;
136 self.atlas.lru_head = Some(key);
137 }
138
139 pub(crate) fn rasterize_glyph(
140 &mut self,
141 font_idx: usize,
142 glyph_id: u16,
143 force_monochrome: bool,
144 ) -> Option<RasterizedGlyph> {
145 let font = self.font_manager.get_font(font_idx)?;
146 use swash::scale::Render;
148 use swash::scale::image::Content;
149 use swash::zeno::Format;
150
151 let use_thin_strokes = self.should_use_thin_strokes();
154 let render_format = if !self.font.font_antialias {
155 Format::Alpha
157 } else if use_thin_strokes {
158 Format::Subpixel
160 } else {
161 Format::Alpha
163 };
164
165 let sources = if force_monochrome {
169 [
173 swash::scale::Source::Outline,
174 swash::scale::Source::ColorOutline(0),
175 swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
176 ]
177 } else {
178 [
180 swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
181 swash::scale::Source::ColorOutline(0),
182 swash::scale::Source::Outline,
183 ]
184 };
185
186 let mut scaler = self
189 .scale_context
190 .builder(*font)
191 .size(self.font.font_size_pixels)
192 .hint(self.font.font_hinting)
193 .build();
194
195 let mut image = Render::new(&sources)
196 .format(render_format)
197 .render(&mut scaler, glyph_id)?;
198
199 if matches!(image.content, Content::Mask) && image.data.iter().all(|&b| b == 0) {
203 if force_monochrome {
204 return None;
209 }
210 #[allow(clippy::drop_non_drop)]
214 drop(scaler);
216 let mut retry_scaler = self
217 .scale_context
218 .builder(*font)
219 .size(self.font.font_size_pixels)
220 .hint(self.font.font_hinting)
221 .build();
222 let color_sources = [
223 swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
224 swash::scale::Source::ColorOutline(0),
225 ];
226 if let Some(color_image) = Render::new(&color_sources)
227 .format(render_format)
228 .render(&mut retry_scaler, glyph_id)
229 {
230 image = color_image;
231 } else {
232 return None;
233 }
234 }
235
236 let (pixels, is_colored) = match image.content {
237 Content::Color => {
238 if force_monochrome {
239 let pixels = convert_color_to_alpha_mask(&image);
244 (pixels, false)
245 } else {
246 (image.data.clone(), true)
247 }
248 }
249 Content::Mask => {
250 let mut pixels = Vec::with_capacity(image.data.len() * 4);
251 for &mask in &image.data {
252 let alpha = if !self.font.font_antialias {
254 if mask > 127 { 255 } else { 0 }
255 } else {
256 mask
257 };
258 pixels.extend_from_slice(&[255, 255, 255, alpha]);
259 }
260 (pixels, false)
261 }
262 Content::SubpixelMask => {
263 let pixels = convert_subpixel_mask_to_rgba(&image);
264 (pixels, false)
265 }
266 };
267
268 if !is_colored && pixels.iter().skip(3).step_by(4).all(|&a| a == 0) {
271 return None;
272 }
273
274 Some(RasterizedGlyph {
275 width: image.placement.width,
276 height: image.placement.height,
277 bearing_x: image.placement.left as f32,
278 bearing_y: image.placement.top as f32,
279 pixels,
280 is_colored,
281 })
282 }
283
284 pub(crate) fn upload_glyph(&mut self, _key: u64, raster: &RasterizedGlyph) -> GlyphInfo {
285 let padding = super::ATLAS_GLYPH_PADDING;
286 let atlas_size = self.atlas.atlas_size;
287 if self.atlas.atlas_next_x + raster.width + padding > atlas_size {
288 self.atlas.atlas_next_x = 0;
289 self.atlas.atlas_next_y += self.atlas.atlas_row_height + padding;
290 self.atlas.atlas_row_height = 0;
291 }
292
293 if self.atlas.atlas_next_y + raster.height + padding > atlas_size {
294 self.clear_glyph_cache();
295 }
296
297 let info = GlyphInfo {
298 key: _key,
299 x: self.atlas.atlas_next_x,
300 y: self.atlas.atlas_next_y,
301 width: raster.width,
302 height: raster.height,
303 bearing_x: raster.bearing_x,
304 bearing_y: raster.bearing_y,
305 is_colored: raster.is_colored,
306 prev: None,
307 next: None,
308 };
309
310 self.queue.write_texture(
311 wgpu::TexelCopyTextureInfo {
312 texture: &self.atlas.atlas_texture,
313 mip_level: 0,
314 origin: wgpu::Origin3d {
315 x: info.x,
316 y: info.y,
317 z: 0,
318 },
319 aspect: wgpu::TextureAspect::All,
320 },
321 &raster.pixels,
322 wgpu::TexelCopyBufferLayout {
323 offset: 0,
324 bytes_per_row: Some(4 * raster.width),
325 rows_per_image: Some(raster.height),
326 },
327 wgpu::Extent3d {
328 width: raster.width,
329 height: raster.height,
330 depth_or_array_layers: 1,
331 },
332 );
333
334 let pad_right_x = info.x + raster.width;
337 let pad_bottom_y = info.y + raster.height;
338
339 if pad_right_x + padding <= atlas_size && raster.height > 0 {
341 let zero = vec![0u8; (padding * raster.height * 4) as usize];
342 self.queue.write_texture(
343 wgpu::TexelCopyTextureInfo {
344 texture: &self.atlas.atlas_texture,
345 mip_level: 0,
346 origin: wgpu::Origin3d {
347 x: pad_right_x,
348 y: info.y,
349 z: 0,
350 },
351 aspect: wgpu::TextureAspect::All,
352 },
353 &zero,
354 wgpu::TexelCopyBufferLayout {
355 offset: 0,
356 bytes_per_row: Some(padding * 4),
357 rows_per_image: Some(raster.height),
358 },
359 wgpu::Extent3d {
360 width: padding,
361 height: raster.height,
362 depth_or_array_layers: 1,
363 },
364 );
365 }
366
367 if pad_bottom_y + padding <= atlas_size && raster.width > 0 {
369 let zero = vec![0u8; (raster.width * padding * 4) as usize];
370 self.queue.write_texture(
371 wgpu::TexelCopyTextureInfo {
372 texture: &self.atlas.atlas_texture,
373 mip_level: 0,
374 origin: wgpu::Origin3d {
375 x: info.x,
376 y: pad_bottom_y,
377 z: 0,
378 },
379 aspect: wgpu::TextureAspect::All,
380 },
381 &zero,
382 wgpu::TexelCopyBufferLayout {
383 offset: 0,
384 bytes_per_row: Some(raster.width * 4),
385 rows_per_image: Some(padding),
386 },
387 wgpu::Extent3d {
388 width: raster.width,
389 height: padding,
390 depth_or_array_layers: 1,
391 },
392 );
393 }
394
395 self.atlas.atlas_next_x += raster.width + padding;
396 self.atlas.atlas_row_height = self.atlas.atlas_row_height.max(raster.height);
397
398 info
399 }
400
401 pub(crate) fn get_or_rasterize_glyph(
410 &mut self,
411 font_idx: usize,
412 glyph_id: u16,
413 force_monochrome: bool,
414 cache_key: u64,
415 ) -> Option<GlyphInfo> {
416 if self.atlas.glyph_cache.contains_key(&cache_key) {
417 self.lru_remove(cache_key);
418 self.lru_push_front(cache_key);
419 return Some(
420 self.atlas
421 .glyph_cache
422 .get(&cache_key)
423 .expect("Glyph cache entry must exist after contains_key check")
424 .clone(),
425 );
426 }
427 let raster = self.rasterize_glyph(font_idx, glyph_id, force_monochrome)?;
428 let info = self.upload_glyph(cache_key, &raster);
429 self.atlas.glyph_cache.insert(cache_key, info.clone());
430 self.lru_push_front(cache_key);
431 Some(info)
432 }
433
434 pub(crate) fn resolve_glyph_with_fallback(
456 &mut self,
457 base_char: char,
458 grapheme: &str,
459 bold: bool,
460 italic: bool,
461 force_monochrome: bool,
462 ) -> Option<GlyphInfo> {
463 let chars: Vec<char> = grapheme.chars().collect();
466 let mut glyph_result = if force_monochrome || chars.len() == 1 {
467 self.font_manager.find_glyph(base_char, bold, italic)
468 } else {
469 self.font_manager
470 .find_grapheme_glyph(grapheme, bold, italic)
471 };
472
473 let mut excluded_fonts: Vec<usize> = Vec::new();
477 let resolved = loop {
478 match glyph_result {
479 Some((font_idx, glyph_id)) => {
480 let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
481 if let Some(info) =
482 self.get_or_rasterize_glyph(font_idx, glyph_id, force_monochrome, cache_key)
483 {
484 break Some(info);
485 }
486 excluded_fonts.push(font_idx);
488 glyph_result = self.font_manager.find_glyph_excluding(
489 base_char,
490 bold,
491 italic,
492 &excluded_fonts,
493 );
494 }
495 None => break None,
496 }
497 };
498
499 if resolved.is_none() && force_monochrome {
505 let mut glyph_result2 = self.font_manager.find_glyph(base_char, bold, italic);
506 loop {
507 match glyph_result2 {
508 Some((font_idx, glyph_id)) => {
509 let cache_key =
510 ((font_idx as u64) << 32) | (glyph_id as u64) | (1u64 << 63);
511 if let Some(info) =
512 self.get_or_rasterize_glyph(font_idx, glyph_id, false, cache_key)
513 {
514 break Some(info);
515 }
516 glyph_result2 = self.font_manager.find_glyph_excluding(
517 base_char,
518 bold,
519 italic,
520 &[font_idx],
521 );
522 }
523 None => break None,
524 }
525 }
526 } else {
527 resolved
528 }
529 }
530}
531
532fn convert_subpixel_mask_to_rgba(image: &swash::scale::image::Image) -> Vec<u8> {
537 let width = image.placement.width as usize;
538 let height = image.placement.height as usize;
539 let mut pixels = Vec::with_capacity(width * height * 4);
540
541 let stride = if width > 0 && height > 0 {
542 image.data.len() / (width * height)
543 } else {
544 0
545 };
546
547 match stride {
548 3 => {
549 for chunk in image.data.chunks_exact(3) {
550 let r = chunk[0];
551 let g = chunk[1];
552 let b = chunk[2];
553 let alpha = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
554 pixels.extend_from_slice(&[255, 255, 255, alpha]);
555 }
556 }
557 4 => {
558 for chunk in image.data.chunks_exact(4) {
559 let r = chunk[0];
560 let g = chunk[1];
561 let b = chunk[2];
562 let alpha = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
564 pixels.extend_from_slice(&[255, 255, 255, alpha]);
565 }
566 }
567 _ => {
568 pixels.resize(width * height * 4, 255);
570 }
571 }
572
573 pixels
574}
575
576fn convert_color_to_alpha_mask(image: &swash::scale::image::Image) -> Vec<u8> {
587 let width = image.placement.width as usize;
588 let height = image.placement.height as usize;
589 let mut pixels = Vec::with_capacity(width * height * 4);
590
591 for chunk in image.data.chunks_exact(4) {
594 let a = chunk[3];
595 pixels.extend_from_slice(&[255, 255, 255, a]);
596 }
597
598 pixels
599}
600
601#[cfg(test)]
602mod tests {
603 use super::convert_subpixel_mask_to_rgba;
604 use swash::scale::{Render, ScaleContext, Source};
605 use swash::zeno::Format;
606
607 #[test]
608 fn subpixel_mask_uses_rgba_stride() {
609 let data = std::fs::read("../par-term-fonts/fonts/DejaVuSansMono.ttf").expect("font file");
610 let font = swash::FontRef::from_index(&data, 0).expect("font ref");
611 let mut context = ScaleContext::new();
612 let glyph_id = font.charmap().map('a');
613 let mut scaler = context.builder(font).size(18.0).hint(true).build();
614
615 let image = Render::new(&[
616 Source::ColorOutline(0),
617 Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
618 Source::Outline,
619 Source::Bitmap(swash::scale::StrikeWith::BestFit),
620 ])
621 .format(Format::Subpixel)
622 .render(&mut scaler, glyph_id)
623 .expect("render");
624
625 let converted = convert_subpixel_mask_to_rgba(&image);
626
627 let width = image.placement.width as usize;
628 let height = image.placement.height as usize;
629 let mut expected = Vec::with_capacity(width * height * 4);
630 let stride = if width > 0 && height > 0 {
631 image.data.len() / (width * height)
632 } else {
633 0
634 };
635
636 match stride {
637 3 => {
638 for chunk in image.data.chunks_exact(3) {
639 let r = chunk[0];
640 let g = chunk[1];
641 let b = chunk[2];
642 let alpha = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
643 expected.extend_from_slice(&[255, 255, 255, alpha]);
644 }
645 }
646 4 => {
647 for chunk in image.data.chunks_exact(4) {
648 let r = chunk[0];
649 let g = chunk[1];
650 let b = chunk[2];
651 let alpha = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
652 expected.extend_from_slice(&[255, 255, 255, alpha]);
653 }
654 }
655 _ => expected.resize(width * height * 4, 255),
656 }
657
658 assert_eq!(converted, expected);
659 }
660
661 use super::should_render_as_symbol;
662
663 #[test]
664 fn test_dingbats_are_symbols() {
665 assert!(
667 should_render_as_symbol('\u{2733}'),
668 "✳ EIGHT SPOKED ASTERISK"
669 );
670 assert!(
671 should_render_as_symbol('\u{2734}'),
672 "✴ EIGHT POINTED BLACK STAR"
673 );
674 assert!(should_render_as_symbol('\u{2747}'), "❇ SPARKLE");
675 assert!(should_render_as_symbol('\u{2744}'), "❄ SNOWFLAKE");
676 assert!(should_render_as_symbol('\u{2702}'), "✂ SCISSORS");
677 assert!(should_render_as_symbol('\u{2714}'), "✔ HEAVY CHECK MARK");
678 assert!(
679 should_render_as_symbol('\u{2716}'),
680 "✖ HEAVY MULTIPLICATION X"
681 );
682 assert!(should_render_as_symbol('\u{2728}'), "✨ SPARKLES");
683 }
684
685 #[test]
686 fn test_misc_symbols_are_symbols() {
687 assert!(should_render_as_symbol('\u{2600}'), "☀ SUN");
689 assert!(should_render_as_symbol('\u{2601}'), "☁ CLOUD");
690 assert!(should_render_as_symbol('\u{263A}'), "☺ SMILING FACE");
691 assert!(should_render_as_symbol('\u{2665}'), "♥ BLACK HEART SUIT");
692 assert!(should_render_as_symbol('\u{2660}'), "♠ BLACK SPADE SUIT");
693 }
694
695 #[test]
696 fn test_misc_symbols_arrows_are_symbols() {
697 assert!(should_render_as_symbol('\u{2B50}'), "⭐ WHITE MEDIUM STAR");
699 assert!(should_render_as_symbol('\u{2B55}'), "⭕ HEAVY LARGE CIRCLE");
700 }
701
702 #[test]
703 fn test_regular_emoji_not_symbols() {
704 assert!(
707 !should_render_as_symbol('\u{1F600}'),
708 "😀 GRINNING FACE should not be a symbol"
709 );
710 assert!(
711 !should_render_as_symbol('\u{1F389}'),
712 "🎉 PARTY POPPER should not be a symbol"
713 );
714 assert!(
715 !should_render_as_symbol('\u{1F44D}'),
716 "👍 THUMBS UP should not be a symbol"
717 );
718 }
719
720 #[test]
721 fn test_regular_chars_not_symbols() {
722 assert!(
724 !should_render_as_symbol('A'),
725 "Letter A should not be a symbol"
726 );
727 assert!(
728 !should_render_as_symbol('*'),
729 "Asterisk should not be a symbol (it's ASCII)"
730 );
731 assert!(
732 !should_render_as_symbol('1'),
733 "Digit 1 should not be a symbol"
734 );
735 }
736}