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