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 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}