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 dom_struct::dom_struct;
9use fonts::FontFaceRuleWithOrigin;
10use js::context::JSContext;
11use js::gc::Handle;
12use js::jsapi::Value;
13use js::realm::CurrentRealm;
14use js::rust::HandleObject;
15use layout_api::{QueryMsg, ReflowGoal};
16use script_bindings::cell::DomRefCell;
17use script_bindings::codegen::GenericBindings::FontFaceBinding::{
18    FontFaceLoadStatus, FontFaceMethods,
19};
20use script_bindings::like::Setlike;
21use script_bindings::reflector::reflect_dom_object_with_proto;
22
23use crate::dom::bindings::codegen::Bindings::FontFaceSetBinding::FontFaceSetMethods;
24use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
25use crate::dom::bindings::error::{Error, Fallible};
26use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
27use crate::dom::bindings::reflector::DomGlobal;
28use crate::dom::bindings::root::{Dom, DomRoot};
29use crate::dom::bindings::str::DOMString;
30use crate::dom::eventtarget::EventTarget;
31use crate::dom::fontface::FontFace;
32use crate::dom::globalscope::GlobalScope;
33use crate::dom::promise::Promise;
34use crate::dom::promisenativehandler::Callback;
35use crate::dom::types::PromiseNativeHandler;
36use crate::dom::window::Window;
37use crate::realms::enter_auto_realm;
38
39/// <https://drafts.csswg.org/css-font-loading/#FontFaceSet-interface>
40#[dom_struct]
41pub(crate) struct FontFaceSet {
42    target: EventTarget,
43
44    /// <https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-readypromise-slot>
45    #[conditional_malloc_size_of]
46    promise: RefCell<Rc<Promise>>,
47
48    set_entries: DomRefCell<Vec<Dom<FontFace>>>,
49}
50
51impl FontFaceSet {
52    fn new_inherited(promise: Rc<Promise>) -> Self {
53        FontFaceSet {
54            target: EventTarget::new_inherited(),
55            promise: promise.into(),
56            set_entries: Default::default(),
57        }
58    }
59
60    pub(crate) fn new(
61        cx: &mut JSContext,
62        global: &GlobalScope,
63        proto: Option<HandleObject>,
64    ) -> DomRoot<Self> {
65        let promise = Promise::new(cx, global);
66        reflect_dom_object_with_proto(
67            cx,
68            Box::new(FontFaceSet::new_inherited(promise)),
69            global,
70            proto,
71        )
72    }
73
74    pub(super) fn handle_font_face_status_changed(&self, cx: &mut JSContext, font_face: &FontFace) {
75        match font_face.Status() {
76            FontFaceLoadStatus::Loading => {
77                self.switch_to_loading(cx);
78            },
79            FontFaceLoadStatus::Loaded => {
80                let Some(window) = DomRoot::downcast::<Window>(self.global()) else {
81                    return;
82                };
83
84                let (family_name, template) = font_face
85                    .template()
86                    .expect("A loaded web font should have a template");
87                window
88                    .font_context()
89                    .add_template_to_font_context(family_name, template);
90                window.Document().dirty_all_nodes(cx.no_gc());
91            },
92            _ => {},
93        }
94    }
95
96    /// Fulfill the font ready promise, returning true if it was not already fulfilled beforehand.
97    pub(crate) fn fulfill_ready_promise_if_needed(&self, cx: &mut JSContext) -> bool {
98        let promise = self.promise.borrow().clone();
99        if promise.is_fulfilled() {
100            return false;
101        }
102        promise.resolve_native(cx, self);
103        true
104    }
105
106    pub(crate) fn waiting_to_fullfill_promise(&self) -> bool {
107        !self.promise.borrow().is_fulfilled()
108    }
109
110    fn contains_face(&self, target: &FontFace) -> bool {
111        self.set_entries
112            .borrow()
113            .iter()
114            .any(|face| &**face == target)
115    }
116
117    /// Removes a face from the set's set entries.
118    fn delete_face(&self, target: &FontFace) -> bool {
119        let mut set_entries = self.set_entries.borrow_mut();
120        let Some(index) = set_entries.iter().position(|face| &**face == target) else {
121            return false;
122        };
123        set_entries.remove(index);
124        true
125    }
126
127    /// <https://drafts.csswg.org/css-font-loading/#switch-the-fontfaceset-to-loading>
128    pub(crate) fn switch_to_loading(&self, cx: &mut JSContext) {
129        // Step 1. Let font face set be the given FontFaceSet.
130        // Note: This is self.
131
132        // Step 2. Set the status attribute of font face set to "loading".
133        // TODO: Implement the FontFaceSet status attribute.
134
135        // Step 3. If font face set’s [[ReadyPromise]] slot currently holds a fulfilled
136        // promise, replace it with a fresh pending promise.
137        if self.promise.borrow().is_fulfilled() {
138            *self.promise.borrow_mut() = Promise::new(cx, &self.global());
139        }
140
141        // Step 4. Queue a task to fire a font load event named loading at font face set.
142        // TODO: Implement support for font loading events.
143    }
144
145    /// Runs the CSS cascade to ensure that new `@font-face` rules have
146    /// an entry in this set.
147    fn flush_author_font_set(&self, cx: &mut JSContext) {
148        // FIXME: Use a new sort of ReflowGoal that only runs the CSS cascade without
149        //        building a new box tree or running any sort of layout really.
150        //        We query for the box area here, but we're not interested in the result.
151        // FIXME: Figure out what to do for worker scopes.
152        if let Some(window) = DomRoot::downcast::<Window>(self.global()) {
153            let document = window.Document();
154            if document.stylesheets_changed_since_last_reflow() {
155                window.reflow(cx, ReflowGoal::LayoutQuery(QueryMsg::BoxArea));
156            }
157        }
158    }
159
160    /// Marks the entries corresponding to removed `@font-face` rules as not [css-connected].
161    ///
162    /// [css-connected]: https://drafts.csswg.org/css-font-loading/#css-connected
163    pub(crate) fn notify_font_face_rules_removed(
164        &self,
165        removed_font_face_rules: &[FontFaceRuleWithOrigin],
166    ) {
167        let entries = self.set_entries.borrow_mut();
168        for removed_font_face_rule in removed_font_face_rules {
169            let Some(matching_font_face_object) = entries
170                .iter()
171                .find(|entry| entry.is_connected_to_font_face_rule(removed_font_face_rule))
172            else {
173                if cfg!(debug_assertions) {
174                    unreachable!("Removed @font-face that was not previously present");
175                }
176                log::warn!("Removed @font-face that was not previously present");
177                continue;
178            };
179
180            // https://drafts.csswg.org/css-font-loading/#font-face-css-connection:
181            // > If a @font-face rule is removed from the document, its corresponding FontFace object is no longer CSS-connected.
182            // > The connection is not restorable by any means.
183            matching_font_face_object.disconnect_from_css();
184        }
185    }
186}
187
188impl FontFaceSetMethods<crate::DomTypeHolder> for FontFaceSet {
189    /// <https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-ready>
190    fn Ready(&self, cx: &mut JSContext) -> Rc<Promise> {
191        if self.promise.borrow().is_fulfilled() {
192            // There may be pending style changes that cause new web fonts to start loading,
193            // re-initializing document.fonts.ready.
194            self.flush_author_font_set(cx);
195        }
196        self.promise.borrow().clone()
197    }
198
199    /// <https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-add>
200    fn Add(&self, cx: &mut JSContext, font_face: &FontFace) -> Fallible<DomRoot<FontFaceSet>> {
201        // Step 1. If font is already in the FontFaceSet’s set entries,
202        // skip to the last step of this algorithm immediately.
203        if self.contains_face(font_face) {
204            return Ok(DomRoot::from_ref(self));
205        }
206
207        // Step 2. If font is CSS-connected, throw an InvalidModificationError
208        // exception and exit this algorithm immediately.
209        if font_face.is_css_connected() {
210            return Err(Error::InvalidModification(Some(
211                "Cannot add CSS-connected FontFace to FontFaceSet".to_owned(),
212            )));
213        }
214
215        // Step 3. Add the font argument to the FontFaceSet’s set entries.
216        self.set_entries.borrow_mut().push(Dom::from_ref(font_face));
217        font_face.set_associated_font_face_set(self);
218
219        // Step 4. If font’s status attribute is "loading":
220        // Step 4.1 If the FontFaceSet’s [[LoadingFonts]] list is empty, switch the FontFaceSet to loading.
221        // Step 4.2 Append font to the FontFaceSet’s [[LoadingFonts]] list.
222        self.handle_font_face_status_changed(cx, font_face);
223
224        // Step 5. Return the FontFaceSet.
225        Ok(DomRoot::from_ref(self))
226    }
227
228    /// <https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-delete>
229    fn Delete(&self, to_delete: &FontFace) -> bool {
230        // Step 1. If font is CSS-connected, return false and exit this algorithm immediately.
231        if to_delete.is_css_connected() {
232            return false;
233        }
234
235        // Step 2. Let deleted be the result of removing font from the FontFaceSet’s set entries.
236        // TODO: Step 3. If font is present in the FontFaceSet’s [[LoadedFonts]], or [[FailedFonts]] lists, remove it.
237        // TODO: Step 4. If font is present in the FontFaceSet’s [[LoadingFonts]] list, remove it. If font was the last
238        // item in that list (and so the list is now empty), switch the FontFaceSet to loaded.
239        // Step 5. Return deleted.
240        self.delete_face(to_delete)
241    }
242
243    /// <https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-clear>
244    fn Clear(&self, cx: &mut JSContext) {
245        self.flush_author_font_set(cx);
246
247        // Step 1. Remove all non-CSS-connected items from the FontFaceSet’s set entries,
248        // its [[LoadedFonts]] list, and its [[FailedFonts]] list.
249        self.set_entries.borrow_mut().clear();
250
251        // TODO Step 2. If the FontFaceSet’s [[LoadingFonts]] list is non-empty, remove all items from it,
252        // then switch the FontFaceSet to loaded.
253    }
254
255    /// <https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-load>
256    fn Load(&self, cx: &mut JSContext, _font: DOMString, _text: DOMString) -> Rc<Promise> {
257        // Step 1. Let font face set be the FontFaceSet object this method was called on. Let
258        // promise be a newly-created promise object.
259        let load_promise = Promise::new(cx, &self.global());
260
261        // Step 3. Find the matching font faces from font face set using the font and text
262        // arguments passed to the function, and let font face list be the return value (ignoring
263        // the found faces flag). If a syntax error was returned, reject promise with a SyntaxError
264        // exception and terminate these steps.
265        //
266        // TODO: Implement this.
267
268        #[derive(MallocSizeOf, JSTraceable)]
269        struct LoadPromiseFulfillmentHandler {
270            #[conditional_malloc_size_of]
271            load_promise: Rc<Promise>,
272        }
273        impl Callback for LoadPromiseFulfillmentHandler {
274            fn callback(&self, cx: &mut CurrentRealm, _: Handle<Value>) {
275                self.load_promise
276                    .resolve_native(cx, &Vec::<&FontFace>::new());
277            }
278        }
279
280        // Step 4. Queue a task to run the following steps synchronously:
281        let trusted_this = Trusted::new(self);
282        let trusted_load_promise = TrustedPromise::new(load_promise.clone());
283        self.global()
284            .task_manager()
285            .font_loading_task_source()
286            .queue(task!(resolve_font_face_set_load_task: move |cx| {
287                let load_promise = trusted_load_promise.root();
288                let this = trusted_this.root();
289
290                // Step 4.1. For all of the font faces in the font face list, call their load()
291                // method.
292                // Step 4.2. Resolve promise with the result of waiting for all of the
293                // [[FontStatusPromise]]s of each font face in the font face list, in order.
294                //
295                // TODO: These steps are not implemented. Instead we wait until all fonts
296                // are loaded by resolving the returned promise when
297                // `document.fonts.ready` is resolved. The return list of fonts will not
298                // be correct, but any code that waits on the promise will have
299                // conservatively consistent behavior. This is important for preventing
300                // intermittent results in WPT tests.
301                let global = this.global();
302                let handler = PromiseNativeHandler::new(
303                    cx,
304                    &global,
305                    Some(Box::new(LoadPromiseFulfillmentHandler {
306                        load_promise,
307                    })),
308                    None,
309                );
310
311                let ready_promise = this.Ready(cx);
312                let mut realm = enter_auto_realm(cx, &*global);
313                ready_promise.append_native_handler(&mut realm.current_realm(), &handler);
314            }));
315
316        // Step 2. Return promise. Complete the rest of these steps asynchronously.
317        load_promise
318    }
319
320    /// <https://html.spec.whatwg.org/multipage/#customstateset>
321    fn Size(&self, cx: &mut JSContext) -> u32 {
322        self.size(cx)
323    }
324}
325
326impl Setlike for FontFaceSet {
327    type Key = DomRoot<FontFace>;
328
329    #[inline(always)]
330    fn get_index(&self, cx: &mut JSContext, index: u32) -> Option<Self::Key> {
331        self.flush_author_font_set(cx);
332        self.set_entries
333            .borrow()
334            .get(index as usize)
335            .map(|face| face.as_rooted())
336    }
337
338    #[inline(always)]
339    fn size(&self, cx: &mut JSContext) -> u32 {
340        self.flush_author_font_set(cx);
341        self.set_entries.borrow().len() as u32
342    }
343
344    #[inline(always)]
345    fn add(&self, _cx: &mut JSContext, face: Self::Key) {
346        self.set_entries.borrow_mut().push(face.as_traced());
347    }
348
349    #[inline(always)]
350    fn has(&self, cx: &mut JSContext, target: Self::Key) -> bool {
351        self.flush_author_font_set(cx);
352        self.contains_face(&target)
353    }
354
355    #[inline(always)]
356    fn clear(&self, cx: &mut JSContext) {
357        self.flush_author_font_set(cx);
358        self.set_entries.borrow_mut().clear();
359    }
360
361    #[inline(always)]
362    fn delete(&self, cx: &mut JSContext, to_delete: Self::Key) -> bool {
363        self.flush_author_font_set(cx);
364        self.delete_face(&to_delete)
365    }
366}