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