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(crate) fn needs_safe_area_insets() -> bool {
86 let Some(window_value) = window() else {
87 return false;
88 };
89 let user_agent: String = window_value.navigator().user_agent().unwrap_or_default();
90 needs_safe_area_insets(&user_agent)
91 }
92
93 pub fn use_safe_area_fix() {
119 Self::cache_safe_area_insets();
120 Self::init_safe_area_contract();
121 App::use_window_event(WINDOW_EVENT_FULLSCREEN_CHANGE, || {
122 let Some(window_value) = window() else {
123 return;
124 };
125 let Some(document_value) = window_value.document() else {
126 return;
127 };
128 let is_fullscreen: bool = document_value.fullscreen_element().is_some();
129 if is_fullscreen {
130 NATIVE_FULLSCREEN_ACTIVE.with(|flag: &Cell<bool>| flag.set(true));
131 Router::overlay_push_state();
132 } else {
133 let was_active: bool =
134 NATIVE_FULLSCREEN_ACTIVE.with(|flag: &Cell<bool>| flag.get());
135 if was_active {
136 NATIVE_FULLSCREEN_ACTIVE.with(|flag: &Cell<bool>| flag.set(false));
137 let exit_by_popstate: bool =
138 NATIVE_FULLSCREEN_EXIT_BY_POPSTATE.with(|flag: &Cell<bool>| flag.get());
139 if exit_by_popstate {
140 NATIVE_FULLSCREEN_EXIT_BY_POPSTATE
141 .with(|flag: &Cell<bool>| flag.set(false));
142 } else {
143 Router::overlay_back(None);
144 }
145 }
146 Self::apply_cached_insets();
147 }
148 });
149 App::use_window_event(WINDOW_EVENT_WEBKIT_FULLSCREEN_CHANGE, || {
150 Self::apply_cached_insets();
151 });
152 App::use_window_event(WINDOW_EVENT_RESIZE, || {
153 Self::apply_cached_insets();
154 });
155 Router::register_popstate_guard(Rc::new(|| {
156 if !NATIVE_FULLSCREEN_ACTIVE.with(|flag: &Cell<bool>| flag.get()) {
157 return false;
158 }
159 NATIVE_FULLSCREEN_EXIT_BY_POPSTATE.with(|flag: &Cell<bool>| flag.set(true));
160 let Some(window_value) = window() else {
161 return false;
162 };
163 let Some(document_value) = window_value.document() else {
164 return false;
165 };
166 document_value.exit_fullscreen();
167 true
168 }));
169 }
170
171 fn init_safe_area_contract() {
188 if !is_edge_to_edge_viewport(Self::is_immersive_declared(), measure_viewport_screen_gap()) {
189 return;
190 }
191 let top_value: String =
192 SAFE_AREA_INSET_TOP.with(|cell: &RefCell<String>| cell.borrow().clone());
193 if top_value.is_empty() {
194 return;
195 }
196 Self::apply_safe_area_contract(&top_value);
197 }
198
199 fn apply_safe_area_contract(top_value: &str) {
206 let Some(window_value) = window() else {
207 return;
208 };
209 let Some(document_value) = window_value.document() else {
210 return;
211 };
212 let Some(root) = document_value.document_element() else {
213 return;
214 };
215 let root_element: HtmlElement = root.unchecked_into();
216 let sides_trusted: bool = Self::needs_safe_area_insets();
220 let right_value: String = safe_area_contract_value(
221 sides_trusted,
222 &SAFE_AREA_INSET_RIGHT.with(|cell: &RefCell<String>| cell.borrow().clone()),
223 );
224 let bottom_value: String = safe_area_contract_value(
225 sides_trusted,
226 &SAFE_AREA_INSET_BOTTOM.with(|cell: &RefCell<String>| cell.borrow().clone()),
227 );
228 let left_value: String = safe_area_contract_value(
229 sides_trusted,
230 &SAFE_AREA_INSET_LEFT.with(|cell: &RefCell<String>| cell.borrow().clone()),
231 );
232 let _: Result<(), JsValue> = root_element
233 .style()
234 .set_property(IMMERSIVE_SAFE_TOP_PROPERTY, top_value);
235 let _: Result<(), JsValue> = root_element
236 .style()
237 .set_property(SAFE_AREA_RIGHT_PROPERTY, &right_value);
238 let _: Result<(), JsValue> = root_element
239 .style()
240 .set_property(SAFE_AREA_BOTTOM_PROPERTY, &bottom_value);
241 let _: Result<(), JsValue> = root_element
242 .style()
243 .set_property(SAFE_AREA_LEFT_PROPERTY, &left_value);
244 }
245
246 pub(crate) fn is_immersive_declared() -> bool {
254 let Some(window_value) = window() else {
255 return false;
256 };
257 let global_flag: bool = js_sys::Reflect::get(
258 &window_value,
259 &JsValue::from_str(IMMERSIVE_WINDOW_FLAG_PROPERTY),
260 )
261 .ok()
262 .and_then(|value: JsValue| value.as_bool())
263 .unwrap_or(false);
264 if global_flag {
265 return true;
266 }
267 window_value
268 .document()
269 .and_then(|document_value: Document| {
270 document_value
271 .query_selector(IMMERSIVE_META_SELECTOR)
272 .ok()
273 .flatten()
274 })
275 .and_then(|meta: Element| meta.get_attribute(IMMERSIVE_META_CONTENT_ATTRIBUTE))
276 .map(|content: String| content == IMMERSIVE_META_CONTENT_ENABLED)
277 .unwrap_or(false)
278 }
279
280 fn cache_safe_area_insets() {
292 let top_cached: String =
293 SAFE_AREA_INSET_TOP.with(|cell: &RefCell<String>| cell.borrow().clone());
294 if !top_cached.is_empty() {
295 return;
296 }
297 let Some(win) = window() else {
298 return;
299 };
300 let Some(document_value) = win.document() else {
301 return;
302 };
303 let Some(body) = document_value.body() else {
304 return;
305 };
306 let Ok(created_element) = document_value.create_element("div") else {
307 return;
308 };
309 let sentinel: HtmlElement = created_element.unchecked_into();
310 let _: Result<(), JsValue> = sentinel.style().set_property(
311 SENTINEL_STYLE_POSITION_PROPERTY,
312 SENTINEL_STYLE_POSITION_ABSOLUTE,
313 );
314 let _: Result<(), JsValue> = sentinel.style().set_property(
315 SENTINEL_STYLE_VISIBILITY_PROPERTY,
316 SENTINEL_STYLE_VISIBILITY_HIDDEN,
317 );
318 let _: Result<(), JsValue> = sentinel.style().set_property(
319 SENTINEL_STYLE_POINTER_EVENTS_PROPERTY,
320 SENTINEL_STYLE_POINTER_EVENTS_NONE,
321 );
322 let _: Result<(), JsValue> = sentinel
323 .style()
324 .set_property(SAFE_AREA_PADDING_TOP_PROPERTY, SAFE_AREA_PADDING_TOP_VALUE);
325 let _: Result<(), JsValue> = sentinel.style().set_property(
326 SAFE_AREA_PADDING_RIGHT_PROPERTY,
327 SAFE_AREA_PADDING_RIGHT_VALUE,
328 );
329 let _: Result<(), JsValue> = sentinel.style().set_property(
330 SAFE_AREA_PADDING_BOTTOM_PROPERTY,
331 SAFE_AREA_PADDING_BOTTOM_VALUE,
332 );
333 let _: Result<(), JsValue> = sentinel.style().set_property(
334 SAFE_AREA_PADDING_LEFT_PROPERTY,
335 SAFE_AREA_PADDING_LEFT_VALUE,
336 );
337 let _: Result<Node, JsValue> = body.append_child(&sentinel);
338 let Some(computed) = win.get_computed_style(&sentinel).ok().flatten() else {
339 let _: Result<Node, JsValue> = body.remove_child(&sentinel);
340 return;
341 };
342 let top_value: String = computed
343 .get_property_value(SAFE_AREA_PADDING_TOP_PROPERTY)
344 .unwrap_or_default();
345 let right_value: String = computed
346 .get_property_value(SAFE_AREA_PADDING_RIGHT_PROPERTY)
347 .unwrap_or_default();
348 let bottom_value: String = computed
349 .get_property_value(SAFE_AREA_PADDING_BOTTOM_PROPERTY)
350 .unwrap_or_default();
351 let left_value: String = computed
352 .get_property_value(SAFE_AREA_PADDING_LEFT_PROPERTY)
353 .unwrap_or_default();
354 let _: Result<Node, JsValue> = body.remove_child(&sentinel);
355 if top_value.is_empty() || top_value == "0px" {
356 return;
357 }
358 SAFE_AREA_INSET_TOP.with(|cell: &RefCell<String>| *cell.borrow_mut() = top_value);
359 SAFE_AREA_INSET_RIGHT.with(|cell: &RefCell<String>| *cell.borrow_mut() = right_value);
360 SAFE_AREA_INSET_BOTTOM.with(|cell: &RefCell<String>| *cell.borrow_mut() = bottom_value);
361 SAFE_AREA_INSET_LEFT.with(|cell: &RefCell<String>| *cell.borrow_mut() = left_value);
362 }
363
364 pub fn apply_cached_insets() {
380 let raw_top: String =
381 SAFE_AREA_INSET_TOP.with(|cell: &RefCell<String>| cell.borrow().clone());
382 let raw_right: String =
383 SAFE_AREA_INSET_RIGHT.with(|cell: &RefCell<String>| cell.borrow().clone());
384 let raw_bottom: String =
385 SAFE_AREA_INSET_BOTTOM.with(|cell: &RefCell<String>| cell.borrow().clone());
386 let raw_left: String =
387 SAFE_AREA_INSET_LEFT.with(|cell: &RefCell<String>| cell.borrow().clone());
388 if raw_top.is_empty()
389 && raw_right.is_empty()
390 && raw_bottom.is_empty()
391 && raw_left.is_empty()
392 {
393 return;
394 }
395 let is_trusted: bool =
396 is_edge_to_edge_viewport(Self::is_immersive_declared(), measure_viewport_screen_gap());
397 let top_value: String = safe_area_contract_value(is_trusted, &raw_top);
398 let sides_trusted: bool = is_trusted && Self::needs_safe_area_insets();
399 let right_value: String = safe_area_contract_value(sides_trusted, &raw_right);
400 let bottom_value: String = safe_area_contract_value(sides_trusted, &raw_bottom);
401 let left_value: String = safe_area_contract_value(sides_trusted, &raw_left);
402 let Some(window_value) = window() else {
403 return;
404 };
405 let Some(document_value) = window_value.document() else {
406 return;
407 };
408 let apply_to: &dyn Fn(&HtmlElement) = &|element: &HtmlElement| {
409 let _: Result<(), JsValue> = element
410 .style()
411 .set_property(SAFE_AREA_INSET_TOP_PROPERTY, &top_value);
412 let _: Result<(), JsValue> = element
413 .style()
414 .set_property(SAFE_AREA_INSET_RIGHT_PROPERTY, &right_value);
415 let _: Result<(), JsValue> = element
416 .style()
417 .set_property(SAFE_AREA_INSET_BOTTOM_PROPERTY, &bottom_value);
418 let _: Result<(), JsValue> = element
419 .style()
420 .set_property(SAFE_AREA_INSET_LEFT_PROPERTY, &left_value);
421 let _: Result<(), JsValue> = element
422 .style()
423 .set_property(IMMERSIVE_SAFE_TOP_PROPERTY, &top_value);
424 let _: Result<(), JsValue> = element
425 .style()
426 .set_property(SAFE_AREA_RIGHT_PROPERTY, &right_value);
427 let _: Result<(), JsValue> = element
428 .style()
429 .set_property(SAFE_AREA_BOTTOM_PROPERTY, &bottom_value);
430 let _: Result<(), JsValue> = element
431 .style()
432 .set_property(SAFE_AREA_LEFT_PROPERTY, &left_value);
433 };
434 if let Some(app_root) = document_value
435 .query_selector(APP_ROOT_MOBILE_SELECTOR)
436 .ok()
437 .flatten()
438 .map(|element: Element| element.unchecked_into::<HtmlElement>())
439 .or_else(|| {
440 document_value
441 .query_selector(APP_ROOT_DESKTOP_SELECTOR)
442 .ok()
443 .flatten()
444 .map(|element: Element| element.unchecked_into::<HtmlElement>())
445 })
446 {
447 apply_to(&app_root);
448 }
449 if let Some(canvas_fullscreen) = document_value
450 .query_selector(FULLSCREEN_CANVAS_CONTAINER_SELECTOR)
451 .ok()
452 .flatten()
453 .map(|element: Element| element.unchecked_into::<HtmlElement>())
454 {
455 apply_to(&canvas_fullscreen);
456 }
457 if let Some(game_fullscreen) = document_value
458 .query_selector(FULLSCREEN_GAME_CONTAINER_SELECTOR)
459 .ok()
460 .flatten()
461 .map(|element: Element| element.unchecked_into::<HtmlElement>())
462 {
463 apply_to(&game_fullscreen);
464 }
465 }
466}