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_safe_area_contract();
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_safe_area_contract() {
168 if !is_edge_to_edge_viewport(Self::is_immersive_declared(), measure_viewport_screen_gap()) {
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 Self::apply_safe_area_contract(&top_value);
177 }
178
179 fn apply_safe_area_contract(top_value: &str) {
186 let Some(window_value) = window() else {
187 return;
188 };
189 let Some(document_value) = window_value.document() else {
190 return;
191 };
192 let Some(root) = document_value.document_element() else {
193 return;
194 };
195 let root_element: HtmlElement = root.unchecked_into();
196 let right_value: String = safe_area_contract_value(
197 true,
198 &SAFE_AREA_INSET_RIGHT.with(|cell: &RefCell<String>| cell.borrow().clone()),
199 );
200 let bottom_value: String = safe_area_contract_value(
201 true,
202 &SAFE_AREA_INSET_BOTTOM.with(|cell: &RefCell<String>| cell.borrow().clone()),
203 );
204 let left_value: String = safe_area_contract_value(
205 true,
206 &SAFE_AREA_INSET_LEFT.with(|cell: &RefCell<String>| cell.borrow().clone()),
207 );
208 let _: Result<(), JsValue> = root_element
209 .style()
210 .set_property(IMMERSIVE_SAFE_TOP_PROPERTY, top_value);
211 let _: Result<(), JsValue> = root_element
212 .style()
213 .set_property(SAFE_AREA_RIGHT_PROPERTY, &right_value);
214 let _: Result<(), JsValue> = root_element
215 .style()
216 .set_property(SAFE_AREA_BOTTOM_PROPERTY, &bottom_value);
217 let _: Result<(), JsValue> = root_element
218 .style()
219 .set_property(SAFE_AREA_LEFT_PROPERTY, &left_value);
220 }
221
222 pub(crate) fn is_immersive_declared() -> bool {
230 let Some(window_value) = window() else {
231 return false;
232 };
233 let global_flag: bool = js_sys::Reflect::get(
234 &window_value,
235 &JsValue::from_str(IMMERSIVE_WINDOW_FLAG_PROPERTY),
236 )
237 .ok()
238 .and_then(|value: JsValue| value.as_bool())
239 .unwrap_or(false);
240 if global_flag {
241 return true;
242 }
243 window_value
244 .document()
245 .and_then(|document_value: Document| {
246 document_value
247 .query_selector(IMMERSIVE_META_SELECTOR)
248 .ok()
249 .flatten()
250 })
251 .and_then(|meta: Element| meta.get_attribute(IMMERSIVE_META_CONTENT_ATTRIBUTE))
252 .map(|content: String| content == IMMERSIVE_META_CONTENT_ENABLED)
253 .unwrap_or(false)
254 }
255
256 fn cache_safe_area_insets() {
268 let top_cached: String =
269 SAFE_AREA_INSET_TOP.with(|cell: &RefCell<String>| cell.borrow().clone());
270 if !top_cached.is_empty() {
271 return;
272 }
273 let Some(win) = window() else {
274 return;
275 };
276 let Some(document_value) = win.document() else {
277 return;
278 };
279 let Some(body) = document_value.body() else {
280 return;
281 };
282 let Ok(created_element) = document_value.create_element("div") else {
283 return;
284 };
285 let sentinel: HtmlElement = created_element.unchecked_into();
286 let _: Result<(), JsValue> = sentinel.style().set_property(
287 SENTINEL_STYLE_POSITION_PROPERTY,
288 SENTINEL_STYLE_POSITION_ABSOLUTE,
289 );
290 let _: Result<(), JsValue> = sentinel.style().set_property(
291 SENTINEL_STYLE_VISIBILITY_PROPERTY,
292 SENTINEL_STYLE_VISIBILITY_HIDDEN,
293 );
294 let _: Result<(), JsValue> = sentinel.style().set_property(
295 SENTINEL_STYLE_POINTER_EVENTS_PROPERTY,
296 SENTINEL_STYLE_POINTER_EVENTS_NONE,
297 );
298 let _: Result<(), JsValue> = sentinel
299 .style()
300 .set_property(SAFE_AREA_PADDING_TOP_PROPERTY, SAFE_AREA_PADDING_TOP_VALUE);
301 let _: Result<(), JsValue> = sentinel.style().set_property(
302 SAFE_AREA_PADDING_RIGHT_PROPERTY,
303 SAFE_AREA_PADDING_RIGHT_VALUE,
304 );
305 let _: Result<(), JsValue> = sentinel.style().set_property(
306 SAFE_AREA_PADDING_BOTTOM_PROPERTY,
307 SAFE_AREA_PADDING_BOTTOM_VALUE,
308 );
309 let _: Result<(), JsValue> = sentinel.style().set_property(
310 SAFE_AREA_PADDING_LEFT_PROPERTY,
311 SAFE_AREA_PADDING_LEFT_VALUE,
312 );
313 let _: Result<Node, JsValue> = body.append_child(&sentinel);
314 let Some(computed) = win.get_computed_style(&sentinel).ok().flatten() else {
315 let _: Result<Node, JsValue> = body.remove_child(&sentinel);
316 return;
317 };
318 let top_value: String = computed
319 .get_property_value(SAFE_AREA_PADDING_TOP_PROPERTY)
320 .unwrap_or_default();
321 let right_value: String = computed
322 .get_property_value(SAFE_AREA_PADDING_RIGHT_PROPERTY)
323 .unwrap_or_default();
324 let bottom_value: String = computed
325 .get_property_value(SAFE_AREA_PADDING_BOTTOM_PROPERTY)
326 .unwrap_or_default();
327 let left_value: String = computed
328 .get_property_value(SAFE_AREA_PADDING_LEFT_PROPERTY)
329 .unwrap_or_default();
330 let _: Result<Node, JsValue> = body.remove_child(&sentinel);
331 if top_value.is_empty() || top_value == "0px" {
332 return;
333 }
334 SAFE_AREA_INSET_TOP.with(|cell: &RefCell<String>| *cell.borrow_mut() = top_value);
335 SAFE_AREA_INSET_RIGHT.with(|cell: &RefCell<String>| *cell.borrow_mut() = right_value);
336 SAFE_AREA_INSET_BOTTOM.with(|cell: &RefCell<String>| *cell.borrow_mut() = bottom_value);
337 SAFE_AREA_INSET_LEFT.with(|cell: &RefCell<String>| *cell.borrow_mut() = left_value);
338 }
339
340 pub fn apply_cached_insets() {
356 let raw_top: String =
357 SAFE_AREA_INSET_TOP.with(|cell: &RefCell<String>| cell.borrow().clone());
358 let raw_right: String =
359 SAFE_AREA_INSET_RIGHT.with(|cell: &RefCell<String>| cell.borrow().clone());
360 let raw_bottom: String =
361 SAFE_AREA_INSET_BOTTOM.with(|cell: &RefCell<String>| cell.borrow().clone());
362 let raw_left: String =
363 SAFE_AREA_INSET_LEFT.with(|cell: &RefCell<String>| cell.borrow().clone());
364 if raw_top.is_empty()
365 && raw_right.is_empty()
366 && raw_bottom.is_empty()
367 && raw_left.is_empty()
368 {
369 return;
370 }
371 let is_trusted: bool =
372 is_edge_to_edge_viewport(Self::is_immersive_declared(), measure_viewport_screen_gap());
373 let top_value: String = safe_area_contract_value(is_trusted, &raw_top);
374 let right_value: String = safe_area_contract_value(is_trusted, &raw_right);
375 let bottom_value: String = safe_area_contract_value(is_trusted, &raw_bottom);
376 let left_value: String = safe_area_contract_value(is_trusted, &raw_left);
377 let Some(window_value) = window() else {
378 return;
379 };
380 let Some(document_value) = window_value.document() else {
381 return;
382 };
383 let apply_to: &dyn Fn(&HtmlElement) = &|element: &HtmlElement| {
384 let _: Result<(), JsValue> = element
385 .style()
386 .set_property(SAFE_AREA_INSET_TOP_PROPERTY, &top_value);
387 let _: Result<(), JsValue> = element
388 .style()
389 .set_property(SAFE_AREA_INSET_RIGHT_PROPERTY, &right_value);
390 let _: Result<(), JsValue> = element
391 .style()
392 .set_property(SAFE_AREA_INSET_BOTTOM_PROPERTY, &bottom_value);
393 let _: Result<(), JsValue> = element
394 .style()
395 .set_property(SAFE_AREA_INSET_LEFT_PROPERTY, &left_value);
396 let _: Result<(), JsValue> = element
397 .style()
398 .set_property(IMMERSIVE_SAFE_TOP_PROPERTY, &top_value);
399 let _: Result<(), JsValue> = element
400 .style()
401 .set_property(SAFE_AREA_RIGHT_PROPERTY, &right_value);
402 let _: Result<(), JsValue> = element
403 .style()
404 .set_property(SAFE_AREA_BOTTOM_PROPERTY, &bottom_value);
405 let _: Result<(), JsValue> = element
406 .style()
407 .set_property(SAFE_AREA_LEFT_PROPERTY, &left_value);
408 };
409 if let Some(app_root) = document_value
410 .query_selector(APP_ROOT_MOBILE_SELECTOR)
411 .ok()
412 .flatten()
413 .map(|element: Element| element.unchecked_into::<HtmlElement>())
414 .or_else(|| {
415 document_value
416 .query_selector(APP_ROOT_DESKTOP_SELECTOR)
417 .ok()
418 .flatten()
419 .map(|element: Element| element.unchecked_into::<HtmlElement>())
420 })
421 {
422 apply_to(&app_root);
423 }
424 if let Some(canvas_fullscreen) = document_value
425 .query_selector(FULLSCREEN_CANVAS_CONTAINER_SELECTOR)
426 .ok()
427 .flatten()
428 .map(|element: Element| element.unchecked_into::<HtmlElement>())
429 {
430 apply_to(&canvas_fullscreen);
431 }
432 if let Some(game_fullscreen) = document_value
433 .query_selector(FULLSCREEN_GAME_CONTAINER_SELECTOR)
434 .ok()
435 .flatten()
436 .map(|element: Element| element.unchecked_into::<HtmlElement>())
437 {
438 apply_to(&game_fullscreen);
439 }
440 }
441}