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