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