perspective_viewer/ui/
font_loader.rs1use std::cell::{Cell, Ref, RefCell};
14use std::future::Future;
15use std::iter::{Iterator, repeat_with};
16use std::rc::Rc;
17
18use futures::future::{join_all, select_all};
19use perspective_js::utils::{global, *};
20use wasm_bindgen::prelude::*;
21use wasm_bindgen::{JsCast, intern};
22use wasm_bindgen_futures::JsFuture;
23use yew::prelude::*;
24
25use crate::utils::*;
26
27const FONT_TEST_SAMPLE: &str = "ABCDΔ";
28
29const FONT_DOWNLOAD_TIMEOUT_MS: i32 = 1000;
30
31#[derive(Clone, Properties)]
32pub struct FontLoaderProps {
33 state: Rc<FontLoaderState>,
34}
35
36impl PartialEq for FontLoaderProps {
37 fn eq(&self, _rhs: &Self) -> bool {
38 false
39 }
40}
41
42#[function_component(FontLoader)]
43pub fn font_loader(props: &FontLoaderProps) -> Html {
44 if matches!(props.get_status(), FontLoaderStatus::Finished) {
45 html! {}
46 } else {
47 let inner = props
48 .get_fonts()
49 .iter()
50 .map(font_test_html)
51 .collect::<Html>();
52
53 html! { <><style>{ ":host{opacity:0!important;}" }</style>{ inner }</> }
54 }
55}
56
57#[derive(Clone, Copy)]
60pub enum FontLoaderStatus {
61 Uninitialized,
62 Loading,
63 Finished,
64}
65
66struct FontLoaderState {
67 status: Cell<FontLoaderStatus>,
68 elem: web_sys::HtmlElement,
69 on_update: Callback<()>,
70 fonts: RefCell<Vec<(String, String)>>,
71}
72
73type PromiseSet = Vec<ApiFuture<JsValue>>;
74
75impl FontLoaderProps {
76 pub fn new(elem: &web_sys::HtmlElement, on_update: Callback<()>) -> Self {
77 let inner = FontLoaderState {
78 status: Cell::new(FontLoaderStatus::Uninitialized),
79 elem: elem.clone(),
80 on_update,
81 fonts: RefCell::new(vec![]),
82 };
83
84 let state = yew::props!(Self {
85 state: Rc::new(inner)
86 });
87
88 ApiFuture::spawn(state.clone().load_fonts_task_safe());
89 state
90 }
91
92 pub fn get_status(&self) -> FontLoaderStatus {
93 self.state.status.get()
94 }
95
96 fn get_fonts(&'_ self) -> Ref<'_, Vec<(String, String)>> {
97 self.state.fonts.borrow()
98 }
99
100 async fn load_fonts_task_safe(self) -> ApiResult<JsValue> {
101 if let Err(msg) = self.load_fonts_task().await {
102 web_sys::console::warn_1(&msg.into());
103 };
104
105 Ok(JsValue::UNDEFINED)
106 }
107
108 async fn load_fonts_task(self) -> ApiResult<JsValue> {
109 await_dom_loaded().await?;
110 let txt = global::window()
111 .get_computed_style(&self.state.elem)?
112 .unwrap()
113 .get_property_value("--preload-fonts")?;
114
115 let mut block_promises: PromiseSet = vec![];
116 let preload_fonts = parse_fonts(&txt);
117 self.state.fonts.borrow_mut().clone_from(&preload_fonts);
118 self.state.status.set(FontLoaderStatus::Loading);
119 self.state.on_update.emit(());
120
121 for (family, weight) in preload_fonts.iter() {
122 let task = timeout_font_task(family, weight);
123 let mut block_fonts: PromiseSet = vec![ApiFuture::new(task)];
124
125 for entry in font_iter(global::document().fonts().values()) {
126 let font_face = js_sys::Reflect::get(&entry, &intern("value").into())?
127 .dyn_into::<web_sys::FontFace>()?;
128
129 if family == &font_face.family().replace('"', "")
130 && (weight == &font_face.weight()
131 || (font_face.weight() == "normal" && weight == "400"))
132 {
133 block_fonts.push(ApiFuture::new(async move {
134 Ok(JsFuture::from(font_face.loaded()?).await?)
135 }));
136 }
137 }
138
139 let fut = async { select_all(block_fonts).await.0 };
140 block_promises.push(ApiFuture::new(fut))
141 }
142
143 if block_promises.len() != preload_fonts.len() {
144 web_sys::console::warn_1(&format!("Missing preload fonts {:?}", preload_fonts).into());
145 }
146
147 let res = join_all(block_promises)
148 .await
149 .into_iter()
150 .collect::<ApiResult<Vec<JsValue>>>()
151 .map(|_| JsValue::UNDEFINED);
152
153 self.state.status.set(FontLoaderStatus::Finished);
154 self.state.on_update.emit(());
155 res
156 }
157}
158
159fn timeout_font_task(
160 family: &str,
161 weight: &str,
162) -> impl Future<Output = ApiResult<JsValue>> + use<> {
163 let timeout_msg = format!("Timeout awaiting font \"{family}:{weight}\"");
164 async {
165 set_timeout(FONT_DOWNLOAD_TIMEOUT_MS).await?;
166 Err(timeout_msg.into())
167 }
168}
169
170fn font_test_html((family, weight): &(String, String)) -> Html {
171 let style = format!("opacity:0;font-family:\"{family}\";font-weight:{weight}");
172
173 html! { <span {style}>{ FONT_TEST_SAMPLE }</span> }
174}
175
176fn parse_font(txt: &str) -> Option<Vec<(String, String)>> {
177 match *txt.trim().split(':').collect::<Vec<_>>().as_slice() {
178 [family, weights] => Some(
179 weights
180 .split(',')
181 .map(|weight| (family.to_owned(), weight.to_owned()))
182 .collect::<Vec<_>>(),
183 ),
184 _ => None,
185 }
186}
187
188fn parse_fonts(txt: &str) -> Vec<(String, String)> {
189 let trim = txt.trim();
190 let trim = if trim.len() > 2 {
191 &trim[1..trim.len() - 1]
192 } else {
193 trim
194 };
195
196 trim.split(';')
197 .filter_map(parse_font)
198 .flatten()
199 .collect::<Vec<_>>()
200}
201
202fn font_iter(
203 iter: web_sys::FontFaceSetIterator,
204) -> impl Iterator<Item = web_sys::FontFaceSetIteratorResult> {
205 repeat_with(move || iter.next())
206 .filter_map(|x| x.ok())
207 .take_while(|entry| {
208 !js_sys::Reflect::get(entry, &intern("done").into())
209 .unwrap()
210 .as_bool()
211 .unwrap()
212 })
213}