1use std::collections::{HashMap, HashSet};
4
5use crate::fallback_data::{
6 ENCODED_NOTO_FONT_SET_RANGES, ENCODED_NOTO_FONT_SETS, NOTO_FONTS, NotoFont,
7};
8
9#[allow(dead_code)]
11const FONT_FALLBACK_BASE_URL: &str = "https://fonts.gstatic.com/s/";
12const PREFIX_DIGIT_0: u32 = 48;
13const PREFIX_RADIX: u32 = 10;
14const FONT_INDEX_DIGIT_0: u32 = 97; const FONT_INDEX_RADIX: u32 = 26;
16const RANGE_SIZE_DIGIT_0: u32 = 97; const RANGE_SIZE_RADIX: u32 = 26;
18const RANGE_VALUE_DIGIT_0: u32 = 65; const RANGE_VALUE_RADIX: u32 = 26;
20const MAX_CODE_POINT: u32 = 0x10FFFF;
21
22pub struct IndexedNotoFont {
23 pub index: usize,
24 pub font: &'static NotoFont,
25 pub cover_count: usize,
26 pub cover_components: Vec<usize>, }
28
29pub struct FallbackFontComponent {
30 pub fonts: Vec<usize>, pub cover_count: usize,
32}
33
34pub struct UnicodePropertyLookup {
35 boundaries: Vec<u32>,
36 values: Vec<FallbackFontComponent>,
40}
41
42impl UnicodePropertyLookup {
43 pub fn lookup(&self, value: u32) -> &FallbackFontComponent {
44 let mut start: usize = 0;
47 let mut end: usize = self.boundaries.len();
48 loop {
49 if start == end {
50 return &self.values[start];
51 }
52 let mid = start + (end - start) / 2;
53 if value >= self.boundaries[mid] {
54 start = mid + 1;
55 } else {
56 end = mid;
57 }
58 }
59 }
60
61 pub fn create() -> Self {
62 let property_enum_values = decode_font_components();
64
65 let packed_data = ENCODED_NOTO_FONT_SET_RANGES;
67
68 let mut boundaries: Vec<u32> = Vec::new();
69 let mut values: Vec<FallbackFontComponent> = Vec::new();
70
71 let mut start: u32 = 0;
72 let mut prefix: u32 = 0;
73 let mut size: u32 = 1;
74
75 for ch in packed_data.chars() {
76 let code = ch as u32;
77 if (RANGE_VALUE_DIGIT_0..RANGE_VALUE_DIGIT_0 + RANGE_VALUE_RADIX).contains(&code) {
78 let idx = (prefix * RANGE_VALUE_RADIX + (code - RANGE_VALUE_DIGIT_0)) as usize;
79 let template = &property_enum_values[idx];
81 let comp = FallbackFontComponent {
83 fonts: template.fonts.clone(),
84 cover_count: 0,
85 };
86 start += size;
87 boundaries.push(start);
88 values.push(comp);
89 prefix = 0;
90 size = 1;
91 } else if (RANGE_SIZE_DIGIT_0..RANGE_SIZE_DIGIT_0 + RANGE_SIZE_RADIX).contains(&code) {
92 size = prefix * RANGE_SIZE_RADIX + (code - RANGE_SIZE_DIGIT_0) + 2;
93 prefix = 0;
94 } else if (PREFIX_DIGIT_0..PREFIX_DIGIT_0 + PREFIX_RADIX).contains(&code) {
95 prefix = prefix * PREFIX_RADIX + (code - PREFIX_DIGIT_0);
96 } else {
97 panic!("Unexpected encoded range character: {}", ch);
98 }
99 }
100
101 assert_eq!(
102 start,
103 MAX_CODE_POINT + 1,
104 "Bad fallback map size: {}",
105 start
106 );
107
108 Self { boundaries, values }
109 }
110}
111
112fn decode_font_components() -> Vec<FallbackFontComponent> {
113 ENCODED_NOTO_FONT_SETS
114 .split(',')
115 .map(|component_data| {
116 let fonts = decode_font_set(component_data);
117 FallbackFontComponent {
118 fonts,
119 cover_count: 0,
120 }
121 })
122 .collect()
123}
124
125fn decode_font_set(data: &str) -> Vec<usize> {
126 let mut result: Vec<usize> = Vec::new();
127 let mut previous_index: i32 = -1;
128 let mut prefix: u32 = 0;
129 for ch in data.chars() {
130 let code = ch as u32;
131 if (FONT_INDEX_DIGIT_0..FONT_INDEX_DIGIT_0 + FONT_INDEX_RADIX).contains(&code) {
132 let delta = (prefix * FONT_INDEX_RADIX + (code - FONT_INDEX_DIGIT_0)) as i32;
133 let index = previous_index + delta + 1;
134 result.push(index as usize);
135 previous_index = index;
136 prefix = 0;
137 } else if (PREFIX_DIGIT_0..PREFIX_DIGIT_0 + PREFIX_RADIX).contains(&code) {
138 prefix = prefix * PREFIX_RADIX + (code - PREFIX_DIGIT_0);
139 } else {
140 panic!("Unexpected encoded font-set char: {}", ch);
141 }
142 }
143 result
144}
145
146pub struct NotoFontDownloader {
148 code_points_with_no_known_font: HashSet<u32>,
149 lookup: UnicodePropertyLookup,
150 }
153
154impl NotoFontDownloader {
155 pub fn new() -> Self {
156 Self {
157 code_points_with_no_known_font: HashSet::new(),
158 lookup: UnicodePropertyLookup::create(),
159 }
160 }
161
162 pub fn get_codepoints_with_no_known_font(&self) -> &HashSet<u32> {
163 &self.code_points_with_no_known_font
164 }
165
166 pub fn get_fonts_to_download(
169 &mut self,
170 codepoints: &HashSet<u32>,
171 language: &str,
172 ) -> Vec<&'static NotoFont> {
173 if codepoints.is_empty() {
174 return Vec::new();
175 }
176
177 let mut components: Vec<FallbackFontComponent> = self
188 .lookup
189 .values
190 .iter()
191 .map(|c| FallbackFontComponent {
192 fonts: c.fonts.clone(),
193 cover_count: 0,
194 })
195 .collect();
196
197 let mut font_index_to_obj: HashMap<usize, IndexedNotoFont> = HashMap::new();
201 fn lookup_idx(boundaries: &[u32], value: u32) -> usize {
213 let mut start = 0usize;
214 let mut end = boundaries.len();
215 loop {
216 if start == end {
217 return start;
218 }
219 let mid = start + (end - start) / 2;
220 if value >= boundaries[mid] {
221 start = mid + 1;
222 } else {
223 end = mid;
224 }
225 }
226 }
227
228 let mut missing: Vec<u32> = Vec::new();
229 let mut required_component_indices: Vec<usize> = Vec::new();
230
231 for &cp in codepoints {
232 if self.code_points_with_no_known_font.contains(&cp) || cp > MAX_CODE_POINT {
233 continue;
234 }
235 let idx = lookup_idx(&self.lookup.boundaries, cp);
236 let comp = &mut components[idx];
237 if comp.fonts.is_empty() {
238 missing.push(cp);
239 } else {
240 if comp.cover_count == 0 {
241 required_component_indices.push(idx);
242 }
243 comp.cover_count += 1;
244 }
245 }
246
247 if !missing.is_empty() {
248 self.code_points_with_no_known_font.extend(missing);
249 }
250
251 if required_component_indices.is_empty() {
252 return Vec::new();
253 }
254
255 for &comp_idx in &required_component_indices {
258 for &font_idx in &components[comp_idx].fonts.clone() {
259 font_index_to_obj
260 .entry(font_idx)
261 .or_insert_with(|| IndexedNotoFont {
262 index: font_idx,
263 font: &NOTO_FONTS[font_idx],
264 cover_count: 0,
265 cover_components: Vec::new(),
266 });
267 }
268 }
269
270 let mut candidate_font_indices: HashSet<usize> = HashSet::new();
272
273 for &comp_idx in &required_component_indices {
274 let comp_cover = components[comp_idx].cover_count;
275 let fonts_clone = components[comp_idx].fonts.clone();
276 for font_idx in fonts_clone {
277 let font_obj = font_index_to_obj.get_mut(&font_idx).unwrap();
278 if font_obj.cover_count == 0 {
279 candidate_font_indices.insert(font_idx);
280 }
281 font_obj.cover_count += comp_cover;
282 font_obj.cover_components.push(comp_idx);
283 }
284 }
285
286 let mut candidate_vec: Vec<usize> = candidate_font_indices.into_iter().collect();
288
289 let mut selected: Vec<&'static NotoFont> = Vec::new();
290
291 while !candidate_vec.is_empty() {
293 let best_idx = select_font(&candidate_vec, &font_index_to_obj, language);
295 let best_font = font_index_to_obj.get(&best_idx).unwrap();
296 selected.push(best_font.font);
297
298 let covered_components: Vec<usize> = best_font.cover_components.clone();
300 for comp_idx in covered_components {
301 let comp_cover = components[comp_idx].cover_count;
302 let fonts_in_comp = components[comp_idx].fonts.clone();
303 for f_idx in fonts_in_comp {
304 if let Some(fobj) = font_index_to_obj.get_mut(&f_idx) {
305 fobj.cover_count = fobj.cover_count.saturating_sub(comp_cover);
306 fobj.cover_components.retain(|&c| c != comp_idx);
308 }
309 }
310 components[comp_idx].cover_count = 0;
311 }
312
313 candidate_vec.retain(|fid| {
315 font_index_to_obj
316 .get(fid)
317 .map(|f| f.cover_count != 0)
318 .unwrap_or(false)
319 });
320 }
321
322 selected
324 }
325}
326
327fn is_cjk_font(font: &NotoFont) -> bool {
328 is_noto_sans_sc(font)
329 || is_noto_sans_tc(font)
330 || is_noto_sans_hk(font)
331 || is_noto_sans_jp(font)
332 || is_noto_sans_kr(font)
333}
334fn is_noto_sans_sc(f: &NotoFont) -> bool {
335 f.name.starts_with("Noto Sans SC")
336}
337fn is_noto_sans_tc(f: &NotoFont) -> bool {
338 f.name.starts_with("Noto Sans TC")
339}
340fn is_noto_sans_hk(f: &NotoFont) -> bool {
341 f.name.starts_with("Noto Sans HK")
342}
343fn is_noto_sans_jp(f: &NotoFont) -> bool {
344 f.name.starts_with("Noto Sans JP")
345}
346fn is_noto_sans_kr(f: &NotoFont) -> bool {
347 f.name.starts_with("Noto Sans KR")
348}
349fn is_noto_color_emoji(f: &NotoFont) -> bool {
350 f.name.starts_with("Noto Color Emoji")
351}
352fn is_noto_sans_symbols(f: &NotoFont) -> bool {
353 f.name.starts_with("Noto Sans Symbols")
354}
355
356fn select_font(
357 candidates: &[usize],
358 arena: &HashMap<usize, IndexedNotoFont>,
359 language: &str,
360) -> usize {
361 let mut max_covered = -1i32;
363 let mut best_fonts: Vec<usize> = Vec::new();
364 let mut best: Option<usize> = None;
365 for &fid in candidates {
366 let f = &arena[&fid];
367 let cc = f.cover_count as i32;
368 if cc > max_covered {
369 best_fonts.clear();
370 best_fonts.push(fid);
371 best = Some(fid);
372 max_covered = cc;
373 } else if cc == max_covered {
374 best_fonts.push(fid);
375 if best.map(|b| fid < b).unwrap_or(true) {
376 best = Some(fid);
377 }
378 }
379 }
380
381 if best_fonts.len() > 1 {
383 let all_cjk = best_fonts.iter().all(|&fid| is_cjk_font(arena[&fid].font));
385 if all_cjk {
386 if let Some(idx) = select_best_for_language(&best_fonts, arena, language) {
387 return idx;
388 }
389 if let Some(idx) = select_best_for_language(candidates, arena, language) {
390 return idx;
391 }
392 } else {
393 if let Some(&fid) = best_fonts
395 .iter()
396 .find(|&&fid| is_noto_color_emoji(arena[&fid].font))
397 {
398 return fid;
399 }
400 if let Some(&fid) = best_fonts
401 .iter()
402 .find(|&&fid| is_noto_sans_symbols(arena[&fid].font))
403 {
404 return fid;
405 }
406 if let Some(&fid) = best_fonts
407 .iter()
408 .find(|&&fid| is_noto_sans_sc(arena[&fid].font))
409 {
410 return fid;
411 }
412 }
413 }
414
415 best.expect("No fallback font selected")
416}
417
418fn select_best_for_language(
419 candidates: &[usize],
420 arena: &HashMap<usize, IndexedNotoFont>,
421 language: &str,
422) -> Option<usize> {
423 match language {
424 "zh-Hans" | "zh-CN" | "zh-SG" | "zh-MY" => candidates
425 .iter()
426 .find(|&&fid| is_noto_sans_sc(arena[&fid].font))
427 .copied(),
428 "zh-Hant" | "zh-TW" | "zh-MO" => candidates
429 .iter()
430 .find(|&&fid| is_noto_sans_tc(arena[&fid].font))
431 .copied(),
432 "zh-HK" => candidates
433 .iter()
434 .find(|&&fid| is_noto_sans_hk(arena[&fid].font))
435 .copied(),
436 "ja" => candidates
437 .iter()
438 .find(|&&fid| is_noto_sans_jp(arena[&fid].font))
439 .copied(),
440 "ko" => candidates
441 .iter()
442 .find(|&&fid| is_noto_sans_kr(arena[&fid].font))
443 .copied(),
444 _ => None,
445 }
446}
447
448#[cfg(target_arch = "wasm32")]
451pub mod wasm_fallback {
452 use super::*;
453 use std::cell::RefCell;
454 use std::collections::HashSet;
455 use std::rc::Rc;
456 use wasm_bindgen::JsCast;
457 use wasm_bindgen::prelude::*;
458 use wasm_bindgen_futures::JsFuture;
459 use web_sys::{Request, Response};
460
461 const BATCH_WINDOW_MS: u32 = 60;
462 const MAX_BATCH_SIZE: usize = 10;
463
464 thread_local! {
465 static GLOBAL: Rc<RefCell<FallbackManager>> = Rc::new(RefCell::new(FallbackManager::new()));
466 }
467
468 struct FallbackManager {
469 downloader: NotoFontDownloader,
470 pending: HashSet<u32>,
471 queued: Vec<HashSet<u32>>,
473 is_running: bool,
474 error_count: u32,
475 }
476
477 impl FallbackManager {
478 fn new() -> Self {
479 Self {
480 downloader: NotoFontDownloader::new(),
481 pending: HashSet::new(),
482 queued: Vec::new(),
483 is_running: false,
484 error_count: 0,
485 }
486 }
487 }
488
489 pub fn submit_unresolved(codepoints: Vec<u32>) {
490 if codepoints.is_empty() {
491 return;
492 }
493 let set: HashSet<u32> = codepoints.into_iter().collect();
494 GLOBAL.with(|g| {
495 let mut mgr = g.borrow_mut();
496 mgr.queued.push(set);
497 if !mgr.is_running {
498 mgr.is_running = true;
499 let g_clone = g.clone();
500 wasm_bindgen_futures::spawn_local(async move {
501 run_loop(g_clone).await;
502 });
503 }
504 });
505 }
506
507 async fn run_loop(global: Rc<RefCell<FallbackManager>>) {
508 loop {
509 let batch = {
511 loop {
513 let has = global.borrow().queued.len() > 0;
514 if has {
515 break;
516 }
517 gloo_timers_approx_delay(16).await;
518 }
519 let mut batch_set = HashSet::new();
521 {
523 let mut mgr = global.borrow_mut();
524 if let Some(first) = mgr.queued.pop() {
525 batch_set.extend(first);
526 }
527 }
528 let mut collected = 1;
530 let start = js_sys::Date::now();
531 while collected < MAX_BATCH_SIZE {
532 let elapsed = js_sys::Date::now() - start;
533 if elapsed >= BATCH_WINDOW_MS as f64 {
534 break;
535 }
536 let maybe = { global.borrow_mut().queued.pop() };
537 if let Some(s) = maybe {
538 batch_set.extend(s);
539 collected += 1;
540 } else {
541 gloo_timers_approx_delay(10).await;
543 if global.borrow().queued.is_empty() {
544 if js_sys::Date::now() - start >= BATCH_WINDOW_MS as f64 {
546 break;
547 }
548 }
549 }
550 }
551 batch_set
552 };
553
554 if batch.is_empty() {
555 continue;
556 }
557
558 let fonts_to_download: Vec<&'static NotoFont> = {
560 let mut mgr = global.borrow_mut();
561 let lang = web_sys::window()
563 .and_then(|w| w.navigator().language())
564 .unwrap_or_else(|| "en".to_string());
565 let res = mgr.downloader.get_fonts_to_download(&batch, &lang);
566 res
567 };
568
569 if fonts_to_download.is_empty() {
570 continue;
572 }
573
574 let mut successes: Vec<Vec<u8>> = Vec::new();
575 let mut any_success = false;
576 let mut all_failed = true;
577
578 for font in &fonts_to_download {
579 let url = format!("{}{}", FONT_FALLBACK_BASE_URL, font.url);
580 match fetch_bytes(&url).await {
581 Ok(bytes) => {
582 successes.push(bytes);
583 any_success = true;
584 all_failed = false;
585 }
586 Err(e) => {
587 log::warn!("Failed to download fallback font [{}]: {:?}", url, e);
588 }
589 }
590 }
591
592 if all_failed && any_success == false && !fonts_to_download.is_empty() {
593 let backoff = {
595 let mut mgr = global.borrow_mut();
596 let pause = mgr.error_count * 5;
597 mgr.error_count += 1;
598 pause
599 };
600 log::warn!("Fallback download failed, retry in {}s", backoff);
601 gloo_timers_approx_delay(backoff * 1000).await;
603 global.borrow_mut().queued.push(batch);
604 continue;
605 }
606
607 {
609 global.borrow_mut().error_count = 0;
610 }
611
612 for bytes in successes {
613 crate::register_font_data(&bytes);
614 }
615
616 if any_success {
617 crate::clear_caches_for_fallback();
620 crate::bump_frame_for_fallback();
624 }
628 }
629 }
630
631 async fn gloo_timers_approx_delay(ms: u32) -> () {
632 let promise = js_sys::Promise::new(&mut |resolve, _reject| {
634 let window = web_sys::window().unwrap();
635 let _ =
636 window.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms as i32);
637 });
638 let _ = JsFuture::from(promise).await;
639 }
640
641 async fn fetch_bytes(url: &str) -> Result<Vec<u8>, JsValue> {
642 let request = Request::new_with_str(url)?;
643 let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
644 let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
645 let resp: Response = resp_value.dyn_into().unwrap();
646 if !resp.ok() {
647 return Err(JsValue::from_str(&format!(
648 "fetch failed status {}",
649 resp.status()
650 )));
651 }
652 let buffer = JsFuture::from(resp.array_buffer()?).await?;
653 let arr = js_sys::Uint8Array::new(&buffer);
654 Ok(arr.to_vec())
655 }
656
657 pub fn ensure_fallback_initialized() {
659 GLOBAL.with(|_| {});
661 }
662}
663
664#[cfg(not(target_arch = "wasm32"))]
665pub mod wasm_fallback {
666 pub fn submit_unresolved(_codepoints: Vec<u32>) {}
667 pub fn ensure_fallback_initialized() {}
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673 use std::collections::HashSet;
674
675 #[test]
676 fn empty_returns_empty() {
677 let mut d = NotoFontDownloader::new();
678 let res = d.get_fonts_to_download(&HashSet::new(), "en");
679 assert!(res.is_empty());
680 assert!(d.get_codepoints_with_no_known_font().is_empty());
681 }
682
683 #[test]
684 fn above_max_ignored() {
685 let mut d = NotoFontDownloader::new();
686 let mut set = HashSet::new();
687 set.insert(0x110000);
688 let res = d.get_fonts_to_download(&set, "en");
689 assert!(res.is_empty());
690 assert!(d.get_codepoints_with_no_known_font().is_empty());
691 }
692
693 #[test]
694 fn pua_remembered() {
695 let mut d = NotoFontDownloader::new();
696 let mut set = HashSet::new();
697 set.insert(0xE000);
698 let first = d.get_fonts_to_download(&set, "en");
699 assert!(first.is_empty());
700 assert!(d.get_codepoints_with_no_known_font().contains(&0xE000));
701 let second = d.get_fonts_to_download(&set, "en");
702 assert!(second.is_empty());
703 }
704
705 #[test]
706 fn arabic_resolves() {
707 let mut d = NotoFontDownloader::new();
708 let set: HashSet<u32> = [0x0639, 0x0641].into_iter().collect();
709 let fonts = d.get_fonts_to_download(&set, "en");
710 assert!(!fonts.is_empty(), "Arabic should resolve");
711 assert!(
712 fonts.iter().all(|f| f.name.starts_with("Noto Sans Arabic")),
713 "got {:?}",
714 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
715 );
716 }
717
718 #[test]
719 fn emoji_resolves() {
720 let mut d = NotoFontDownloader::new();
721 let set: HashSet<u32> = [0x1F600, 0x1F389].into_iter().collect();
722 let fonts = d.get_fonts_to_download(&set, "en");
723 assert!(!fonts.is_empty());
724 assert!(
725 fonts.iter().all(|f| f.name.starts_with("Noto Color Emoji")),
726 "got {:?}",
727 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
728 );
729 }
730
731 #[test]
732 fn cjk_zh_cn() {
733 let mut d = NotoFontDownloader::new();
734 let set: HashSet<u32> = [0x5B57].into_iter().collect();
735 let fonts = d.get_fonts_to_download(&set, "zh-CN");
736 assert!(!fonts.is_empty());
737 assert!(
738 fonts.iter().all(|f| f.name.starts_with("Noto Sans SC")),
739 "got {:?}",
740 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
741 );
742 }
743
744 #[test]
745 fn japanese_hiragana() {
746 let mut d = NotoFontDownloader::new();
747 let set: HashSet<u32> = [0x3042].into_iter().collect();
748 let fonts = d.get_fonts_to_download(&set, "ja");
749 assert!(!fonts.is_empty());
750 assert!(
751 fonts.iter().all(|f| f.name.starts_with("Noto Sans JP")),
752 "got {:?}",
753 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
754 );
755 }
756
757 #[test]
758 fn devanagari() {
759 let mut d = NotoFontDownloader::new();
760 let set: HashSet<u32> = [0x0905, 0x0915].into_iter().collect();
761 let fonts = d.get_fonts_to_download(&set, "en");
762 assert!(!fonts.is_empty(), "Devanagari should resolve to Noto Sans*");
763 assert!(
765 fonts.iter().all(|f| f.name.starts_with("Noto Sans")),
766 "got {:?}",
767 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
768 );
769 }
770
771 #[test]
772 fn hebrew_resolves() {
773 let mut d = NotoFontDownloader::new();
774 let set: HashSet<u32> = [0x05E9, 0x05D0].into_iter().collect();
775 let fonts = d.get_fonts_to_download(&set, "en");
776 assert!(!fonts.is_empty());
777 assert!(
778 fonts.iter().all(|f| f.name.starts_with("Noto Sans Hebrew")),
779 "got {:?}",
780 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
781 );
782 }
783
784 #[test]
785 fn thai_resolves() {
786 let mut d = NotoFontDownloader::new();
787 let set: HashSet<u32> = [0x0E01, 0x0E2A].into_iter().collect();
788 let fonts = d.get_fonts_to_download(&set, "en");
789 assert!(!fonts.is_empty());
790 assert!(
791 fonts.iter().all(|f| f.name.starts_with("Noto Sans Thai")),
792 "got {:?}",
793 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
794 );
795 }
796
797 #[test]
798 fn bengali_resolves() {
799 let mut d = NotoFontDownloader::new();
800 let set: HashSet<u32> = [0x0985, 0x0995].into_iter().collect();
801 let fonts = d.get_fonts_to_download(&set, "en");
802 assert!(!fonts.is_empty());
803 assert!(
804 fonts
805 .iter()
806 .all(|f| f.name.starts_with("Noto Sans Bengali")),
807 "got {:?}",
808 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
809 );
810 }
811
812 #[test]
813 fn box_drawing_resolves() {
814 let mut d = NotoFontDownloader::new();
815 let set: HashSet<u32> = [0x2500, 0x2502].into_iter().collect();
816 let fonts = d.get_fonts_to_download(&set, "en");
817 assert!(!fonts.is_empty());
818 assert!(
820 fonts.iter().all(|f| f.name.starts_with("Noto Sans")),
821 "got {:?}",
822 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
823 );
824 }
825
826 #[test]
827 fn korean_ko() {
828 let mut d = NotoFontDownloader::new();
829 let set: HashSet<u32> = [0xACA8, 0xACAF, 0xACF0].into_iter().collect();
830 let fonts = d.get_fonts_to_download(&set, "ko");
831 assert!(!fonts.is_empty());
832 assert!(
833 fonts.iter().all(|f| f.name.starts_with("Noto Sans KR")),
834 "got {:?}",
835 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
836 );
837 }
838
839 #[test]
840 fn cjk_zh_hant() {
841 let mut d = NotoFontDownloader::new();
842 let set: HashSet<u32> = [0x5B57].into_iter().collect();
843 let fonts = d.get_fonts_to_download(&set, "zh-TW");
844 assert!(!fonts.is_empty());
845 assert!(
846 fonts.iter().all(|f| f.name.starts_with("Noto Sans TC")),
847 "got {:?}",
848 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
849 );
850 }
851}