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
9const PREFIX_DIGIT_0: u32 = 48;
11const PREFIX_RADIX: u32 = 10;
12const FONT_INDEX_DIGIT_0: u32 = 97; const FONT_INDEX_RADIX: u32 = 26;
14const RANGE_SIZE_DIGIT_0: u32 = 97; const RANGE_SIZE_RADIX: u32 = 26;
16const RANGE_VALUE_DIGIT_0: u32 = 65; const RANGE_VALUE_RADIX: u32 = 26;
18const MAX_CODE_POINT: u32 = 0x10FFFF;
19
20#[cfg(target_arch = "wasm32")]
21fn direct_font_url(font: &NotoFont) -> &'static str {
22 font.url
23}
24
25pub struct IndexedNotoFont {
26 pub index: usize,
27 pub font: &'static NotoFont,
28 pub cover_count: usize,
29 pub cover_components: Vec<usize>, }
31
32pub struct FallbackFontComponent {
33 pub fonts: Vec<usize>, pub cover_count: usize,
35}
36
37static EMPTY_FALLBACK_COMPONENT: FallbackFontComponent = FallbackFontComponent {
38 fonts: Vec::new(),
39 cover_count: 0,
40};
41
42pub struct UnicodePropertyLookup {
43 boundaries: Vec<u32>,
44 values: Vec<FallbackFontComponent>,
48}
49
50impl UnicodePropertyLookup {
51 pub fn lookup(&self, value: u32) -> &FallbackFontComponent {
52 self.try_lookup(value).unwrap_or(&EMPTY_FALLBACK_COMPONENT)
53 }
54
55 pub fn try_lookup(&self, value: u32) -> Option<&FallbackFontComponent> {
56 if value > MAX_CODE_POINT {
57 return None;
58 }
59 let mut start: usize = 0;
60 let mut end: usize = self.boundaries.len();
61 loop {
62 if start == end {
63 return self.values.get(start);
64 }
65 let mid = start + (end - start) / 2;
66 if value >= self.boundaries[mid] {
67 start = mid + 1;
68 } else {
69 end = mid;
70 }
71 }
72 }
73
74 pub fn create() -> Self {
75 let property_enum_values = decode_font_components();
77
78 let packed_data = ENCODED_NOTO_FONT_SET_RANGES;
80
81 let mut boundaries: Vec<u32> = Vec::new();
82 let mut values: Vec<FallbackFontComponent> = Vec::new();
83
84 let mut start: u32 = 0;
85 let mut prefix: u32 = 0;
86 let mut size: u32 = 1;
87
88 for ch in packed_data.chars() {
89 let code = ch as u32;
90 if (RANGE_VALUE_DIGIT_0..RANGE_VALUE_DIGIT_0 + RANGE_VALUE_RADIX).contains(&code) {
91 let idx = (prefix * RANGE_VALUE_RADIX + (code - RANGE_VALUE_DIGIT_0)) as usize;
92 let template = &property_enum_values[idx];
94 let comp = FallbackFontComponent {
96 fonts: template.fonts.clone(),
97 cover_count: 0,
98 };
99 start += size;
100 boundaries.push(start);
101 values.push(comp);
102 prefix = 0;
103 size = 1;
104 } else if (RANGE_SIZE_DIGIT_0..RANGE_SIZE_DIGIT_0 + RANGE_SIZE_RADIX).contains(&code) {
105 size = prefix * RANGE_SIZE_RADIX + (code - RANGE_SIZE_DIGIT_0) + 2;
106 prefix = 0;
107 } else if (PREFIX_DIGIT_0..PREFIX_DIGIT_0 + PREFIX_RADIX).contains(&code) {
108 prefix = prefix * PREFIX_RADIX + (code - PREFIX_DIGIT_0);
109 } else {
110 panic!("Unexpected encoded range character: {}", ch);
111 }
112 }
113
114 assert_eq!(
115 start,
116 MAX_CODE_POINT + 1,
117 "Bad fallback map size: {}",
118 start
119 );
120
121 Self { boundaries, values }
122 }
123}
124
125fn decode_font_components() -> Vec<FallbackFontComponent> {
126 ENCODED_NOTO_FONT_SETS
127 .split(',')
128 .map(|component_data| {
129 let fonts = decode_font_set(component_data);
130 FallbackFontComponent {
131 fonts,
132 cover_count: 0,
133 }
134 })
135 .collect()
136}
137
138fn decode_font_set(data: &str) -> Vec<usize> {
139 let mut result: Vec<usize> = Vec::new();
140 let mut previous_index: i32 = -1;
141 let mut prefix: u32 = 0;
142 for ch in data.chars() {
143 let code = ch as u32;
144 if (FONT_INDEX_DIGIT_0..FONT_INDEX_DIGIT_0 + FONT_INDEX_RADIX).contains(&code) {
145 let delta = (prefix * FONT_INDEX_RADIX + (code - FONT_INDEX_DIGIT_0)) as i32;
146 let index = previous_index + delta + 1;
147 result.push(index as usize);
148 previous_index = index;
149 prefix = 0;
150 } else if (PREFIX_DIGIT_0..PREFIX_DIGIT_0 + PREFIX_RADIX).contains(&code) {
151 prefix = prefix * PREFIX_RADIX + (code - PREFIX_DIGIT_0);
152 } else {
153 panic!("Unexpected encoded font-set char: {}", ch);
154 }
155 }
156 result
157}
158
159pub struct NotoFontDownloader {
161 code_points_with_no_known_font: HashSet<u32>,
162 lookup: UnicodePropertyLookup,
163 }
166
167impl Default for NotoFontDownloader {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173impl NotoFontDownloader {
174 pub fn new() -> Self {
175 Self {
176 code_points_with_no_known_font: HashSet::new(),
177 lookup: UnicodePropertyLookup::create(),
178 }
179 }
180
181 pub fn get_codepoints_with_no_known_font(&self) -> &HashSet<u32> {
182 &self.code_points_with_no_known_font
183 }
184
185 pub fn get_fonts_to_download(
188 &mut self,
189 codepoints: &HashSet<u32>,
190 language: &str,
191 ) -> Vec<&'static NotoFont> {
192 if codepoints.is_empty() {
193 return Vec::new();
194 }
195
196 let mut cover_counts: HashMap<usize, usize> = HashMap::new();
197 let mut font_index_to_obj: HashMap<usize, IndexedNotoFont> = HashMap::new();
198
199 fn lookup_idx(boundaries: &[u32], value: u32) -> Option<usize> {
200 if value > MAX_CODE_POINT {
201 return None;
202 }
203 let mut start = 0usize;
204 let mut end = boundaries.len();
205 loop {
206 if start == end {
207 return Some(start);
208 }
209 let mid = start + (end - start) / 2;
210 if value >= boundaries[mid] {
211 start = mid + 1;
212 } else {
213 end = mid;
214 }
215 }
216 }
217
218 let mut required_component_indices: Vec<usize> = Vec::new();
219 let mut codepoints: Vec<u32> = codepoints.iter().copied().collect();
220 codepoints.sort_unstable();
221
222 for cp in codepoints {
223 if self.code_points_with_no_known_font.contains(&cp) || cp > MAX_CODE_POINT {
224 continue;
225 }
226 let Some(idx) = lookup_idx(&self.lookup.boundaries, cp) else {
227 continue;
228 };
229 let Some(comp) = self.lookup.values.get(idx) else {
230 continue;
231 };
232 if comp.fonts.is_empty() {
233 self.code_points_with_no_known_font.insert(cp);
234 } else {
235 let count = cover_counts.entry(idx).or_insert(0);
236 if *count == 0 {
237 required_component_indices.push(idx);
238 }
239 *count += 1;
240 }
241 }
242
243 if required_component_indices.is_empty() {
244 return Vec::new();
245 }
246
247 for &comp_idx in &required_component_indices {
249 for &font_idx in &self.lookup.values[comp_idx].fonts {
250 font_index_to_obj
251 .entry(font_idx)
252 .or_insert_with(|| IndexedNotoFont {
253 index: font_idx,
254 font: &NOTO_FONTS[font_idx],
255 cover_count: 0,
256 cover_components: Vec::new(),
257 });
258 }
259 }
260
261 let mut candidate_font_indices: HashSet<usize> = HashSet::new();
263
264 for &comp_idx in &required_component_indices {
265 let comp_cover = cover_counts.get(&comp_idx).copied().unwrap_or(0);
266 for &font_idx in &self.lookup.values[comp_idx].fonts {
267 let font_obj = font_index_to_obj.get_mut(&font_idx).unwrap();
268 if font_obj.cover_count == 0 {
269 candidate_font_indices.insert(font_idx);
270 }
271 font_obj.cover_count += comp_cover;
272 font_obj.cover_components.push(comp_idx);
273 }
274 }
275
276 let mut candidate_vec: Vec<usize> = candidate_font_indices.into_iter().collect();
278 candidate_vec.sort_unstable();
279
280 let mut selected: Vec<&'static NotoFont> = Vec::new();
281
282 while !candidate_vec.is_empty() {
284 let best_idx = select_font(&candidate_vec, &font_index_to_obj, language);
286 let best_font = font_index_to_obj.get(&best_idx).unwrap();
287 selected.push(best_font.font);
288 let covered_components = {
289 let best_font = font_index_to_obj.get_mut(&best_idx).unwrap();
290 std::mem::take(&mut best_font.cover_components)
291 };
292 for comp_idx in covered_components {
293 let comp_cover = cover_counts.get(&comp_idx).copied().unwrap_or(0);
294 for &f_idx in &self.lookup.values[comp_idx].fonts {
295 if let Some(fobj) = font_index_to_obj.get_mut(&f_idx) {
296 fobj.cover_count = fobj.cover_count.saturating_sub(comp_cover);
297 fobj.cover_components.retain(|&c| c != comp_idx);
299 }
300 }
301 cover_counts.remove(&comp_idx);
302 }
303
304 candidate_vec.retain(|fid| {
305 font_index_to_obj
306 .get(fid)
307 .map(|f| f.cover_count != 0)
308 .unwrap_or(false)
309 });
310 }
311
312 selected
314 }
315}
316
317#[cfg(any(target_arch = "wasm32", test))]
318#[derive(Default)]
319struct FontDownloadProgress {
320 installed_urls: HashSet<String>,
321}
322
323#[cfg(any(target_arch = "wasm32", test))]
324impl FontDownloadProgress {
325 fn needs_fetch(&self, url: &str) -> bool {
326 !self.installed_urls.contains(url)
327 }
328
329 fn record_success(&mut self, url: String) {
330 self.installed_urls.insert(url);
331 }
332}
333
334fn is_cjk_font(font: &NotoFont) -> bool {
335 is_noto_sans_sc(font)
336 || is_noto_sans_tc(font)
337 || is_noto_sans_hk(font)
338 || is_noto_sans_jp(font)
339 || is_noto_sans_kr(font)
340}
341fn is_noto_sans_sc(f: &NotoFont) -> bool {
342 f.name.starts_with("Noto Sans SC")
343}
344fn is_noto_sans_tc(f: &NotoFont) -> bool {
345 f.name.starts_with("Noto Sans TC")
346}
347fn is_noto_sans_hk(f: &NotoFont) -> bool {
348 f.name.starts_with("Noto Sans HK")
349}
350fn is_noto_sans_jp(f: &NotoFont) -> bool {
351 f.name.starts_with("Noto Sans JP")
352}
353fn is_noto_sans_kr(f: &NotoFont) -> bool {
354 f.name.starts_with("Noto Sans KR")
355}
356fn is_noto_color_emoji(f: &NotoFont) -> bool {
357 f.name.starts_with("Noto Color Emoji")
358}
359fn is_noto_sans_symbols(f: &NotoFont) -> bool {
360 f.name.starts_with("Noto Sans Symbols")
361}
362
363fn select_font(
364 candidates: &[usize],
365 arena: &HashMap<usize, IndexedNotoFont>,
366 language: &str,
367) -> usize {
368 let mut max_covered = -1i32;
370 let mut best_fonts: Vec<usize> = Vec::new();
371 let mut best: Option<usize> = None;
372 for &fid in candidates {
373 let f = &arena[&fid];
374 let cc = f.cover_count as i32;
375 if cc > max_covered {
376 best_fonts.clear();
377 best_fonts.push(fid);
378 best = Some(fid);
379 max_covered = cc;
380 } else if cc == max_covered {
381 best_fonts.push(fid);
382 if best.map(|b| fid < b).unwrap_or(true) {
383 best = Some(fid);
384 }
385 }
386 }
387
388 if best_fonts.len() > 1 {
390 let all_cjk = best_fonts.iter().all(|&fid| is_cjk_font(arena[&fid].font));
392 if all_cjk {
393 if let Some(idx) = select_best_for_language(&best_fonts, arena, language) {
394 return idx;
395 }
396 if let Some(idx) = select_best_for_language(candidates, arena, language) {
397 return idx;
398 }
399 } else {
400 if let Some(&fid) = best_fonts
402 .iter()
403 .find(|&&fid| is_noto_color_emoji(arena[&fid].font))
404 {
405 return fid;
406 }
407 if let Some(&fid) = best_fonts
408 .iter()
409 .find(|&&fid| is_noto_sans_symbols(arena[&fid].font))
410 {
411 return fid;
412 }
413 if let Some(&fid) = best_fonts
414 .iter()
415 .find(|&&fid| is_noto_sans_sc(arena[&fid].font))
416 {
417 return fid;
418 }
419 }
420 }
421
422 best.expect("No fallback font selected")
423}
424
425fn select_best_for_language(
426 candidates: &[usize],
427 arena: &HashMap<usize, IndexedNotoFont>,
428 language: &str,
429) -> Option<usize> {
430 match language {
431 "zh-Hans" | "zh-CN" | "zh-SG" | "zh-MY" => candidates
432 .iter()
433 .find(|&&fid| is_noto_sans_sc(arena[&fid].font))
434 .copied(),
435 "zh-Hant" | "zh-TW" | "zh-MO" => candidates
436 .iter()
437 .find(|&&fid| is_noto_sans_tc(arena[&fid].font))
438 .copied(),
439 "zh-HK" => candidates
440 .iter()
441 .find(|&&fid| is_noto_sans_hk(arena[&fid].font))
442 .copied(),
443 "ja" => candidates
444 .iter()
445 .find(|&&fid| is_noto_sans_jp(arena[&fid].font))
446 .copied(),
447 "ko" => candidates
448 .iter()
449 .find(|&&fid| is_noto_sans_kr(arena[&fid].font))
450 .copied(),
451 _ => None,
452 }
453}
454
455#[cfg(target_arch = "wasm32")]
458pub mod wasm_fallback {
459 use super::*;
460 use std::cell::RefCell;
461 use std::collections::{HashSet, VecDeque};
462 use std::rc::Rc;
463 use wasm_bindgen::JsCast;
464 use wasm_bindgen::prelude::*;
465 use wasm_bindgen_futures::JsFuture;
466 use web_sys::{Request, Response};
467
468 const MAX_BATCH_SIZE: usize = 10;
469
470 thread_local! {
471 static GLOBAL: Rc<RefCell<WebFallbackFontDownloader>> = Rc::new(RefCell::new(WebFallbackFontDownloader::new()));
472 static INSTALLED: RefCell<bool> = const { RefCell::new(false) };
473 static LISTENER_KEEP: RefCell<Option<std::sync::Arc<dyn crate::unresolved::UnresolvedListener>>> = const { RefCell::new(None) };
474 }
475
476 struct WebFallbackFontDownloader {
477 downloader: NotoFontDownloader,
478 queued: VecDeque<HashSet<u32>>,
479 is_running: bool,
480 error_count: u32,
481 progress: FontDownloadProgress,
482 }
483
484 impl WebFallbackFontDownloader {
485 fn new() -> Self {
486 Self {
487 downloader: NotoFontDownloader::new(),
488 queued: VecDeque::new(),
489 is_running: false,
490 error_count: 0,
491 progress: FontDownloadProgress::default(),
492 }
493 }
494
495 fn submit(&mut self, codepoints: HashSet<u32>) {
496 if codepoints.is_empty() {
497 return;
498 }
499 self.queued.push_back(codepoints);
500 }
501 }
502
503 fn start_worker(global: Rc<RefCell<WebFallbackFontDownloader>>) {
504 let should_start = {
505 let mut state = global.borrow_mut();
506 if state.is_running {
507 false
508 } else {
509 state.is_running = true;
510 true
511 }
512 };
513 if should_start {
514 wasm_bindgen_futures::spawn_local(async move {
515 run_worker(global).await;
516 });
517 }
518 }
519
520 fn enqueue(codepoints: HashSet<u32>) {
521 let global = GLOBAL.with(|global| {
522 let mut state = global.borrow_mut();
523 if let Some(queued) = state.queued.back_mut() {
524 queued.extend(codepoints);
525 } else {
526 state.submit(codepoints);
527 }
528 global.clone()
529 });
530 start_worker(global);
531 }
532
533 pub fn submit_unresolved(codepoints: Vec<u32>) {
534 if codepoints.is_empty() {
535 return;
536 }
537 crate::unresolved::web_unresolved_registry().add_unresolved_codepoints(&codepoints);
538 let set: HashSet<u32> = codepoints.into_iter().collect();
539 let installed = INSTALLED.with(|installed| *installed.borrow());
540 if !installed {
541 enqueue(set);
542 }
543 }
544
545 pub fn install_fallback_font_downloader() {
546 let already = INSTALLED.with(|v| {
547 let mut b = v.borrow_mut();
548 if *b {
549 true
550 } else {
551 *b = true;
552 false
553 }
554 });
555 if already {
556 return;
557 }
558
559 struct RegistryListener;
561 impl crate::unresolved::UnresolvedListener for RegistryListener {
562 fn on_unresolved_codepoints(&self, codepoints: &HashSet<u32>) {
563 enqueue(codepoints.clone());
564 }
565 fn on_new_font_installed(&self) {
566 }
569 }
570
571 let listener: std::sync::Arc<dyn crate::unresolved::UnresolvedListener> =
572 std::sync::Arc::new(RegistryListener);
573 crate::unresolved::web_unresolved_registry().add_listener(listener.clone());
574 LISTENER_KEEP.with(|c| *c.borrow_mut() = Some(listener));
576 let pending = crate::unresolved::web_unresolved_registry().snapshot();
577 if !pending.is_empty() {
578 enqueue(pending);
579 }
580 }
581
582 async fn run_worker(global: Rc<RefCell<WebFallbackFontDownloader>>) {
583 loop {
584 let batch = {
585 let mut state = global.borrow_mut();
586 if state.queued.is_empty() {
587 state.is_running = false;
588 return;
589 }
590 let Some(mut batch) = state.queued.pop_front() else {
591 continue;
592 };
593 let mut count = 1;
594 while count < MAX_BATCH_SIZE && !state.queued.is_empty() {
595 if let Some(next) = state.queued.pop_front() {
596 batch.extend(next);
597 }
598 count += 1;
599 }
600 batch
601 };
602
603 if batch.is_empty() {
604 continue;
605 }
606
607 let fonts_to_download: Vec<&'static NotoFont> = {
608 let mut state = global.borrow_mut();
609 let language = web_sys::window()
610 .and_then(|window| window.navigator().language())
611 .unwrap_or_else(|| "en".to_string());
612 state.downloader.get_fonts_to_download(&batch, &language)
613 };
614
615 if fonts_to_download.is_empty() {
616 continue;
617 }
618
619 let mut any_success = false;
620 let mut failed = false;
621 let mut seen_urls: HashSet<&'static str> = HashSet::new();
622 for font in &fonts_to_download {
623 let url = direct_font_url(font);
624 if !seen_urls.insert(url) {
625 continue;
626 }
627 let needs_fetch = global.borrow().progress.needs_fetch(&url);
628 if !needs_fetch {
629 continue;
630 }
631 match fetch_bytes(&url).await {
632 Ok(bytes) => {
633 if crate::register_font_data_if_usable(&bytes) {
634 global.borrow_mut().progress.record_success(url.to_owned());
635 any_success = true;
636 } else {
637 failed = true;
638 }
639 }
640 Err(_) => failed = true,
641 }
642 }
643
644 if any_success {
645 crate::unresolved::web_unresolved_registry().on_new_font_installed();
646 }
647
648 if failed {
649 let backoff = {
650 let mut state = global.borrow_mut();
651 let pause = state.error_count.saturating_mul(5).min(60);
652 state.error_count = state.error_count.saturating_add(1);
653 pause
654 };
655 if backoff > 0 {
656 gloo_timers_approx_delay(backoff.saturating_mul(1000)).await;
657 }
658 global.borrow_mut().queued.push_back(batch);
659 continue;
660 }
661
662 global.borrow_mut().error_count = 0;
663 }
664 }
665
666 async fn gloo_timers_approx_delay(ms: u32) -> () {
667 let promise = js_sys::Promise::new(&mut |resolve, _reject| {
668 let Some(window) = web_sys::window() else {
669 let _ = resolve.call0(&JsValue::NULL);
670 return;
671 };
672 let _ =
673 window.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms as i32);
674 });
675 let _ = JsFuture::from(promise).await;
676 }
677
678 async fn fetch_bytes(url: &str) -> Result<Vec<u8>, JsValue> {
679 let opts = web_sys::RequestInit::new();
680 opts.set_method("GET");
681 opts.set_mode(web_sys::RequestMode::Cors);
682 opts.set_cache(web_sys::RequestCache::Default);
683 let request = Request::new_with_str_and_init(url, &opts)?;
684 let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
685 let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
686 let resp: Response = resp_value
687 .dyn_into()
688 .map_err(|_| JsValue::from_str("fetch did not return a Response"))?;
689 if !resp.ok() {
690 return Err(JsValue::from_str(&format!(
691 "fetch failed status {}",
692 resp.status()
693 )));
694 }
695 let buffer = JsFuture::from(resp.array_buffer()?).await?;
696 let arr = js_sys::Uint8Array::new(&buffer);
697 let bytes = arr.to_vec();
698 Ok(bytes)
699 }
700
701 pub fn ensure_fallback_initialized() {
702 install_fallback_font_downloader();
703 }
704}
705
706#[cfg(not(target_arch = "wasm32"))]
707pub mod wasm_fallback {
708 pub fn submit_unresolved(_codepoints: Vec<u32>) {}
709 pub fn ensure_fallback_initialized() {}
710 pub fn install_fallback_font_downloader() {}
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716 use std::collections::HashSet;
717
718 #[test]
719 fn empty_returns_empty() {
720 let mut d = NotoFontDownloader::new();
721 let res = d.get_fonts_to_download(&HashSet::new(), "en");
722 assert!(res.is_empty());
723 assert!(d.get_codepoints_with_no_known_font().is_empty());
724 }
725
726 #[test]
727 fn above_max_ignored() {
728 let mut d = NotoFontDownloader::new();
729 let mut set = HashSet::new();
730 set.insert(0x110000);
731 let res = d.get_fonts_to_download(&set, "en");
732 assert!(res.is_empty());
733 assert!(d.get_codepoints_with_no_known_font().is_empty());
734 }
735
736 #[test]
737 fn pua_remembered() {
738 let mut d = NotoFontDownloader::new();
739 let mut set = HashSet::new();
740 set.insert(0xE000);
741 let first = d.get_fonts_to_download(&set, "en");
742 assert!(first.is_empty());
743 assert!(d.get_codepoints_with_no_known_font().contains(&0xE000));
744 let second = d.get_fonts_to_download(&set, "en");
745 assert!(second.is_empty());
746 }
747
748 #[test]
749 fn arabic_resolves() {
750 let mut d = NotoFontDownloader::new();
751 let set: HashSet<u32> = [0x0639, 0x0641].into_iter().collect();
752 let fonts = d.get_fonts_to_download(&set, "en");
753 assert!(!fonts.is_empty(), "Arabic should resolve");
754 assert!(
755 fonts.iter().all(|f| f.name.starts_with("Noto Sans Arabic")),
756 "got {:?}",
757 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
758 );
759 }
760
761 #[test]
762 fn emoji_resolves() {
763 let mut d = NotoFontDownloader::new();
764 let set: HashSet<u32> = [0x1F600, 0x1F389].into_iter().collect();
765 let fonts = d.get_fonts_to_download(&set, "en");
766 assert!(!fonts.is_empty());
767 assert!(
768 fonts.iter().all(|f| f.name.starts_with("Noto Color Emoji")),
769 "got {:?}",
770 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
771 );
772 }
773
774 #[test]
775 fn cjk_zh_cn() {
776 let mut d = NotoFontDownloader::new();
777 let set: HashSet<u32> = [0x5B57].into_iter().collect();
778 let fonts = d.get_fonts_to_download(&set, "zh-CN");
779 assert!(!fonts.is_empty());
780 assert!(
781 fonts.iter().all(|f| f.name.starts_with("Noto Sans SC")),
782 "got {:?}",
783 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
784 );
785 }
786
787 #[test]
788 fn japanese_hiragana() {
789 let mut d = NotoFontDownloader::new();
790 let set: HashSet<u32> = [0x3042].into_iter().collect();
791 let fonts = d.get_fonts_to_download(&set, "ja");
792 assert!(!fonts.is_empty());
793 assert!(
794 fonts.iter().all(|f| f.name.starts_with("Noto Sans JP")),
795 "got {:?}",
796 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
797 );
798 }
799
800 #[test]
801 fn devanagari() {
802 let mut d = NotoFontDownloader::new();
803 let set: HashSet<u32> = [0x0905, 0x0915].into_iter().collect();
804 let fonts = d.get_fonts_to_download(&set, "en");
805 assert!(!fonts.is_empty(), "Devanagari should resolve to Noto Sans*");
806 assert!(
808 fonts.iter().all(|f| f.name.starts_with("Noto Sans")),
809 "got {:?}",
810 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
811 );
812 }
813
814 #[test]
815 fn hebrew_resolves() {
816 let mut d = NotoFontDownloader::new();
817 let set: HashSet<u32> = [0x05E9, 0x05D0].into_iter().collect();
818 let fonts = d.get_fonts_to_download(&set, "en");
819 assert!(!fonts.is_empty());
820 assert!(
821 fonts.iter().all(|f| f.name.starts_with("Noto Sans Hebrew")),
822 "got {:?}",
823 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
824 );
825 }
826
827 #[test]
828 fn thai_resolves() {
829 let mut d = NotoFontDownloader::new();
830 let set: HashSet<u32> = [0x0E01, 0x0E2A].into_iter().collect();
831 let fonts = d.get_fonts_to_download(&set, "en");
832 assert!(!fonts.is_empty());
833 assert!(
834 fonts.iter().all(|f| f.name.starts_with("Noto Sans Thai")),
835 "got {:?}",
836 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
837 );
838 }
839
840 #[test]
841 fn bengali_resolves() {
842 let mut d = NotoFontDownloader::new();
843 let set: HashSet<u32> = [0x0985, 0x0995].into_iter().collect();
844 let fonts = d.get_fonts_to_download(&set, "en");
845 assert!(!fonts.is_empty());
846 assert!(
847 fonts
848 .iter()
849 .all(|f| f.name.starts_with("Noto Sans Bengali")),
850 "got {:?}",
851 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
852 );
853 }
854
855 #[test]
856 fn box_drawing_resolves() {
857 let mut d = NotoFontDownloader::new();
858 let set: HashSet<u32> = [0x2500, 0x2502].into_iter().collect();
859 let fonts = d.get_fonts_to_download(&set, "en");
860 assert!(!fonts.is_empty());
861 assert!(
863 fonts.iter().all(|f| f.name.starts_with("Noto Sans")),
864 "got {:?}",
865 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
866 );
867 }
868
869 #[test]
870 fn korean_ko() {
871 let mut d = NotoFontDownloader::new();
872 let set: HashSet<u32> = [0xACA8, 0xACAF, 0xACF0].into_iter().collect();
873 let fonts = d.get_fonts_to_download(&set, "ko");
874 assert!(!fonts.is_empty());
875 assert!(
876 fonts.iter().all(|f| f.name.starts_with("Noto Sans KR")),
877 "got {:?}",
878 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
879 );
880 }
881
882 #[test]
883 fn cjk_zh_hant() {
884 let mut d = NotoFontDownloader::new();
885 let set: HashSet<u32> = [0x5B57].into_iter().collect();
886 let fonts = d.get_fonts_to_download(&set, "zh-TW");
887 assert!(!fonts.is_empty());
888 assert!(
889 fonts.iter().all(|f| f.name.starts_with("Noto Sans TC")),
890 "got {:?}",
891 fonts.iter().map(|f| f.name).collect::<Vec<_>>()
892 );
893 }
894
895 #[test]
896 fn successful_font_is_not_fetched_again() {
897 let mut progress = FontDownloadProgress::default();
898 assert!(progress.needs_fetch("successful"));
899 assert!(progress.needs_fetch("failed"));
900 progress.record_success("successful".into());
901 assert!(!progress.needs_fetch("successful"));
902 assert!(progress.needs_fetch("failed"));
903 }
904}