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