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