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