Skip to main content

yew_hooks/hooks/
use_fullscreen.rs

1use std::rc::Rc;
2
3use gloo::events::EventListener;
4use gloo::utils::document;
5use wasm_bindgen::prelude::*;
6use web_sys::HtmlElement;
7use yew::prelude::*;
8
9use super::{use_state_ptr_eq, UseStatePtrEqHandle};
10
11/// State handle for the [`use_fullscreen`] hook.
12pub struct UseFullscreenHandle {
13    /// Whether fullscreen is currently active.
14    pub is_fullscreen: UseStatePtrEqHandle<bool>,
15    /// Whether the Fullscreen API is supported in the current browser.
16    pub is_supported: Rc<bool>,
17
18    enter: Rc<dyn Fn()>,
19    exit: Rc<dyn Fn()>,
20    toggle: Rc<dyn Fn()>,
21}
22
23impl UseFullscreenHandle {
24    /// Enter fullscreen mode for the element.
25    pub fn enter(&self) {
26        (self.enter)();
27    }
28
29    /// Exit fullscreen mode.
30    pub fn exit(&self) {
31        (self.exit)();
32    }
33
34    /// Toggle fullscreen mode for the element.
35    /// If currently in fullscreen, exits fullscreen.
36    /// If not in fullscreen, enters fullscreen for the element.
37    pub fn toggle(&self) {
38        (self.toggle)();
39    }
40}
41
42impl Clone for UseFullscreenHandle {
43    fn clone(&self) -> Self {
44        Self {
45            is_fullscreen: self.is_fullscreen.clone(),
46            is_supported: self.is_supported.clone(),
47            enter: self.enter.clone(),
48            exit: self.exit.clone(),
49            toggle: self.toggle.clone(),
50        }
51    }
52}
53
54/// This hook provides functionality to control fullscreen mode for elements.
55/// It allows entering, exiting, and toggling fullscreen mode, and tracks
56/// the current fullscreen state and element.
57///
58/// # Example
59///
60/// ```rust
61/// # use yew::prelude::*;
62/// #
63/// use yew_hooks::prelude::*;
64///
65/// #[function_component(UseFullscreen)]
66/// fn fullscreen() -> Html {
67///     let element_ref = use_node_ref();
68///     let fullscreen = use_fullscreen(element_ref.clone());
69///
70///     let onenter = {
71///         let fullscreen = fullscreen.clone();
72///         Callback::from(move |_| {
73///             fullscreen.enter();
74///         })
75///     };
76///
77///     let onexit = {
78///         let fullscreen = fullscreen.clone();
79///         Callback::from(move |_| {
80///             fullscreen.exit();
81///         })
82///     };
83///
84///     let ontoggle = {
85///         let fullscreen = fullscreen.clone();
86///         Callback::from(move |_| {
87///             fullscreen.toggle();
88///         })
89///     };
90///
91///     html! {
92///         <div>
93///             <div ref={element_ref} style="width: 100%; height: 300px; background-color: lightblue; display: flex; align-items: center; justify-content: center;">
94///                 <p>{ "This element can go fullscreen" }</p>
95///             </div>
96///             <div>
97///                 <button onclick={onenter} disabled={*fullscreen.is_fullscreen}>
98///                     { "Enter Fullscreen" }
99///                 </button>
100///                 <button onclick={onexit} disabled={!*fullscreen.is_fullscreen}>
101///                     { "Exit Fullscreen" }
102///                 </button>
103///                 <button onclick={ontoggle}>
104///                     { "Toggle Fullscreen" }
105///                 </button>
106///             </div>
107///             <p>{ format!("Is fullscreen: {}", *fullscreen.is_fullscreen) }</p>
108///             <p>{ format!("Is supported: {}", *fullscreen.is_supported) }</p>
109///         </div>
110///     }
111/// }
112/// ```
113#[hook]
114pub fn use_fullscreen(node: NodeRef) -> UseFullscreenHandle {
115    let is_fullscreen = use_state_ptr_eq(|| false);
116
117    // Check if Fullscreen API is supported
118    let is_supported = use_memo((), |_| {
119        let doc = document();
120        // Check if fullscreen API is available
121        js_sys::Reflect::has(&doc, &JsValue::from("fullscreenEnabled")).unwrap_or(false)
122    });
123
124    // Function to get the current fullscreen element
125    let get_fullscreen_element = || {
126        let doc = document();
127        // Try to get fullscreen element using reflection
128        js_sys::Reflect::get(&doc, &JsValue::from("fullscreenElement"))
129            .ok()
130            .and_then(|v| v.dyn_into::<web_sys::Element>().ok())
131    };
132
133    // Update state based on current fullscreen status
134    let update_fullscreen_state = {
135        let is_fullscreen = is_fullscreen.clone();
136        move || {
137            let fullscreen_element = get_fullscreen_element();
138            let is_fs = fullscreen_element.is_some();
139            is_fullscreen.set(is_fs);
140        }
141    };
142
143    // Set up event listeners for fullscreen change events
144    {
145        let update_fullscreen_state1 = update_fullscreen_state.clone();
146        let update_fullscreen_state2 = update_fullscreen_state.clone();
147        use_effect_with((), move |_| {
148            let doc = document();
149            let listener = EventListener::new(&doc, "fullscreenchange", move |_| {
150                update_fullscreen_state1();
151            });
152
153            // Also listen for webkit-specific events for Safari compatibility
154            let webkit_listener = EventListener::new(&doc, "webkitfullscreenchange", move |_| {
155                update_fullscreen_state2();
156            });
157
158            // Keep listeners alive for the duration of the component
159            Box::new(move || {
160                drop(listener);
161                drop(webkit_listener);
162            })
163        });
164    }
165
166    // Initial state update
167    {
168        let update_fullscreen_state = update_fullscreen_state.clone();
169        use_effect_with((), move |_| {
170            update_fullscreen_state();
171            || ()
172        });
173    }
174
175    let enter = {
176        let is_supported = is_supported.clone();
177        let node = node.clone();
178        Rc::new(move || {
179            if !*is_supported {
180                return;
181            }
182
183            let Some(element) = node.cast::<HtmlElement>() else {
184                // Only HtmlElement can go fullscreen
185                return;
186            };
187
188            // Try standard method first using reflection
189            let result = js_sys::Reflect::get(&element, &JsValue::from("requestFullscreen"));
190            if let Ok(func) = result {
191                if func.is_function() {
192                    let func = js_sys::Function::from(func);
193                    let _ = func.call0(&element);
194                } else {
195                    // Fallback to webkit method for Safari
196                    let func =
197                        js_sys::Reflect::get(&element, &JsValue::from("webkitRequestFullscreen"));
198                    if let Ok(func) = func {
199                        if func.is_function() {
200                            let func = js_sys::Function::from(func);
201                            let _ = func.call0(&element);
202                        }
203                    }
204                }
205            } else {
206                // Fallback to webkit method for Safari
207                let func =
208                    js_sys::Reflect::get(&element, &JsValue::from("webkitRequestFullscreen"));
209                if let Ok(func) = func {
210                    if func.is_function() {
211                        let func = js_sys::Function::from(func);
212                        let _ = func.call0(&element);
213                    }
214                }
215            }
216        })
217    };
218
219    let exit = {
220        let is_supported = is_supported.clone();
221        Rc::new(move || {
222            if !*is_supported {
223                return;
224            }
225
226            let doc = document();
227            // Try standard method first using reflection
228            let result = js_sys::Reflect::get(&doc, &JsValue::from("exitFullscreen"));
229            if let Ok(func) = result {
230                if func.is_function() {
231                    let func = js_sys::Function::from(func);
232                    let _ = func.call0(&doc);
233                } else {
234                    // Fallback to webkit method for Safari
235                    let func = js_sys::Reflect::get(&doc, &JsValue::from("webkitExitFullscreen"));
236                    if let Ok(func) = func {
237                        if func.is_function() {
238                            let func = js_sys::Function::from(func);
239                            let _ = func.call0(&doc);
240                        }
241                    }
242                }
243            } else {
244                // Fallback to webkit method for Safari
245                let func = js_sys::Reflect::get(&doc, &JsValue::from("webkitExitFullscreen"));
246                if let Ok(func) = func {
247                    if func.is_function() {
248                        let func = js_sys::Function::from(func);
249                        let _ = func.call0(&doc);
250                    }
251                }
252            }
253        })
254    };
255
256    let toggle = {
257        let is_fullscreen = is_fullscreen.clone();
258        let enter = enter.clone();
259        let exit = exit.clone();
260        Rc::new(move || {
261            if *is_fullscreen {
262                exit();
263            } else {
264                enter();
265            }
266        })
267    };
268
269    UseFullscreenHandle {
270        is_fullscreen,
271        is_supported,
272        enter,
273        exit,
274        toggle,
275    }
276}