script/dom/css/fontfaceset.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::RefCell;
6use std::rc::Rc;
7
8use cssparser::{Parser, ParserInput, UnicodeRange};
9use dom_struct::dom_struct;
10use fonts::FontFaceRuleInfo;
11use js::context::JSContext;
12use js::gc::Handle;
13use js::jsapi::Value;
14use js::realm::CurrentRealm;
15use js::rust::HandleObject;
16use layout_api::{QueryMsg, ReflowGoal};
17use script_bindings::cell::DomRefCell;
18use script_bindings::codegen::GenericBindings::FontFaceBinding::{
19 FontFaceLoadStatus, FontFaceMethods,
20};
21use script_bindings::like::Setlike;
22use script_bindings::reflector::reflect_dom_object_with_proto;
23use servo_arc::Arc as ServoArc;
24use style::font_face::FamilyName;
25use style::properties::shorthands::font;
26use style::stylesheets::CssRuleType;
27use style::values::computed::font::{FontFamilyList, SingleFontFamily};
28use style::values::specified::font as specified_font;
29use style_traits::ParsingMode;
30
31use crate::css::css::parser_context_for_document;
32use crate::dom::bindings::codegen::Bindings::FontFaceSetBinding::FontFaceSetMethods;
33use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
34use crate::dom::bindings::error::{Error, Fallible};
35use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
36use crate::dom::bindings::reflector::DomGlobal;
37use crate::dom::bindings::root::{Dom, DomRoot};
38use crate::dom::bindings::str::DOMString;
39use crate::dom::document::Document;
40use crate::dom::eventtarget::EventTarget;
41use crate::dom::fontface::FontFace;
42use crate::dom::globalscope::GlobalScope;
43use crate::dom::promise::Promise;
44use crate::dom::promisenativehandler::Callback;
45use crate::dom::types::PromiseNativeHandler;
46use crate::dom::window::Window;
47use crate::realms::enter_auto_realm;
48
49/// <https://drafts.csswg.org/css-font-loading/#FontFaceSet-interface>
50#[dom_struct]
51pub(crate) struct FontFaceSet {
52 target: EventTarget,
53
54 /// <https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-readypromise-slot>
55 #[conditional_malloc_size_of]
56 promise: RefCell<Rc<Promise>>,
57
58 set_entries: DomRefCell<Vec<Dom<FontFace>>>,
59}
60
61impl FontFaceSet {
62 fn new_inherited(promise: Rc<Promise>) -> Self {
63 FontFaceSet {
64 target: EventTarget::new_inherited(),
65 promise: promise.into(),
66 set_entries: Default::default(),
67 }
68 }
69
70 pub(crate) fn new(
71 cx: &mut JSContext,
72 global: &GlobalScope,
73 proto: Option<HandleObject>,
74 ) -> DomRoot<Self> {
75 let promise = Promise::new(cx, global);
76 reflect_dom_object_with_proto(
77 cx,
78 Box::new(FontFaceSet::new_inherited(promise)),
79 global,
80 proto,
81 )
82 }
83
84 pub(super) fn handle_font_face_status_changed(&self, cx: &mut JSContext, font_face: &FontFace) {
85 match font_face.Status() {
86 FontFaceLoadStatus::Loading => {
87 self.switch_to_loading(cx);
88 },
89 FontFaceLoadStatus::Loaded => {
90 let Some(window) = DomRoot::downcast::<Window>(self.global()) else {
91 return;
92 };
93
94 let (family_name, template) = font_face
95 .template()
96 .expect("A loaded web font should have a template");
97 window
98 .font_context()
99 .add_template_to_font_context(family_name, template);
100 window.Document().dirty_all_nodes(cx.no_gc());
101 },
102 _ => {},
103 }
104 }
105
106 /// Fulfill the font ready promise, returning true if it was not already fulfilled beforehand.
107 pub(crate) fn fulfill_ready_promise_if_needed(&self, cx: &mut JSContext) -> bool {
108 let promise = self.promise.borrow().clone();
109 if promise.is_fulfilled() {
110 return false;
111 }
112 promise.resolve_native(cx, self);
113 true
114 }
115
116 pub(crate) fn waiting_to_fullfill_promise(&self) -> bool {
117 !self.promise.borrow().is_fulfilled()
118 }
119
120 fn contains_face(&self, target: &FontFace) -> bool {
121 self.set_entries
122 .borrow()
123 .iter()
124 .any(|face| &**face == target)
125 }
126
127 /// Removes a face from the set's set entries.
128 fn delete_face(&self, target: &FontFace) -> bool {
129 let mut set_entries = self.set_entries.borrow_mut();
130 let Some(index) = set_entries.iter().position(|face| &**face == target) else {
131 return false;
132 };
133 set_entries.remove(index);
134 true
135 }
136
137 /// <https://drafts.csswg.org/css-font-loading/#switch-the-fontfaceset-to-loading>
138 pub(crate) fn switch_to_loading(&self, cx: &mut JSContext) {
139 // Step 1. Let font face set be the given FontFaceSet.
140 // Note: This is self.
141
142 // Step 2. Set the status attribute of font face set to "loading".
143 // TODO: Implement the FontFaceSet status attribute.
144
145 // Step 3. If font face set’s [[ReadyPromise]] slot currently holds a fulfilled
146 // promise, replace it with a fresh pending promise.
147 if self.promise.borrow().is_fulfilled() {
148 *self.promise.borrow_mut() = Promise::new(cx, &self.global());
149 }
150
151 // Step 4. Queue a task to fire a font load event named loading at font face set.
152 // TODO: Implement support for font loading events.
153 }
154
155 /// Runs the CSS cascade to ensure that new `@font-face` rules have
156 /// an entry in this set.
157 fn flush_author_font_set(&self, cx: &mut JSContext) {
158 // FIXME: Use a new sort of ReflowGoal that only runs the CSS cascade without
159 // building a new box tree or running any sort of layout really.
160 // We query for the box area here, but we're not interested in the result.
161 // FIXME: Figure out what to do for worker scopes.
162 if let Some(window) = DomRoot::downcast::<Window>(self.global()) {
163 let document = window.Document();
164 if document.stylesheets_changed_since_last_reflow() {
165 window.reflow(cx, ReflowGoal::LayoutQuery(QueryMsg::BoxArea));
166 }
167 }
168 }
169
170 /// Marks the entries corresponding to removed `@font-face` rules as not [css-connected].
171 ///
172 /// [css-connected]: https://drafts.csswg.org/css-font-loading/#css-connected
173 pub(crate) fn notify_font_face_rules_removed(
174 &self,
175 removed_font_face_rules: &[ServoArc<FontFaceRuleInfo>],
176 ) {
177 let entries = self.set_entries.borrow_mut();
178 for removed_font_face_rule in removed_font_face_rules {
179 let Some(matching_font_face_object) = entries
180 .iter()
181 .find(|entry| entry.is_connected_to_font_face_rule(removed_font_face_rule))
182 else {
183 if cfg!(debug_assertions) {
184 unreachable!("Removed @font-face that was not previously present");
185 }
186 log::warn!("Removed @font-face that was not previously present");
187 continue;
188 };
189
190 // https://drafts.csswg.org/css-font-loading/#font-face-css-connection:
191 // > If a @font-face rule is removed from the document, its corresponding FontFace object is no longer CSS-connected.
192 // > The connection is not restorable by any means.
193 matching_font_face_object.disconnect_from_css();
194 }
195 }
196
197 /// Uses the font matching rules to select font faces within `self` that can be used to
198 /// render the provided text.
199 ///
200 /// This is used to implement Step 6 of
201 /// <https://drafts.csswg.org/css-font-loading/#find-the-matching-font-faces>.
202 fn query_fonts(&self, target_family: &FamilyName, sample_text: &str) -> Vec<DomRoot<FontFace>> {
203 let mut matching_fonts = Vec::default();
204 for font in self.set_entries.borrow().iter() {
205 let font_face_rule = font.css_font_face_rule();
206 let Some(font_face_rule) = font_face_rule.as_ref() else {
207 // FIXME: Don't ignore font faces that are not css-connected here.
208 continue;
209 };
210
211 if font_face_rule
212 .descriptors
213 .font_family
214 .as_ref()
215 .is_none_or(|family| family != target_family)
216 {
217 continue;
218 }
219
220 if font_face_rule
221 .descriptors
222 .unicode_range
223 .as_ref()
224 .is_some_and(|ranges| !any_character_in_any_unicode_range(sample_text, ranges))
225 {
226 continue;
227 }
228
229 // FIXME: Check other fields (weight, style, ...) here too. We need to investigate what other
230 // browsers are doing, because at this point the font isn't actually loaded yet,
231 // so the full descriptor is not available.
232 matching_fonts.push(font.as_rooted());
233 }
234
235 matching_fonts
236 }
237
238 /// <https://drafts.csswg.org/css-font-loading/#find-the-matching-font-faces>
239 fn find_the_matching_font_faces(
240 &self,
241 document: &Document,
242 font: &str,
243 sample_text: &str,
244 ) -> Result<Vec<DomRoot<FontFace>>, FontQuerySyntaxError> {
245 // Step 1. (Parse "font") and Step 2. (Unpack font shorthand) are implemented
246 // in FontQueryParameters::parse.
247 let parameters = FontQueryParameters::parse(document, font)?;
248
249 // Step 2. If text was not explicitly provided, let it be a string containing a
250 // single space character (U+0020 SPACE).
251 // Note: "text" is not optional in our implementation yet.
252
253 // Step 4. Let available font faces be the available font faces within source.
254 // If the allow system fonts flag is specified, add all system fonts to available font faces.
255
256 // Step 5. Let matched font faces initially be an empty list.
257 let mut matched_faces = vec![];
258
259 // Step 6. For each family in font family list, use the font matching rules to select the font faces
260 // from available font faces that match the font style, and add them to matched font faces.
261 // The use of the unicodeRange attribute means that this may be more than just a single font face.
262 // Step 7. If matched font faces is empty, set the found faces flag to false. Otherwise, set it to true.
263 // Note We don't need this yet.
264 // Step 8. For each font face in matched font faces, if its defined unicode-range does not include the
265 // codepoint of at least one character in text, remove it from the list.
266 for family in parameters.families.list.iter() {
267 let SingleFontFamily::FamilyName(target_family) = family else {
268 continue; // Skip generic font faces
269 };
270
271 matched_faces.extend_from_slice(&self.query_fonts(target_family, sample_text));
272 }
273
274 // Step 9. Return matched font faces and the found faces flag.
275 Ok(matched_faces)
276 }
277}
278
279impl FontFaceSetMethods<crate::DomTypeHolder> for FontFaceSet {
280 /// <https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-ready>
281 fn Ready(&self, cx: &mut JSContext) -> Rc<Promise> {
282 if self.promise.borrow().is_fulfilled() {
283 // There may be pending style changes that cause new web fonts to start loading,
284 // re-initializing document.fonts.ready.
285 self.flush_author_font_set(cx);
286 }
287 self.promise.borrow().clone()
288 }
289
290 /// <https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-add>
291 fn Add(&self, cx: &mut JSContext, font_face: &FontFace) -> Fallible<DomRoot<FontFaceSet>> {
292 // Step 1. If font is already in the FontFaceSet’s set entries,
293 // skip to the last step of this algorithm immediately.
294 if self.contains_face(font_face) {
295 return Ok(DomRoot::from_ref(self));
296 }
297
298 // Step 2. If font is CSS-connected, throw an InvalidModificationError
299 // exception and exit this algorithm immediately.
300 if font_face.is_css_connected() {
301 return Err(Error::InvalidModification(Some(
302 "Cannot add CSS-connected FontFace to FontFaceSet".to_owned(),
303 )));
304 }
305
306 // Step 3. Add the font argument to the FontFaceSet’s set entries.
307 self.set_entries.borrow_mut().push(Dom::from_ref(font_face));
308 font_face.set_associated_font_face_set(self);
309
310 // Step 4. If font’s status attribute is "loading":
311 // Step 4.1 If the FontFaceSet’s [[LoadingFonts]] list is empty, switch the FontFaceSet to loading.
312 // Step 4.2 Append font to the FontFaceSet’s [[LoadingFonts]] list.
313 self.handle_font_face_status_changed(cx, font_face);
314
315 // Step 5. Return the FontFaceSet.
316 Ok(DomRoot::from_ref(self))
317 }
318
319 /// <https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-delete>
320 fn Delete(&self, to_delete: &FontFace) -> bool {
321 // Step 1. If font is CSS-connected, return false and exit this algorithm immediately.
322 if to_delete.is_css_connected() {
323 return false;
324 }
325
326 // Step 2. Let deleted be the result of removing font from the FontFaceSet’s set entries.
327 // TODO: Step 3. If font is present in the FontFaceSet’s [[LoadedFonts]], or [[FailedFonts]] lists, remove it.
328 // TODO: Step 4. If font is present in the FontFaceSet’s [[LoadingFonts]] list, remove it. If font was the last
329 // item in that list (and so the list is now empty), switch the FontFaceSet to loaded.
330 // Step 5. Return deleted.
331 self.delete_face(to_delete)
332 }
333
334 /// <https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-clear>
335 fn Clear(&self, cx: &mut JSContext) {
336 self.flush_author_font_set(cx);
337
338 // Step 1. Remove all non-CSS-connected items from the FontFaceSet’s set entries,
339 // its [[LoadedFonts]] list, and its [[FailedFonts]] list.
340 self.set_entries.borrow_mut().clear();
341
342 // TODO Step 2. If the FontFaceSet’s [[LoadingFonts]] list is non-empty, remove all items from it,
343 // then switch the FontFaceSet to loaded.
344 }
345
346 /// <https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-load>
347 fn Load(&self, cx: &mut JSContext, font: DOMString, text: DOMString) -> Rc<Promise> {
348 // Step 1. Let font face set be the FontFaceSet object this method was called on. Let
349 // promise be a newly-created promise object.
350 let load_promise = Promise::new(cx, &self.global());
351
352 // Step 2. Return promise. Complete the rest of these steps asynchronously.
353 #[derive(MallocSizeOf, JSTraceable)]
354 #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
355 struct LoadPromiseFulfillmentHandler {
356 /// The font faces that this should wait on.
357 ///
358 /// (Our current implementation waits for `document.fonts.ready` instead)
359 font_face_objects: Vec<Dom<FontFace>>,
360
361 #[conditional_malloc_size_of]
362 load_promise: Rc<Promise>,
363 }
364 impl Callback for LoadPromiseFulfillmentHandler {
365 fn callback(&self, cx: &mut CurrentRealm, _: Handle<Value>) {
366 let font_face_objects: Vec<DomRoot<FontFace>> = self
367 .font_face_objects
368 .iter()
369 .map(|font_face| font_face.as_rooted())
370 .collect();
371 self.load_promise.resolve_native(cx, &font_face_objects);
372 }
373 }
374
375 // Step 4. Queue a task to run the following steps synchronously:
376 let trusted_this = Trusted::new(self);
377 let trusted_load_promise = TrustedPromise::new(load_promise.clone());
378 let font = font.to_string();
379 let text = text.to_string();
380 self.global()
381 .task_manager()
382 .font_loading_task_source()
383 .queue(task!(resolve_font_face_set_load_task: move |cx| {
384 let load_promise = trusted_load_promise.root();
385 let this = trusted_this.root();
386
387 // This will need adjustments once FontFaceSet is exposed to workers.
388 let Some(window) = DomRoot::downcast::<Window>(this.global()) else {
389 log::error!("FontFaceSet should not be exposed to non-window globals");
390 return;
391 };
392 let document = window.Document();
393
394 // Step 3. Find the matching font faces from font face set using the font and text
395 // arguments passed to the function, and let font face list be the return value (ignoring
396 // the found faces flag). If a syntax error was returned, reject promise with a SyntaxError
397 // exception and terminate these steps.
398 let Ok(font_face_objects) = this.find_the_matching_font_faces(&document, &font, &text) else {
399 load_promise.reject_error(cx, Error::Syntax(Some("Failed to parse font query".into())));
400 return;
401 };
402
403 // Step 4.1. For all of the font faces in the font face list, call their load()
404 // method.
405 // Step 4.2. Resolve promise with the result of waiting for all of the
406 // [[FontStatusPromise]]s of each font face in the font face list, in order.
407 //
408 // TODO: These steps are not implemented. Instead we wait until all fonts
409 // are loaded by resolving the returned promise when
410 // `document.fonts.ready` is resolved. The return list of fonts will not
411 // be correct, but any code that waits on the promise will have
412 // conservatively consistent behavior. This is important for preventing
413 // intermittent results in WPT tests.
414 let global = this.global();
415 let handler = PromiseNativeHandler::new(
416 cx,
417 &global,
418 Some(Box::new(LoadPromiseFulfillmentHandler {
419 font_face_objects: font_face_objects.into_iter().map(|font_face| font_face.as_traced()).collect(),
420 load_promise,
421 })),
422 None,
423 );
424
425 let ready_promise = this.Ready(cx);
426 let mut realm = enter_auto_realm(cx, &*global);
427 ready_promise.append_native_handler(&mut realm.current_realm(), &handler);
428 }));
429
430 // Step 2. Return promise. Complete the rest of these steps asynchronously.
431 load_promise
432 }
433
434 /// <https://html.spec.whatwg.org/multipage/#customstateset>
435 fn Size(&self, cx: &mut JSContext) -> u32 {
436 self.size(cx)
437 }
438}
439
440impl Setlike for FontFaceSet {
441 type Key = DomRoot<FontFace>;
442
443 #[inline(always)]
444 fn get_index(&self, cx: &mut JSContext, index: u32) -> Option<Self::Key> {
445 self.flush_author_font_set(cx);
446 self.set_entries
447 .borrow()
448 .get(index as usize)
449 .map(|face| face.as_rooted())
450 }
451
452 #[inline(always)]
453 fn size(&self, cx: &mut JSContext) -> u32 {
454 self.flush_author_font_set(cx);
455 self.set_entries.borrow().len() as u32
456 }
457
458 #[inline(always)]
459 fn add(&self, _cx: &mut JSContext, face: Self::Key) {
460 self.set_entries.borrow_mut().push(face.as_traced());
461 }
462
463 #[inline(always)]
464 fn has(&self, cx: &mut JSContext, target: Self::Key) -> bool {
465 self.flush_author_font_set(cx);
466 self.contains_face(&target)
467 }
468
469 #[inline(always)]
470 fn clear(&self, cx: &mut JSContext) {
471 self.flush_author_font_set(cx);
472 self.set_entries.borrow_mut().clear();
473 }
474
475 #[inline(always)]
476 fn delete(&self, cx: &mut JSContext, to_delete: Self::Key) -> bool {
477 self.flush_author_font_set(cx);
478 self.delete_face(&to_delete)
479 }
480}
481
482/// Represents a parsed query for [`FontFaceSet::load`] and [`FontFaceSet::check`].
483///
484/// [`FontFaceSet::load`]: https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-load
485/// [`FontFaceSet::check`]: https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-check
486struct FontQueryParameters {
487 families: FontFamilyList,
488 // TODO: Store a font descriptor here once we actually use that for matching.
489}
490
491/// Returned from <https://drafts.csswg.org/css-font-loading/#find-the-matching-font-faces> to indicate failure.
492struct FontQuerySyntaxError;
493
494impl FontQueryParameters {
495 /// Implements Steps 1 and 3 of <https://drafts.csswg.org/css-font-loading/#find-the-matching-font-faces>.
496 fn parse(document: &Document, font: &str) -> Result<Self, FontQuerySyntaxError> {
497 // Step 1. Parse font using the CSS value syntax of the font property.
498 // If a syntax error occurs, return a syntax error.
499 // If the parsed value is a CSS-wide keyword, return a syntax error.
500 // Absolutize all relative lengths against the initial values of the corresponding properties.
501 // (For example, a relative font weight like bolder is evaluated against the initial value normal.)
502 // Step 3. Let font family list be the list of font families parsed from font,
503 // and font style be the other font style attributes parsed from font.
504 let font_family;
505
506 let urlextradata = document.url().into_url().into();
507 let parser_context = parser_context_for_document(
508 document,
509 CssRuleType::FontFace,
510 ParsingMode::DEFAULT,
511 &urlextradata,
512 );
513
514 let mut input = ParserInput::new(font);
515 let mut parser = Parser::new(&mut input);
516 let Ok(font_shorthand) =
517 parser.parse_entirely(|parser| font::parse_value(&parser_context, parser))
518 else {
519 return Err(FontQuerySyntaxError);
520 };
521
522 match font_shorthand.font_family {
523 specified_font::FontFamily::Values(family_list) => font_family = family_list,
524 specified_font::FontFamily::System(_) => return Err(FontQuerySyntaxError),
525 }
526
527 Ok(Self {
528 families: font_family,
529 })
530 }
531}
532
533fn any_character_in_any_unicode_range(text: &str, unicode_ranges: &[UnicodeRange]) -> bool {
534 for character in text.chars() {
535 for unicode_range in unicode_ranges {
536 if (unicode_range.start..=unicode_range.end).contains(&(character as u32)) {
537 return true;
538 }
539 }
540 }
541 false
542}