script/dom/fullscreen/
lib.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::rc::Rc;
6
7use embedder_traits::EmbedderMsg;
8use html5ever::{local_name, ns};
9use servo_config::pref;
10
11use crate::dom::bindings::codegen::Bindings::NodeBinding::GetRootNodeOptions;
12use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods;
13use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRootMethods;
14use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
15use crate::dom::bindings::error::Error;
16use crate::dom::bindings::inheritance::Castable;
17use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
18use crate::dom::bindings::reflector::DomGlobal;
19use crate::dom::bindings::root::DomRoot;
20use crate::dom::document::document::Document;
21use crate::dom::document::documentorshadowroot::DocumentOrShadowRoot;
22use crate::dom::element::Element;
23use crate::dom::event::event::{EventBubbles, EventCancelable, EventComposed};
24use crate::dom::event::eventtarget::EventTarget;
25use crate::dom::node::NodeTraits;
26use crate::dom::node::node::Node;
27use crate::dom::promise::Promise;
28use crate::dom::shadowroot::ShadowRoot;
29use crate::dom::types::HTMLDialogElement;
30use crate::messaging::{CommonScriptMsg, MainThreadScriptMsg};
31use crate::realms::{AlreadyInRealm, InRealm};
32use crate::script_runtime::{CanGc, ScriptThreadEventCategory};
33use crate::task::TaskOnce;
34use crate::task_source::TaskSourceName;
35
36impl Document {
37    /// <https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen>
38    pub(crate) fn enter_fullscreen(&self, pending: &Element, can_gc: CanGc) -> Rc<Promise> {
39        // Step 1
40        // > Let pendingDoc be this’s node document.
41        // `Self` is the pending document.
42
43        // Step 2
44        // > Let promise be a new promise.
45        let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>();
46        let promise = Promise::new_in_current_realm(InRealm::Already(&in_realm_proof), can_gc);
47
48        // Step 3
49        // > If pendingDoc is not fully active, then reject promise with a TypeError exception and return promise.
50        if !self.is_fully_active() {
51            promise.reject_error(
52                Error::Type(c"Document is not fully active".to_owned()),
53                can_gc,
54            );
55            return promise;
56        }
57
58        // Step 4
59        // > Let error be false.
60        let mut error = false;
61
62        // Step 5
63        // > If any of the following conditions are false, then set error to true:
64        {
65            // > - This’s namespace is the HTML namespace or this is an SVG svg or MathML math element. [SVG] [MATHML]
66            match *pending.namespace() {
67                ns!(mathml) => {
68                    if pending.local_name().as_ref() != "math" {
69                        error = true;
70                    }
71                },
72                ns!(svg) => {
73                    if pending.local_name().as_ref() != "svg" {
74                        error = true;
75                    }
76                },
77                ns!(html) => (),
78                _ => error = true,
79            }
80
81            // > - This is not a dialog element.
82            if pending.is::<HTMLDialogElement>() {
83                error = true;
84            }
85
86            // > - The fullscreen element ready check for this returns true.
87            if !pending.fullscreen_element_ready_check() {
88                error = true;
89            }
90
91            // > - Fullscreen is supported.
92            // <https://fullscreen.spec.whatwg.org/#fullscreen-is-supported>
93            // > Fullscreen is supported if there is no previously-established user preference, security risk, or platform limitation.
94            // TODO: Add checks for whether fullscreen is supported as definition.
95
96            // > - This’s relevant global object has transient activation or the algorithm is triggered by a user generated orientation change.
97            // TODO: implement screen orientation API
98            if !pending.owner_window().has_transient_activation() {
99                error = true;
100            }
101        }
102
103        if pref!(dom_fullscreen_test) {
104            // For reftests we just take over the current window,
105            // and don't try to really enter fullscreen.
106            info!("Tests don't really enter fullscreen.");
107        } else {
108            // TODO fullscreen is supported
109            // TODO This algorithm is allowed to request fullscreen.
110            warn!("Fullscreen not supported yet");
111        }
112
113        // Step 6
114        // > If error is false, then consume user activation given pendingDoc’s relevant global object.
115        if !error {
116            pending.owner_window().consume_user_activation();
117        }
118
119        // Step 8.
120        // > If error is false, then resize pendingDoc’s node navigable’s top-level traversable’s active document’s viewport’s dimensions,
121        // > optionally taking into account options["navigationUI"]:
122        // TODO(#21600): Improve spec compliance of steps 7-13 paralelism.
123        // TODO(#42064): Implement fullscreen options, and ensure that this is spec compliant for all embedder.
124        if !error {
125            let event = EmbedderMsg::NotifyFullscreenStateChanged(self.webview_id(), true);
126            self.send_to_embedder(event);
127        }
128
129        // Step 7
130        // > Return promise, and run the remaining steps in parallel.
131        let pipeline_id = self.window().pipeline_id();
132
133        let trusted_pending = Trusted::new(pending);
134        let trusted_pending_doc = Trusted::new(self);
135        let trusted_promise = TrustedPromise::new(promise.clone());
136        let handler = ElementPerformFullscreenEnter::new(
137            trusted_pending,
138            trusted_pending_doc,
139            trusted_promise,
140            error,
141        );
142        let script_msg = CommonScriptMsg::Task(
143            ScriptThreadEventCategory::EnterFullscreen,
144            handler,
145            Some(pipeline_id),
146            TaskSourceName::DOMManipulation,
147        );
148        let msg = MainThreadScriptMsg::Common(script_msg);
149        self.window().main_thread_script_chan().send(msg).unwrap();
150
151        promise
152    }
153
154    /// <https://fullscreen.spec.whatwg.org/#exit-fullscreen>
155    pub(crate) fn exit_fullscreen(&self, can_gc: CanGc) -> Rc<Promise> {
156        let global = self.global();
157
158        // Step 1
159        // > Let promise be a new promise
160        let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>();
161        let promise = Promise::new_in_current_realm(InRealm::Already(&in_realm_proof), can_gc);
162
163        // Step 2
164        // > If doc is not fully active or doc’s fullscreen element is null, then reject promise with a TypeError exception and return promise.
165        if !self.is_fully_active() || self.fullscreen_element().is_none() {
166            promise.reject_error(
167                Error::Type(
168                    c"No fullscreen element to exit or document is not fully active".to_owned(),
169                ),
170                can_gc,
171            );
172            return promise;
173        }
174
175        // TODO(#42067): Implement step 3-7, handling fullscreen's propagation across navigables.
176
177        let element = self.fullscreen_element().unwrap();
178        let window = self.window();
179
180        // Step 10
181        // > If resize is true, resize doc’s viewport to its "normal" dimensions.
182        // TODO(#21600): Improve spec compliance of steps 8-15 paralelism.
183        let event = EmbedderMsg::NotifyFullscreenStateChanged(self.webview_id(), false);
184        self.send_to_embedder(event);
185
186        // Step 8
187        // > Return promise, and run the remaining steps in parallel.
188        let trusted_element = Trusted::new(&*element);
189        let trusted_promise = TrustedPromise::new(promise.clone());
190        let handler = ElementPerformFullscreenExit::new(trusted_element, trusted_promise);
191        let pipeline_id = Some(global.pipeline_id());
192        let script_msg = CommonScriptMsg::Task(
193            ScriptThreadEventCategory::ExitFullscreen,
194            handler,
195            pipeline_id,
196            TaskSourceName::DOMManipulation,
197        );
198        let msg = MainThreadScriptMsg::Common(script_msg);
199        window.main_thread_script_chan().send(msg).unwrap();
200
201        promise
202    }
203
204    pub(crate) fn get_allow_fullscreen(&self) -> bool {
205        // https://html.spec.whatwg.org/multipage/#allowed-to-use
206        match self.browsing_context() {
207            // Step 1
208            None => false,
209            Some(_) => {
210                // Step 2
211                let window = self.window();
212                if window.is_top_level() {
213                    true
214                } else {
215                    // Step 3
216                    window
217                        .GetFrameElement()
218                        .is_some_and(|el| el.has_attribute(&local_name!("allowfullscreen")))
219                }
220            },
221        }
222    }
223}
224
225impl DocumentOrShadowRoot {
226    /// <https://fullscreen.spec.whatwg.org/#dom-document-fullscreenelement>
227    pub(crate) fn get_fullscreen_element(
228        node: &Node,
229        fullscreen_element: Option<DomRoot<Element>>,
230    ) -> Option<DomRoot<Element>> {
231        // Step 1. If this is a shadow root and its host is not connected, then return null.
232        if let Some(shadow_root) = node.downcast::<ShadowRoot>() {
233            if !shadow_root.Host().is_connected() {
234                return None;
235            }
236        }
237
238        // Step 2. Let candidate be the result of retargeting fullscreen element against this.
239        let retargeted = fullscreen_element?
240            .upcast::<EventTarget>()
241            .retarget(node.upcast());
242        // It's safe to unwrap downcasting to `Element` because `retarget` either returns `fullscreen_element` or a host of `fullscreen_element` and hosts are always elements.
243        let candidate = DomRoot::downcast::<Element>(retargeted).unwrap();
244
245        // Step 3. If candidate and this are in the same tree, then return candidate.
246        if *candidate
247            .upcast::<Node>()
248            .GetRootNode(&GetRootNodeOptions::empty()) ==
249            *node
250        {
251            return Some(candidate);
252        }
253
254        // Step 4. Return null.
255        None
256    }
257}
258
259impl Element {
260    // https://fullscreen.spec.whatwg.org/#fullscreen-element-ready-check
261    pub(crate) fn fullscreen_element_ready_check(&self) -> bool {
262        if !self.is_connected() {
263            return false;
264        }
265        self.owner_document().get_allow_fullscreen()
266    }
267}
268
269struct ElementPerformFullscreenEnter {
270    element: Trusted<Element>,
271    document: Trusted<Document>,
272    promise: TrustedPromise,
273    error: bool,
274}
275
276impl ElementPerformFullscreenEnter {
277    fn new(
278        element: Trusted<Element>,
279        document: Trusted<Document>,
280        promise: TrustedPromise,
281        error: bool,
282    ) -> Box<ElementPerformFullscreenEnter> {
283        Box::new(ElementPerformFullscreenEnter {
284            element,
285            document,
286            promise,
287            error,
288        })
289    }
290}
291
292impl TaskOnce for ElementPerformFullscreenEnter {
293    /// Step 9-14 of <https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen>
294    fn run_once(self, cx: &mut js::context::JSContext) {
295        let element = self.element.root();
296        let promise = self.promise.root();
297        let document = element.owner_document();
298
299        // Step 9
300        // > If any of the following conditions are false, then set error to true:
301        // > - This’s node document is pendingDoc.
302        // > - The fullscreen element ready check for this returns true.
303        // Step 10
304        // > If error is true:
305        // > - Append (fullscreenerror, this) to pendingDoc’s list of pending fullscreen events.
306        // > - Reject promise with a TypeError exception and terminate these steps.
307        if self.document.root() != document ||
308            !element.fullscreen_element_ready_check() ||
309            self.error
310        {
311            // TODO(#31866): we should queue this and fire them in update the rendering.
312            document
313                .upcast::<EventTarget>()
314                .fire_event(atom!("fullscreenerror"), CanGc::from_cx(cx));
315            promise.reject_error(
316                Error::Type(c"fullscreen is not connected".to_owned()),
317                CanGc::from_cx(cx),
318            );
319            return;
320        }
321
322        // TODO(#42067): Implement step 11-13
323        // The following operations is based on the old version of the specs.
324        element.set_fullscreen_state(true);
325        document.set_fullscreen_element(Some(&element));
326        document.upcast::<EventTarget>().fire_event_with_params(
327            atom!("fullscreenchange"),
328            EventBubbles::Bubbles,
329            EventCancelable::NotCancelable,
330            EventComposed::Composed,
331            CanGc::from_cx(cx),
332        );
333
334        // Step 14.
335        // > Resolve promise with undefined.
336        promise.resolve_native(&(), CanGc::from_cx(cx));
337    }
338}
339
340struct ElementPerformFullscreenExit {
341    element: Trusted<Element>,
342    promise: TrustedPromise,
343}
344
345impl ElementPerformFullscreenExit {
346    fn new(
347        element: Trusted<Element>,
348        promise: TrustedPromise,
349    ) -> Box<ElementPerformFullscreenExit> {
350        Box::new(ElementPerformFullscreenExit { element, promise })
351    }
352}
353
354impl TaskOnce for ElementPerformFullscreenExit {
355    /// Step 9-16 of <https://fullscreen.spec.whatwg.org/#exit-fullscreen>
356    fn run_once(self, cx: &mut js::context::JSContext) {
357        let element = self.element.root();
358        let document = element.owner_document();
359        // Step 9.
360        // > Run the fully unlock the screen orientation steps with doc.
361        // TODO: Need to implement ScreenOrientation API first
362
363        // TODO(#42067): Implement step 10-15
364        // The following operations is based on the old version of the specs.
365        element.set_fullscreen_state(false);
366        document.set_fullscreen_element(None);
367        document.upcast::<EventTarget>().fire_event_with_params(
368            atom!("fullscreenchange"),
369            EventBubbles::Bubbles,
370            EventCancelable::NotCancelable,
371            EventComposed::Composed,
372            CanGc::from_cx(cx),
373        );
374
375        // Step 16
376        // > Resolve promise with undefined.
377        self.promise.root().resolve_native(&(), CanGc::from_cx(cx));
378    }
379}