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}