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 image = Render::new(&color_sources)
227 .format(render_format)
228 .render(&mut retry_scaler, glyph_id)?;
229 }
230
231 let (pixels, is_colored) = match image.content {
232 Content::Color => {
233 if force_monochrome {
234 let pixels = convert_color_to_alpha_mask(&image);
239 (pixels, false)
240 } else {
241 (image.data.clone(), true)
242 }
243 }
244 Content::Mask => {
245 let mut pixels = Vec::with_capacity(image.data.len() * 4);
246 for &mask in &image.data {
247 let alpha = if !self.font.font_antialias {
249 if mask > 127 { 255 } else { 0 }
250 } else {
251 mask
252 };
253 pixels.extend_from_slice(&[255, 255, 255, alpha]);
254 }
255 (pixels, false)
256 }
257 Content::SubpixelMask => {
258 let pixels = convert_subpixel_mask_to_rgba(&image);
259 (pixels, false)
260 }
261 };
262
263 if !is_colored && pixels.iter().skip(3).step_by(4).all(|&a| a == 0) {
266 return None;
267 }
268
269 Some(RasterizedGlyph {
270 width: image.placement.width,
271 height: image.placement.height,
272 bearing_x: image.placement.left as f32,
273 bearing_y: image.placement.top as f32,
274 pixels,
275 is_colored,
276 })
277 }
278
279 pub(crate) fn upload_glyph(&mut self, _key: u64, raster: &RasterizedGlyph) -> GlyphInfo {
280 let padding = super::ATLAS_GLYPH_PADDING;
281 let atlas_size = self.atlas.atlas_size;
282 if self.atlas.atlas_next_x + raster.width + padding > atlas_size {
283 self.atlas.atlas_next_x = 0;
284 self.atlas.atlas_next_y += self.atlas.atlas_row_height + padding;
285 self.atlas.atlas_row_height = 0;
286 }
287
288 if self.atlas.atlas_next_y + raster.height + padding > atlas_size {
289 self.clear_glyph_cache();
290 }
291
292 let info = GlyphInfo {
293 key: _key,
294 x: self.atlas.atlas_next_x,
295 y: self.atlas.atlas_next_y,
296 width: raster.width,
297 height: raster.height,
298 bearing_x: raster.bearing_x,
299 bearing_y: raster.bearing_y,
300 is_colored: raster.is_colored,
301 prev: None,
302 next: None,
303 };
304
305 self.queue.write_texture(
306 wgpu::TexelCopyTextureInfo {
307 texture: &self.atlas.atlas_texture,
308 mip_level: 0,
309 origin: wgpu::Origin3d {
310 x: info.x,
311 y: info.y,
312 z: 0,
313 },
314 aspect: wgpu::TextureAspect::All,
315 },
316 &raster.pixels,
317 wgpu::TexelCopyBufferLayout {
318 offset: 0,
319 bytes_per_row: Some(4 * raster.width),
320 rows_per_image: Some(raster.height),
321 },
322 wgpu::Extent3d {
323 width: raster.width,
324 height: raster.height,
325 depth_or_array_layers: 1,
326 },
327 );
328
329 let pad_right_x = info.x + raster.width;
332 let pad_bottom_y = info.y + raster.height;
333
334 if pad_right_x + padding <= atlas_size && raster.height > 0 {
336 let zero = vec![0u8; (padding * raster.height * 4) as usize];
337 self.queue.write_texture(
338 wgpu::TexelCopyTextureInfo {
339 texture: &self.atlas.atlas_texture,
340 mip_level: 0,
341 origin: wgpu::Origin3d {
342 x: pad_right_x,
343 y: info.y,
344 z: 0,
345 },
346 aspect: wgpu::TextureAspect::All,
347 },
348 &zero,
349 wgpu::TexelCopyBufferLayout {
350 offset: 0,
351 bytes_per_row: Some(padding * 4),
352 rows_per_image: Some(raster.height),
353 },
354 wgpu::Extent3d {
355 width: padding,
356 height: raster.height,
357 depth_or_array_layers: 1,
358 },
359 );
360 }
361
362 if pad_bottom_y + padding <= atlas_size && raster.width > 0 {
364 let zero = vec![0u8; (raster.width * padding * 4) as usize];
365 self.queue.write_texture(
366 wgpu::TexelCopyTextureInfo {
367 texture: &self.atlas.atlas_texture,
368 mip_level: 0,
369 origin: wgpu::Origin3d {
370 x: info.x,
371 y: pad_bottom_y,
372 z: 0,
373 },
374 aspect: wgpu::TextureAspect::All,
375 },
376 &zero,
377 wgpu::TexelCopyBufferLayout {
378 offset: 0,
379 bytes_per_row: Some(raster.width * 4),
380 rows_per_image: Some(padding),
381 },
382 wgpu::Extent3d {
383 width: raster.width,
384 height: padding,
385 depth_or_array_layers: 1,
386 },
387 );
388 }
389
390 self.atlas.atlas_next_x += raster.width + padding;
391 self.atlas.atlas_row_height = self.atlas.atlas_row_height.max(raster.height);
392
393 info
394 }
395
396 pub(crate) fn get_or_rasterize_glyph(
405 &mut self,
406 font_idx: usize,
407 glyph_id: u16,
408 force_monochrome: bool,
409 cache_key: u64,
410 ) -> Option<GlyphInfo> {
411 if self.atlas.glyph_cache.contains_key(&cache_key) {
412 self.lru_remove(cache_key);
413 self.lru_push_front(cache_key);
414 return Some(
415 self.atlas
416 .glyph_cache
417 .get(&cache_key)
418 .expect("Glyph cache entry must exist after contains_key check")
419 .clone(),
420 );
421 }
422 let raster = self.rasterize_glyph(font_idx, glyph_id, force_monochrome)?;
423 let info = self.upload_glyph(cache_key, &raster);
424 self.atlas.glyph_cache.insert(cache_key, info.clone());
425 self.lru_push_front(cache_key);
426 Some(info)
427 }
428
429 pub(crate) fn resolve_glyph_with_fallback(
451 &mut self,
452 base_char: char,
453 grapheme: &str,
454 bold: bool,
455 italic: bool,
456 force_monochrome: bool,
457 ) -> Option<GlyphInfo> {
458 let chars: Vec<char> = grapheme.chars().collect();
461 let mut glyph_result = if force_monochrome || chars.len() == 1 {
462 self.font_manager.find_glyph(base_char, bold, italic)
463 } else {
464 self.font_manager
465 .find_grapheme_glyph(grapheme, bold, italic)
466 };
467
468 let mut excluded_fonts: Vec<usize> = Vec::new();
472 let resolved = loop {
473 match glyph_result {
474 Some((font_idx, glyph_id)) => {
475 let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
476 if let Some(info) =
477 self.get_or_rasterize_glyph(font_idx, glyph_id, force_monochrome, cache_key)
478 {
479 break Some(info);
480 }
481 excluded_fonts.push(font_idx);
483 glyph_result = self.font_manager.find_glyph_excluding(
484 base_char,
485 bold,
486 italic,
487 &excluded_fonts,
488 );
489 }
490 None => break None,
491 }
492 };
493
494 if resolved.is_none() && force_monochrome {
500 let mut glyph_result2 = self.font_manager.find_glyph(base_char, bold, italic);
501 loop {
502 match glyph_result2 {
503 Some((font_idx, glyph_id)) => {
504 let cache_key =
505 ((font_idx as u64) << 32) | (glyph_id as u64) | (1u64 << 63);
506 if let Some(info) =
507 self.get_or_rasterize_glyph(font_idx, glyph_id, false, cache_key)
508 {
509 break Some(info);
510 }
511 glyph_result2 = self.font_manager.find_glyph_excluding(
512 base_char,
513 bold,
514 italic,
515 &[font_idx],
516 );
517 }
518 None => break None,
519 }
520 }
521 } else {
522 resolved
523 }
524 }
525}
526
527fn convert_subpixel_mask_to_rgba(image: &swash::scale::image::Image) -> Vec<u8> {
532 let width = image.placement.width as usize;
533 let height = image.placement.height as usize;
534 let mut pixels = Vec::with_capacity(width * height * 4);
535
536 let stride = if width > 0 && height > 0 {
537 image.data.len() / (width * height)
538 } else {
539 0
540 };
541
542 match stride {
543 3 => {
544 for chunk in image.data.chunks_exact(3) {
545 let r = chunk[0];
546 let g = chunk[1];
547 let b = chunk[2];
548 let alpha = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
549 pixels.extend_from_slice(&[255, 255, 255, alpha]);
550 }
551 }
552 4 => {
553 for chunk in image.data.chunks_exact(4) {
554 let r = chunk[0];
555 let g = chunk[1];
556 let b = chunk[2];
557 let alpha = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
559 pixels.extend_from_slice(&[255, 255, 255, alpha]);
560 }
561 }
562 _ => {
563 pixels.resize(width * height * 4, 255);
565 }
566 }
567
568 pixels
569}
570
571fn convert_color_to_alpha_mask(image: &swash::scale::image::Image) -> Vec<u8> {
582 let width = image.placement.width as usize;
583 let height = image.placement.height as usize;
584 let mut pixels = Vec::with_capacity(width * height * 4);
585
586 for chunk in image.data.chunks_exact(4) {
589 let a = chunk[3];
590 pixels.extend_from_slice(&[255, 255, 255, a]);
591 }
592
593 pixels
594}
595
596#[cfg(test)]
597mod tests {
598 use super::convert_subpixel_mask_to_rgba;
599 use swash::scale::{Render, ScaleContext, Source};
600 use swash::zeno::Format;
601
602 #[test]
603 fn subpixel_mask_uses_rgba_stride() {
604 let data = std::fs::read("../par-term-fonts/fonts/DejaVuSansMono.ttf").expect("font file");
605 let font = swash::FontRef::from_index(&data, 0).expect("font ref");
606 let mut context = ScaleContext::new();
607 let glyph_id = font.charmap().map('a');
608 let mut scaler = context.builder(font).size(18.0).hint(true).build();
609
610 let image = Render::new(&[
611 Source::ColorOutline(0),
612 Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
613 Source::Outline,
614 Source::Bitmap(swash::scale::StrikeWith::BestFit),
615 ])
616 .format(Format::Subpixel)
617 .render(&mut scaler, glyph_id)
618 .expect("render");
619
620 let converted = convert_subpixel_mask_to_rgba(&image);
621
622 let width = image.placement.width as usize;
623 let height = image.placement.height as usize;
624 let mut expected = Vec::with_capacity(width * height * 4);
625 let stride = if width > 0 && height > 0 {
626 image.data.len() / (width * height)
627 } else {
628 0
629 };
630
631 match stride {
632 3 => {
633 for chunk in image.data.chunks_exact(3) {
634 let r = chunk[0];
635 let g = chunk[1];
636 let b = chunk[2];
637 let alpha = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
638 expected.extend_from_slice(&[255, 255, 255, alpha]);
639 }
640 }
641 4 => {
642 for chunk in image.data.chunks_exact(4) {
643 let r = chunk[0];
644 let g = chunk[1];
645 let b = chunk[2];
646 let alpha = ((r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000) as u8;
647 expected.extend_from_slice(&[255, 255, 255, alpha]);
648 }
649 }
650 _ => expected.resize(width * height * 4, 255),
651 }
652
653 assert_eq!(converted, expected);
654 }
655
656 use super::should_render_as_symbol;
657
658 #[test]
659 fn test_dingbats_are_symbols() {
660 assert!(
662 should_render_as_symbol('\u{2733}'),
663 "✳ EIGHT SPOKED ASTERISK"
664 );
665 assert!(
666 should_render_as_symbol('\u{2734}'),
667 "✴ EIGHT POINTED BLACK STAR"
668 );
669 assert!(should_render_as_symbol('\u{2747}'), "❇ SPARKLE");
670 assert!(should_render_as_symbol('\u{2744}'), "❄ SNOWFLAKE");
671 assert!(should_render_as_symbol('\u{2702}'), "✂ SCISSORS");
672 assert!(should_render_as_symbol('\u{2714}'), "✔ HEAVY CHECK MARK");
673 assert!(
674 should_render_as_symbol('\u{2716}'),
675 "✖ HEAVY MULTIPLICATION X"
676 );
677 assert!(should_render_as_symbol('\u{2728}'), "✨ SPARKLES");
678 }
679
680 #[test]
681 fn test_misc_symbols_are_symbols() {
682 assert!(should_render_as_symbol('\u{2600}'), "☀ SUN");
684 assert!(should_render_as_symbol('\u{2601}'), "☁ CLOUD");
685 assert!(should_render_as_symbol('\u{263A}'), "☺ SMILING FACE");
686 assert!(should_render_as_symbol('\u{2665}'), "♥ BLACK HEART SUIT");
687 assert!(should_render_as_symbol('\u{2660}'), "♠ BLACK SPADE SUIT");
688 }
689
690 #[test]
691 fn test_misc_symbols_arrows_are_symbols() {
692 assert!(should_render_as_symbol('\u{2B50}'), "⭐ WHITE MEDIUM STAR");
694 assert!(should_render_as_symbol('\u{2B55}'), "⭕ HEAVY LARGE CIRCLE");
695 }
696
697 #[test]
698 fn test_regular_emoji_not_symbols() {
699 assert!(
702 !should_render_as_symbol('\u{1F600}'),
703 "😀 GRINNING FACE should not be a symbol"
704 );
705 assert!(
706 !should_render_as_symbol('\u{1F389}'),
707 "🎉 PARTY POPPER should not be a symbol"
708 );
709 assert!(
710 !should_render_as_symbol('\u{1F44D}'),
711 "👍 THUMBS UP should not be a symbol"
712 );
713 }
714
715 #[test]
716 fn test_regular_chars_not_symbols() {
717 assert!(
719 !should_render_as_symbol('A'),
720 "Letter A should not be a symbol"
721 );
722 assert!(
723 !should_render_as_symbol('*'),
724 "Asterisk should not be a symbol (it's ASCII)"
725 );
726 assert!(
727 !should_render_as_symbol('1'),
728 "Digit 1 should not be a symbol"
729 );
730 }
731}