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
50pub struct FontLoader {}
53
54impl Component for FontLoader {
55 type Message = ();
56 type Properties = FontLoaderProps;
57
58 fn create(_ctx: &Context<Self>) -> Self {
59 Self {}
60 }
61
62 fn update(&mut self, _ctx: &Context<Self>, _msg: ()) -> bool {
63 false
64 }
65
66 fn view(&self, ctx: &Context<Self>) -> yew::virtual_dom::VNode {
67 if matches!(ctx.props().get_status(), FontLoaderStatus::Finished) {
68 html! {}
69 } else {
70 let inner = ctx
71 .props()
72 .get_fonts()
73 .iter()
74 .map(font_test_html)
75 .collect::<Html>();
76
77 html! { <><style>{ ":host{opacity:0!important;}" }</style>{ inner }</> }
78 }
79 }
80}
81
82#[derive(Clone, Copy)]
85pub enum FontLoaderStatus {
86 Uninitialized,
87 Loading,
88 Finished,
89}
90
91struct FontLoaderState {
92 status: Cell<FontLoaderStatus>,
93 elem: web_sys::HtmlElement,
94 on_update: Callback<()>,
95 fonts: RefCell<Vec<(String, String)>>,
96}
97
98type PromiseSet = Vec<ApiFuture<JsValue>>;
99
100impl FontLoaderProps {
101 pub fn new(elem: &web_sys::HtmlElement, on_update: Callback<()>) -> Self {
102 let inner = FontLoaderState {
103 status: Cell::new(FontLoaderStatus::Uninitialized),
104 elem: elem.clone(),
105 on_update,
106 fonts: RefCell::new(vec![]),
107 };
108
109 let state = yew::props!(Self {
110 state: Rc::new(inner)
111 });
112
113 ApiFuture::spawn(state.clone().load_fonts_task_safe());
114 state
115 }
116
117 pub fn get_status(&self) -> FontLoaderStatus {
118 self.state.status.get()
119 }
120
121 fn get_fonts(&'_ self) -> Ref<'_, Vec<(String, String)>> {
122 self.state.fonts.borrow()
123 }
124
125 async fn load_fonts_task_safe(self) -> ApiResult<JsValue> {
128 if let Err(msg) = self.load_fonts_task().await {
129 web_sys::console::warn_1(&msg.into());
130 };
131
132 Ok(JsValue::UNDEFINED)
133 }
134
135 async fn load_fonts_task(self) -> ApiResult<JsValue> {
143 await_dom_loaded().await?;
144 let txt = global::window()
145 .get_computed_style(&self.state.elem)?
146 .unwrap()
147 .get_property_value("--preload-fonts")?;
148
149 let mut block_promises: PromiseSet = vec![];
150 let preload_fonts = parse_fonts(&txt);
151 self.state.fonts.borrow_mut().clone_from(&preload_fonts);
152 self.state.status.set(FontLoaderStatus::Loading);
153 self.state.on_update.emit(());
154
155 for (family, weight) in preload_fonts.iter() {
156 let task = timeout_font_task(family, weight);
157 let mut block_fonts: PromiseSet = vec![ApiFuture::new(task)];
158
159 for entry in font_iter(global::document().fonts().values()) {
160 let font_face = js_sys::Reflect::get(&entry, &intern("value").into())?
161 .dyn_into::<web_sys::FontFace>()?;
162
163 if family == &font_face.family().replace('"', "")
165 && (weight == &font_face.weight()
166 || (font_face.weight() == "normal" && weight == "400"))
167 {
168 block_fonts.push(ApiFuture::new(async move {
169 Ok(JsFuture::from(font_face.loaded()?).await?)
170 }));
171 }
172 }
173
174 let fut = async { select_all(block_fonts.into_iter()).await.0 };
175 block_promises.push(ApiFuture::new(fut))
176 }
177
178 if block_promises.len() != preload_fonts.len() {
179 web_sys::console::warn_1(&format!("Missing preload fonts {:?}", &preload_fonts).into());
180 }
181
182 let res = join_all(block_promises)
183 .await
184 .into_iter()
185 .collect::<ApiResult<Vec<JsValue>>>()
186 .map(|_| JsValue::UNDEFINED);
187
188 self.state.status.set(FontLoaderStatus::Finished);
189 self.state.on_update.emit(());
190 res
191 }
192}
193
194fn timeout_font_task(
197 family: &str,
198 weight: &str,
199) -> impl Future<Output = ApiResult<JsValue>> + use<> {
200 let timeout_msg = format!("Timeout awaiting font \"{family}:{weight}\"");
201 async {
202 set_timeout(FONT_DOWNLOAD_TIMEOUT_MS).await?;
203 Err(timeout_msg.into())
204 }
205}
206
207fn font_test_html((family, weight): &(String, String)) -> Html {
211 let style = format!("opacity:0;font-family:\"{family}\";font-weight:{weight}");
212
213 html! { <span {style}>{ FONT_TEST_SAMPLE }</span> }
214}
215
216fn parse_font(txt: &str) -> Option<Vec<(String, String)>> {
217 match *txt.trim().split(':').collect::<Vec<_>>().as_slice() {
218 [family, weights] => Some(
219 weights
220 .split(',')
221 .map(|weight| (family.to_owned(), weight.to_owned()))
222 .collect::<Vec<_>>(),
223 ),
224 _ => None,
225 }
226}
227
228fn parse_fonts(txt: &str) -> Vec<(String, String)> {
230 let trim = txt.trim();
231 let trim = if trim.len() > 2 {
232 &trim[1..trim.len() - 1]
233 } else {
234 trim
235 };
236
237 trim.split(';')
238 .filter_map(parse_font)
239 .flatten()
240 .collect::<Vec<_>>()
241}
242
243fn font_iter(
246 iter: web_sys::FontFaceSetIterator,
247) -> impl Iterator<Item = web_sys::FontFaceSetIteratorResult> {
248 repeat_with(move || iter.next())
249 .filter_map(|x| x.ok())
250 .take_while(|entry| {
251 !js_sys::Reflect::get(entry, &intern("done").into())
252 .unwrap()
253 .as_bool()
254 .unwrap()
255 })
256}