Skip to main content

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