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 Default for NotoFontDownloader {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160impl NotoFontDownloader {
161 pub fn new() -> Self {
162 Self {
163 code_points_with_no_known_font: HashSet::new(),
164 lookup: UnicodePropertyLookup::create(),
165 }
166 }
167
168 pub fn get_codepoints_with_no_known_font(&self) -> &HashSet<u32> {
169 &self.code_points_with_no_known_font
170 }
171
172 pub fn get_fonts_to_download(
175 &mut self,
176 codepoints: &HashSet<u32>,
177 language: &str,
178 ) -> Vec<&'static NotoFont> {
179 if codepoints.is_empty() {
180 return Vec::new();
181 }
182
183 let mut components: Vec<FallbackFontComponent> = self
194 .lookup
195 .values
196 .iter()
197 .map(|c| FallbackFontComponent {
198 fonts: c.fonts.clone(),
199 cover_count: 0,
200 })
201 .collect();
202
203 let mut font_index_to_obj: HashMap<usize, IndexedNotoFont> = HashMap::new();
207 fn lookup_idx(boundaries: &[u32], value: u32) -> usize {
219 let mut start = 0usize;
220 let mut end = boundaries.len();
221 loop {
222 if start == end {
223 return start;
224 }
225 let mid = start + (end - start) / 2;
226 if value >= boundaries[mid] {
227 start = mid + 1;
228 } else {
229 end = mid;
230 }
231 }
232 }
233
234 let mut missing: Vec<u32> = Vec::new();
235 let mut required_component_indices: Vec<usize> = Vec::new();
236
237 for &cp in codepoints {
238 if self.code_points_with_no_known_font.contains(&cp) || cp > MAX_CODE_POINT {
239 continue;
240 }
241 let idx = lookup_idx(&self.lookup.boundaries, cp);
242 let comp = &mut components[idx];
243 if comp.fonts.is_empty() {
244 missing.push(cp);
245 } else {
246 if comp.cover_count == 0 {
247 required_component_indices.push(idx);
248 }
249 comp.cover_count += 1;
250 }
251 }
252
253 if !missing.is_empty() {
254 self.code_points_with_no_known_font.extend(missing);
255 }
256
257 if required_component_indices.is_empty() {
258 return Vec::new();
259 }
260
261 for &comp_idx in &required_component_indices {
263 for &font_idx in &components[comp_idx].fonts.clone() {
264 font_index_to_obj
265 .entry(font_idx)
266 .or_insert_with(|| IndexedNotoFont {
267 index: font_idx,
268 font: &NOTO_FONTS[font_idx],
269 cover_count: 0,
270 cover_components: Vec::new(),
271 });
272 }
273 }
274
275 let mut candidate_font_indices: HashSet<usize> = HashSet::new();
277
278 for &comp_idx in &required_component_indices {
279 let comp_cover = components[comp_idx].cover_count;
280 let fonts_clone = components[comp_idx].fonts.clone();
281 for font_idx in fonts_clone {
282 let font_obj = font_index_to_obj.get_mut(&font_idx).unwrap();
283 if font_obj.cover_count == 0 {
284 candidate_font_indices.insert(font_idx);
285 }
286 font_obj.cover_count += comp_cover;
287 font_obj.cover_components.push(comp_idx);
288 }
289 }
290
291 let mut candidate_vec: Vec<usize> = candidate_font_indices.into_iter().collect();
293
294 let mut selected: Vec<&'static NotoFont> = Vec::new();
295
296 while !candidate_vec.is_empty() {
298 let best_idx = select_font(&candidate_vec, &font_index_to_obj, language);
300 let best_font = font_index_to_obj.get(&best_idx).unwrap();
301 selected.push(best_font.font);
302
303 let covered_components: Vec<usize> = best_font.cover_components.clone();
304 for comp_idx in covered_components {
305 let comp_cover = components[comp_idx].cover_count;
306 let fonts_in_comp = components[comp_idx].fonts.clone();
307 for f_idx in fonts_in_comp {
308 if let Some(fobj) = font_index_to_obj.get_mut(&f_idx) {
309 fobj.cover_count = fobj.cover_count.saturating_sub(comp_cover);
310 fobj.cover_components.retain(|&c| c != comp_idx);
312 }
313 }
314 components[comp_idx].cover_count = 0;
315 }
316
317 candidate_vec.retain(|fid| {
318 font_index_to_obj
319 .get(fid)
320 .map(|f| f.cover_count != 0)
321 .unwrap_or(false)
322 });
323 }
324
325 selected
327 }
328}
329
330fn is_cjk_font(font: &NotoFont) -> bool {
331 is_noto_sans_sc(font)
332 || is_noto_sans_tc(font)
333 || is_noto_sans_hk(font)
334 || is_noto_sans_jp(font)
335 || is_noto_sans_kr(font)
336}
337fn is_noto_sans_sc(f: &NotoFont) -> bool {
338 f.name.starts_with("Noto Sans SC")
339}
340fn is_noto_sans_tc(f: &NotoFont) -> bool {
341 f.name.starts_with("Noto Sans TC")
342}
343fn is_noto_sans_hk(f: &NotoFont) -> bool {
344 f.name.starts_with("Noto Sans HK")
345}
346fn is_noto_sans_jp(f: &NotoFont) -> bool {
347 f.name.starts_with("Noto Sans JP")
348}
349fn is_noto_sans_kr(f: &NotoFont) -> bool {
350 f.name.starts_with("Noto Sans KR")
351}
352fn is_noto_color_emoji(f: &NotoFont) -> bool {
353 f.name.starts_with("Noto Color Emoji")
354}
355fn is_noto_sans_symbols(f: &NotoFont) -> bool {
356 f.name.starts_with("Noto Sans Symbols")
357}
358
359fn select_font(
360 candidates: &[usize],
361 arena: &HashMap<usize, IndexedNotoFont>,
362 language: &str,
363) -> usize {
364 let mut max_covered = -1i32;
366 let mut best_fonts: Vec<usize> = Vec::new();
367 let mut best: Option<usize> = None;
368 for &fid in candidates {
369 let f = &arena[&fid];
370 let cc = f.cover_count as i32;
371 if cc > max_covered {
372 best_fonts.clear();
373 best_fonts.push(fid);
374 best = Some(fid);
375 max_covered = cc;
376 } else if cc == max_covered {
377 best_fonts.push(fid);
378 if best.map(|b| fid < b).unwrap_or(true) {
379 best = Some(fid);
380 }
381 }
382 }
383
384 if best_fonts.len() > 1 {
386 let all_cjk = best_fonts.iter().all(|&fid| is_cjk_font(arena[&fid].font));
388 if all_cjk {
389 if let Some(idx) = select_best_for_language(&best_fonts, arena, language) {
390 return idx;
391 }
392 if let Some(idx) = select_best_for_language(candidates, arena, language) {
393 return idx;
394 }
395 } else {
396 if let Some(&fid) = best_fonts
398 .iter()
399 .find(|&&fid| is_noto_color_emoji(arena[&fid].font))
400 {
401 return fid;
402 }
403 if let Some(&fid) = best_fonts
404 .iter()
405 .find(|&&fid| is_noto_sans_symbols(arena[&fid].font))
406 {
407 return fid;
408 }
409 if let Some(&fid) = best_fonts
410 .iter()
411 .find(|&&fid| is_noto_sans_sc(arena[&fid].font))
412 {
413 return fid;
414 }
415 }
416 }
417
418 best.expect("No fallback font selected")
419}
420
421fn select_best_for_language(
422 candidates: &[usize],
423 arena: &HashMap<usize, IndexedNotoFont>,
424 language: &str,
425) -> Option<usize> {
426 match language {
427 "zh-Hans" | "zh-CN" | "zh-SG" | "zh-MY" => candidates
428 .iter()
429 .find(|&&fid| is_noto_sans_sc(arena[&fid].font))
430 .copied(),
431 "zh-Hant" | "zh-TW" | "zh-MO" => candidates
432 .iter()
433 .find(|&&fid| is_noto_sans_tc(arena[&fid].font))
434 .copied(),
435 "zh-HK" => candidates
436 .iter()
437 .find(|&&fid| is_noto_sans_hk(arena[&fid].font))
438 .copied(),
439 "ja" => candidates
440 .iter()
441 .find(|&&fid| is_noto_sans_jp(arena[&fid].font))
442 .copied(),
443 "ko" => candidates
444 .iter()
445 .find(|&&fid| is_noto_sans_kr(arena[&fid].font))
446 .copied(),
447 _ => None,
448 }
449}
450
451#[cfg(target_arch = "wasm32")]
454pub mod wasm_fallback {
455 use super::*;
456 use std::cell::RefCell;
457 use std::collections::HashSet;
458 use std::rc::Rc;
459 use wasm_bindgen::JsCast;
460 use wasm_bindgen::prelude::*;
461 use wasm_bindgen_futures::JsFuture;
462 use web_sys::{Request, Response};
463
464 const BATCH_WINDOW_MS: u32 = 60;
465 const MAX_BATCH_SIZE: usize = 10;
466
467 thread_local! {
468 static GLOBAL: Rc<RefCell<WebFallbackFontDownloader>> = Rc::new(RefCell::new(WebFallbackFontDownloader::new()));
469 static INSTALLED: RefCell<bool> = const { RefCell::new(false) };
470 static LISTENER_KEEP: RefCell<Option<std::sync::Arc<dyn crate::unresolved::UnresolvedListener>>> = const { RefCell::new(None) };
471 }
472
473 struct WebFallbackFontDownloader {
474 downloader: NotoFontDownloader,
475 queued: Vec<HashSet<u32>>,
476 is_running: bool,
477 error_count: u32,
478 }
479
480 impl WebFallbackFontDownloader {
481 fn new() -> Self {
482 Self {
483 downloader: NotoFontDownloader::new(),
484 queued: Vec::new(),
485 is_running: false,
486 error_count: 0,
487 }
488 }
489
490 fn submit(&mut self, codepoints: HashSet<u32>) {
491 if codepoints.is_empty() {
492 return;
493 }
494 self.queued.push(codepoints);
495 }
496
497 fn drain_channel(&mut self) {
498 self.queued.clear();
499 }
500 }
501
502 pub fn submit_unresolved(codepoints: Vec<u32>) {
503 if codepoints.is_empty() {
504 return;
505 }
506 let is_installed = INSTALLED.with(|v| *v.borrow());
508 if is_installed {
509 crate::unresolved::web_unresolved_registry().add_unresolved_vec(codepoints);
511 } else {
512 let set: HashSet<u32> = codepoints.into_iter().collect();
513 GLOBAL.with(|g| {
514 let mut mgr = g.borrow_mut();
515 mgr.submit(set.clone());
516 if !mgr.is_running {
517 mgr.is_running = true;
518 let g_clone = g.clone();
519 wasm_bindgen_futures::spawn_local(async move {
520 run_loop(g_clone).await;
521 });
522 }
523 });
524 crate::unresolved::web_unresolved_registry()
526 .add_unresolved_codepoints(&set.into_iter().collect::<Vec<_>>());
527 }
528 }
529
530 pub fn install_fallback_font_downloader() {
531 let already = INSTALLED.with(|v| {
532 let mut b = v.borrow_mut();
533 if *b {
534 true
535 } else {
536 *b = true;
537 false
538 }
539 });
540 if already {
541 return;
542 }
543
544 struct RegistryListener;
546 impl crate::unresolved::UnresolvedListener for RegistryListener {
547 fn on_unresolved_codepoints(&self, codepoints: &HashSet<u32>) {
548 let set = codepoints.clone();
549 GLOBAL.with(|g| {
550 let mut mgr = g.borrow_mut();
551 mgr.submit(set);
552 if !mgr.is_running {
553 mgr.is_running = true;
554 let g_clone = g.clone();
555 wasm_bindgen_futures::spawn_local(async move {
556 run_loop(g_clone).await;
557 });
558 }
559 });
560 }
561 fn on_new_font_installed(&self) {
562 }
565 }
566
567 let listener: std::sync::Arc<dyn crate::unresolved::UnresolvedListener> =
568 std::sync::Arc::new(RegistryListener);
569 crate::unresolved::web_unresolved_registry().add_listener(listener.clone());
570 LISTENER_KEEP.with(|c| *c.borrow_mut() = Some(listener));
572 }
573
574 async fn run_loop(global: Rc<RefCell<WebFallbackFontDownloader>>) {
575 loop {
576 let batch = {
577 loop {
579 let has = global.borrow().queued.len() > 0;
580 if has {
581 break;
582 }
583 gloo_timers_approx_delay(16).await;
584 }
585 let mut batch_set = HashSet::new();
586 {
588 let mut mgr = global.borrow_mut();
589 if !mgr.queued.is_empty() {
590 let first = mgr.queued.remove(0);
591 batch_set.extend(first);
592 }
593 }
594 let mut collected = 1;
596 let start = js_sys::Date::now();
597 while collected < MAX_BATCH_SIZE {
598 let elapsed = js_sys::Date::now() - start;
599 if elapsed >= BATCH_WINDOW_MS as f64 {
600 break;
601 }
602 let maybe = {
603 let mut mgr = global.borrow_mut();
604 if !mgr.queued.is_empty() {
605 Some(mgr.queued.remove(0))
606 } else {
607 None
608 }
609 };
610 if let Some(s) = maybe {
611 batch_set.extend(s);
612 collected += 1;
613 } else {
614 let remaining = (BATCH_WINDOW_MS as f64 - elapsed).max(0.0) as u32;
615 let wait = remaining.min(10);
616 if wait > 0 {
617 gloo_timers_approx_delay(wait).await;
618 }
619 if global.borrow().queued.is_empty() {
620 if js_sys::Date::now() - start >= BATCH_WINDOW_MS as f64 {
621 break;
622 }
623 }
624 }
625 }
626 batch_set
627 };
628
629 if batch.is_empty() {
630 continue;
631 }
632
633 let fonts_to_download: Vec<&'static NotoFont> = {
635 let mut mgr = global.borrow_mut();
636 let lang = web_sys::window()
637 .and_then(|w| w.navigator().language())
638 .unwrap_or_else(|| "en".to_string());
639 let res = mgr.downloader.get_fonts_to_download(&batch, &lang);
640 res
641 };
642
643 if fonts_to_download.is_empty() {
644 continue;
646 }
647
648 let mut successes: Vec<Vec<u8>> = Vec::new();
649 let mut any_success = false;
650 let mut all_failed = true;
651
652 use std::collections::HashSet;
653 let mut seen_urls: HashSet<String> = HashSet::new();
654 for font in &fonts_to_download {
655 let url = if font.name.starts_with("Noto Color Emoji") {
656 "https://cdn.jsdelivr.net/gh/googlefonts/noto-emoji@main/fonts/NotoColorEmoji.ttf".to_string()
658 } else {
659 format!("{}{}", FONT_FALLBACK_BASE_URL, font.url)
660 };
661 if !seen_urls.insert(url.clone()) {
662 continue;
663 }
664 match fetch_bytes(&url).await {
665 Ok(bytes) => {
666 successes.push(bytes);
667 any_success = true;
668 all_failed = false;
669 }
670 Err(_) => {}
671 }
672 }
673
674 if all_failed && !any_success {
675 let backoff = {
676 let mut mgr = global.borrow_mut();
677 let pause = mgr.error_count * 5;
678 mgr.error_count += 1;
679 pause
680 };
681 let batch_clone = batch.clone();
683 wasm_bindgen_futures::spawn_local(async move {
684 if backoff > 0 {
685 gloo_timers_approx_delay(backoff * 1000).await;
686 }
687 GLOBAL.with(|g| {
688 g.borrow_mut().submit(batch_clone);
689 });
690 });
691 continue;
692 }
693
694 {
696 let mut mgr = global.borrow_mut();
697 mgr.error_count = 0;
698 mgr.drain_channel();
700 }
701
702 for bytes in &successes {
703 crate::register_font_data(&bytes);
704 }
705
706 if any_success {
707 crate::unresolved::web_unresolved_registry().on_new_font_installed();
708 crate::clear_caches_for_fallback();
710 crate::bump_frame_for_fallback();
711 }
712 }
713 }
714
715 async fn gloo_timers_approx_delay(ms: u32) -> () {
716 let promise = js_sys::Promise::new(&mut |resolve, _reject| {
717 let window = web_sys::window().unwrap();
718 let _ =
719 window.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms as i32);
720 });
721 let _ = JsFuture::from(promise).await;
722 }
723
724 async fn fetch_bytes(url: &str) -> Result<Vec<u8>, JsValue> {
725 let mut opts = web_sys::RequestInit::new();
727 opts.set_method("GET");
728 opts.set_mode(web_sys::RequestMode::Cors);
729 opts.set_cache(web_sys::RequestCache::Default);
730 let request = Request::new_with_str_and_init(url, &opts)?;
731 let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
732 let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
733 let resp: Response = resp_value.dyn_into().unwrap();
734 if !resp.ok() {
735 return Err(JsValue::from_str(&format!(
736 "fetch failed status {}",
737 resp.status()
738 )));
739 }
740 let buffer = JsFuture::from(resp.array_buffer()?).await?;
741 let arr = js_sys::Uint8Array::new(&buffer);
742 let bytes = arr.to_vec();
743 Ok(bytes)
744 }
745
746 pub fn ensure_fallback_initialized() {
748 install_fallback_font_downloader();
749 }
750}
751
752#[cfg(not(target_arch = "wasm32"))]
753pub mod wasm_fallback {
754 pub fn submit_unresolved(_codepoints: Vec<u32>) {}
755 pub fn ensure_fallback_initialized() {}
756 pub fn install_fallback_font_downloader() {}
757}
758
759#[cfg(test)]
760mod tests {
761 use super::*;
762 use std::collections::HashSet;
763
764 #[test]
765 fn empty_returns_empty() {
766 let mut d = NotoFontDownloader::new();
767 let res = d.get_fonts_to_download(&HashSet::new(), "en");
768 assert!(res.is_empty());
769 assert!(d.get_codepoints_with_no_known_font().is_empty());
770 }
771
772 #[test]
773 fn above_max_ignored() {
774 let mut d = NotoFontDownloader::new();
775 let mut set = HashSet::new();
776 set.insert(0x110000);
777 let res = d.get_fonts_to_download(&set, "en");
778 assert!(res.is_empty());
779 assert!(d.get_codepoints_with_no_known_font().is_empty());
780 }
781
782 #[test]
783 fn pua_remembered() {
784 let mut d = NotoFontDownloader::new();
785 let mut set = HashSet::new();
786 set.insert(0xE000);
787 let first = d.get_fonts_to_download(&set, "en");
788 assert!(first.is_empty());
789 assert!(d.get_codepoints_with_no_known_font().contains(&0xE000));
790 let second = d.get_fonts_to_download(&set, "en");
791 assert!(second.is_empty());
792 }
793
794 #[test]
795 fn arabic_resolves() {
796 let mut d = NotoFontDownloader::new();
797 let set: HashSet<u32> = [0x0639, 0x0641].into_iter().collect();
798 let fonts = d.get_fonts_to_download(&set, "en");
799 assert!(!fonts.is_empty(), "Arabic should resolve");
800 assert!(
801 fonts.iter().all(|f| f.name.starts_with("Noto Sans Arabic")),
802 "got {:?}",
803 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
804 );
805 }
806
807 #[test]
808 fn emoji_resolves() {
809 let mut d = NotoFontDownloader::new();
810 let set: HashSet<u32> = [0x1F600, 0x1F389].into_iter().collect();
811 let fonts = d.get_fonts_to_download(&set, "en");
812 assert!(!fonts.is_empty());
813 assert!(
814 fonts.iter().all(|f| f.name.starts_with("Noto Color Emoji")),
815 "got {:?}",
816 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
817 );
818 }
819
820 #[test]
821 fn cjk_zh_cn() {
822 let mut d = NotoFontDownloader::new();
823 let set: HashSet<u32> = [0x5B57].into_iter().collect();
824 let fonts = d.get_fonts_to_download(&set, "zh-CN");
825 assert!(!fonts.is_empty());
826 assert!(
827 fonts.iter().all(|f| f.name.starts_with("Noto Sans SC")),
828 "got {:?}",
829 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
830 );
831 }
832
833 #[test]
834 fn japanese_hiragana() {
835 let mut d = NotoFontDownloader::new();
836 let set: HashSet<u32> = [0x3042].into_iter().collect();
837 let fonts = d.get_fonts_to_download(&set, "ja");
838 assert!(!fonts.is_empty());
839 assert!(
840 fonts.iter().all(|f| f.name.starts_with("Noto Sans JP")),
841 "got {:?}",
842 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
843 );
844 }
845
846 #[test]
847 fn devanagari() {
848 let mut d = NotoFontDownloader::new();
849 let set: HashSet<u32> = [0x0905, 0x0915].into_iter().collect();
850 let fonts = d.get_fonts_to_download(&set, "en");
851 assert!(!fonts.is_empty(), "Devanagari should resolve to Noto Sans*");
852 assert!(
854 fonts.iter().all(|f| f.name.starts_with("Noto Sans")),
855 "got {:?}",
856 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
857 );
858 }
859
860 #[test]
861 fn hebrew_resolves() {
862 let mut d = NotoFontDownloader::new();
863 let set: HashSet<u32> = [0x05E9, 0x05D0].into_iter().collect();
864 let fonts = d.get_fonts_to_download(&set, "en");
865 assert!(!fonts.is_empty());
866 assert!(
867 fonts.iter().all(|f| f.name.starts_with("Noto Sans Hebrew")),
868 "got {:?}",
869 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
870 );
871 }
872
873 #[test]
874 fn thai_resolves() {
875 let mut d = NotoFontDownloader::new();
876 let set: HashSet<u32> = [0x0E01, 0x0E2A].into_iter().collect();
877 let fonts = d.get_fonts_to_download(&set, "en");
878 assert!(!fonts.is_empty());
879 assert!(
880 fonts.iter().all(|f| f.name.starts_with("Noto Sans Thai")),
881 "got {:?}",
882 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
883 );
884 }
885
886 #[test]
887 fn bengali_resolves() {
888 let mut d = NotoFontDownloader::new();
889 let set: HashSet<u32> = [0x0985, 0x0995].into_iter().collect();
890 let fonts = d.get_fonts_to_download(&set, "en");
891 assert!(!fonts.is_empty());
892 assert!(
893 fonts
894 .iter()
895 .all(|f| f.name.starts_with("Noto Sans Bengali")),
896 "got {:?}",
897 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
898 );
899 }
900
901 #[test]
902 fn box_drawing_resolves() {
903 let mut d = NotoFontDownloader::new();
904 let set: HashSet<u32> = [0x2500, 0x2502].into_iter().collect();
905 let fonts = d.get_fonts_to_download(&set, "en");
906 assert!(!fonts.is_empty());
907 assert!(
909 fonts.iter().all(|f| f.name.starts_with("Noto Sans")),
910 "got {:?}",
911 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
912 );
913 }
914
915 #[test]
916 fn korean_ko() {
917 let mut d = NotoFontDownloader::new();
918 let set: HashSet<u32> = [0xACA8, 0xACAF, 0xACF0].into_iter().collect();
919 let fonts = d.get_fonts_to_download(&set, "ko");
920 assert!(!fonts.is_empty());
921 assert!(
922 fonts.iter().all(|f| f.name.starts_with("Noto Sans KR")),
923 "got {:?}",
924 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
925 );
926 }
927
928 #[test]
929 fn cjk_zh_hant() {
930 let mut d = NotoFontDownloader::new();
931 let set: HashSet<u32> = [0x5B57].into_iter().collect();
932 let fonts = d.get_fonts_to_download(&set, "zh-TW");
933 assert!(!fonts.is_empty());
934 assert!(
935 fonts.iter().all(|f| f.name.starts_with("Noto Sans TC")),
936 "got {:?}",
937 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
938 );
939 }
940}