Skip to main content

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