euv_ui/component/layout/hook/
impl.rs1use super::*;
2
3impl UseEuvLayout {
7 pub fn use_resize() -> Signal<bool> {
18 let mobile_signal: Signal<bool> = App::use_signal(Router::is_mobile);
19 let timer_signal: Signal<Option<i32>> = App::use_signal(|| None);
20 let debounce_closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
21 let mobile: bool = Router::is_mobile();
22 mobile_signal.set(mobile);
23 }));
24 let debounce_callback: Function = debounce_closure
25 .as_ref()
26 .unchecked_ref::<Function>()
27 .clone();
28 debounce_closure.forget();
29 let Some(timeout_window) = window() else {
30 return mobile_signal;
31 };
32 App::use_window_event(WINDOW_EVENT_RESIZE, move || {
33 let old_timer: Option<i32> = timer_signal.get();
34 if let Some(timer_id) = old_timer {
35 timeout_window.clear_timeout_with_handle(timer_id);
36 }
37 let new_timer: i32 = timeout_window
38 .set_timeout_with_callback_and_timeout_and_arguments_0(
39 &debounce_callback,
40 RESIZE_DEBOUNCE_MILLIS,
41 )
42 .unwrap_or_default();
43 timer_signal.set(Some(new_timer));
44 });
45 mobile_signal
46 }
47
48 pub fn use_drawer_toggle(drawer_open: Signal<bool>) -> Option<Rc<dyn Fn(Event)>> {
64 Some(Rc::new(move |_: Event| {
65 let is_open: bool = drawer_open.get();
66 if is_open {
67 Router::overlay_stack_close();
68 }
69 drawer_open.set(!is_open);
70 }))
71 }
72
73 pub fn use_safe_area_fix() {
99 Self::cache_safe_area_insets();
100 Self::init_immersive_safe_area();
101 App::use_window_event(WINDOW_EVENT_FULLSCREEN_CHANGE, || {
102 let Some(window_value) = window() else {
103 return;
104 };
105 let Some(document_value) = window_value.document() else {
106 return;
107 };
108 let is_fullscreen: bool = document_value.fullscreen_element().is_some();
109 if is_fullscreen {
110 NATIVE_FULLSCREEN_ACTIVE.with(|flag: &Cell<bool>| flag.set(true));
111 Router::overlay_push_state();
112 } else {
113 let was_active: bool =
114 NATIVE_FULLSCREEN_ACTIVE.with(|flag: &Cell<bool>| flag.get());
115 if was_active {
116 NATIVE_FULLSCREEN_ACTIVE.with(|flag: &Cell<bool>| flag.set(false));
117 let exit_by_popstate: bool =
118 NATIVE_FULLSCREEN_EXIT_BY_POPSTATE.with(|flag: &Cell<bool>| flag.get());
119 if exit_by_popstate {
120 NATIVE_FULLSCREEN_EXIT_BY_POPSTATE
121 .with(|flag: &Cell<bool>| flag.set(false));
122 } else {
123 Router::overlay_back(None);
124 }
125 }
126 Self::apply_cached_insets();
127 }
128 });
129 App::use_window_event(WINDOW_EVENT_WEBKIT_FULLSCREEN_CHANGE, || {
130 Self::apply_cached_insets();
131 });
132 App::use_window_event(WINDOW_EVENT_RESIZE, || {
133 Self::apply_cached_insets();
134 });
135 Router::register_popstate_guard(Rc::new(|| {
136 if !NATIVE_FULLSCREEN_ACTIVE.with(|flag: &Cell<bool>| flag.get()) {
137 return false;
138 }
139 NATIVE_FULLSCREEN_EXIT_BY_POPSTATE.with(|flag: &Cell<bool>| flag.set(true));
140 let Some(window_value) = window() else {
141 return false;
142 };
143 let Some(document_value) = window_value.document() else {
144 return false;
145 };
146 document_value.exit_fullscreen();
147 true
148 }));
149 }
150
151 fn init_immersive_safe_area() {
168 if !Self::is_immersive_declared() {
169 return;
170 }
171 let top_value: String =
172 SAFE_AREA_INSET_TOP.with(|cell: &RefCell<String>| cell.borrow().clone());
173 if top_value.is_empty() {
174 return;
175 }
176 let Some(window_value) = window() else {
177 return;
178 };
179 let Some(document_value) = window_value.document() else {
180 return;
181 };
182 let Some(root) = document_value.document_element() else {
183 return;
184 };
185 let root_element: HtmlElement = root.unchecked_into();
186 let _: Result<(), JsValue> = root_element
187 .style()
188 .set_property(IMMERSIVE_SAFE_TOP_PROPERTY, &top_value);
189 }
190
191 fn is_immersive_declared() -> bool {
199 let Some(window_value) = window() else {
200 return false;
201 };
202 let global_flag: bool = js_sys::Reflect::get(
203 &window_value,
204 &JsValue::from_str(IMMERSIVE_WINDOW_FLAG_PROPERTY),
205 )
206 .ok()
207 .and_then(|value: JsValue| value.as_bool())
208 .unwrap_or(false);
209 if global_flag {
210 return true;
211 }
212 window_value
213 .document()
214 .and_then(|document_value: Document| {
215 document_value
216 .query_selector(IMMERSIVE_META_SELECTOR)
217 .ok()
218 .flatten()
219 })
220 .and_then(|meta: Element| meta.get_attribute(IMMERSIVE_META_CONTENT_ATTRIBUTE))
221 .map(|content: String| content == IMMERSIVE_META_CONTENT_ENABLED)
222 .unwrap_or(false)
223 }
224
225 fn cache_safe_area_insets() {
237 let top_cached: String =
238 SAFE_AREA_INSET_TOP.with(|cell: &RefCell<String>| cell.borrow().clone());
239 if !top_cached.is_empty() {
240 return;
241 }
242 let Some(win) = window() else {
243 return;
244 };
245 let Some(document_value) = win.document() else {
246 return;
247 };
248 let Some(body) = document_value.body() else {
249 return;
250 };
251 let Ok(created_element) = document_value.create_element("div") else {
252 return;
253 };
254 let sentinel: HtmlElement = created_element.unchecked_into();
255 let _: Result<(), JsValue> = sentinel.style().set_property(
256 SENTINEL_STYLE_POSITION_PROPERTY,
257 SENTINEL_STYLE_POSITION_ABSOLUTE,
258 );
259 let _: Result<(), JsValue> = sentinel.style().set_property(
260 SENTINEL_STYLE_VISIBILITY_PROPERTY,
261 SENTINEL_STYLE_VISIBILITY_HIDDEN,
262 );
263 let _: Result<(), JsValue> = sentinel.style().set_property(
264 SENTINEL_STYLE_POINTER_EVENTS_PROPERTY,
265 SENTINEL_STYLE_POINTER_EVENTS_NONE,
266 );
267 let _: Result<(), JsValue> = sentinel
268 .style()
269 .set_property(SAFE_AREA_PADDING_TOP_PROPERTY, SAFE_AREA_PADDING_TOP_VALUE);
270 let _: Result<(), JsValue> = sentinel.style().set_property(
271 SAFE_AREA_PADDING_RIGHT_PROPERTY,
272 SAFE_AREA_PADDING_RIGHT_VALUE,
273 );
274 let _: Result<(), JsValue> = sentinel.style().set_property(
275 SAFE_AREA_PADDING_BOTTOM_PROPERTY,
276 SAFE_AREA_PADDING_BOTTOM_VALUE,
277 );
278 let _: Result<(), JsValue> = sentinel.style().set_property(
279 SAFE_AREA_PADDING_LEFT_PROPERTY,
280 SAFE_AREA_PADDING_LEFT_VALUE,
281 );
282 let _: Result<Node, JsValue> = body.append_child(&sentinel);
283 let Some(computed) = win.get_computed_style(&sentinel).ok().flatten() else {
284 let _: Result<Node, JsValue> = body.remove_child(&sentinel);
285 return;
286 };
287 let top_value: String = computed
288 .get_property_value(SAFE_AREA_PADDING_TOP_PROPERTY)
289 .unwrap_or_default();
290 let right_value: String = computed
291 .get_property_value(SAFE_AREA_PADDING_RIGHT_PROPERTY)
292 .unwrap_or_default();
293 let bottom_value: String = computed
294 .get_property_value(SAFE_AREA_PADDING_BOTTOM_PROPERTY)
295 .unwrap_or_default();
296 let left_value: String = computed
297 .get_property_value(SAFE_AREA_PADDING_LEFT_PROPERTY)
298 .unwrap_or_default();
299 let _: Result<Node, JsValue> = body.remove_child(&sentinel);
300 if top_value.is_empty() || top_value == "0px" {
301 return;
302 }
303 SAFE_AREA_INSET_TOP.with(|cell: &RefCell<String>| *cell.borrow_mut() = top_value);
304 SAFE_AREA_INSET_RIGHT.with(|cell: &RefCell<String>| *cell.borrow_mut() = right_value);
305 SAFE_AREA_INSET_BOTTOM.with(|cell: &RefCell<String>| *cell.borrow_mut() = bottom_value);
306 SAFE_AREA_INSET_LEFT.with(|cell: &RefCell<String>| *cell.borrow_mut() = left_value);
307 }
308
309 pub fn apply_cached_insets() {
325 let top_value: String =
326 SAFE_AREA_INSET_TOP.with(|cell: &RefCell<String>| cell.borrow().clone());
327 if top_value.is_empty() {
328 return;
329 }
330 let right_value: String =
331 SAFE_AREA_INSET_RIGHT.with(|cell: &RefCell<String>| cell.borrow().clone());
332 let bottom_value: String =
333 SAFE_AREA_INSET_BOTTOM.with(|cell: &RefCell<String>| cell.borrow().clone());
334 let left_value: String =
335 SAFE_AREA_INSET_LEFT.with(|cell: &RefCell<String>| cell.borrow().clone());
336 let Some(window_value) = window() else {
337 return;
338 };
339 let Some(document_value) = window_value.document() else {
340 return;
341 };
342 let apply_to: &dyn Fn(&HtmlElement) = &|element: &HtmlElement| {
343 let _: Result<(), JsValue> = element
344 .style()
345 .set_property(SAFE_AREA_INSET_TOP_PROPERTY, &top_value);
346 let _: Result<(), JsValue> = element
347 .style()
348 .set_property(SAFE_AREA_INSET_RIGHT_PROPERTY, &right_value);
349 let _: Result<(), JsValue> = element
350 .style()
351 .set_property(SAFE_AREA_INSET_BOTTOM_PROPERTY, &bottom_value);
352 let _: Result<(), JsValue> = element
353 .style()
354 .set_property(SAFE_AREA_INSET_LEFT_PROPERTY, &left_value);
355 };
356 if let Some(app_root) = document_value
357 .query_selector(APP_ROOT_MOBILE_SELECTOR)
358 .ok()
359 .flatten()
360 .map(|element: Element| element.unchecked_into::<HtmlElement>())
361 .or_else(|| {
362 document_value
363 .query_selector(APP_ROOT_DESKTOP_SELECTOR)
364 .ok()
365 .flatten()
366 .map(|element: Element| element.unchecked_into::<HtmlElement>())
367 })
368 {
369 apply_to(&app_root);
370 }
371 if let Some(canvas_fullscreen) = document_value
372 .query_selector(FULLSCREEN_CANVAS_CONTAINER_SELECTOR)
373 .ok()
374 .flatten()
375 .map(|element: Element| element.unchecked_into::<HtmlElement>())
376 {
377 apply_to(&canvas_fullscreen);
378 }
379 if let Some(game_fullscreen) = document_value
380 .query_selector(FULLSCREEN_GAME_CONTAINER_SELECTOR)
381 .ok()
382 .flatten()
383 .map(|element: Element| element.unchecked_into::<HtmlElement>())
384 {
385 apply_to(&game_fullscreen);
386 }
387 }
388}