1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
use dioxus::prelude::*;
use dioxus_signals::use_signal;
use dioxus_signals::Signal;
use dioxus_use_mounted::UseMounted;
use js_sys::Array;
use web_sys::DomRectReadOnly;
use std::rc::Rc;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
use web_sys::ResizeObserver;
use web_sys::ResizeObserverEntry;

pub type Rect = DomRectReadOnly;

/// Hook to get an element's size, updating on changes.
/// 
/// ## Examples
/// ```
/// use dioxus::prelude::*;
/// use dioxus_resize_observer::use_size;
/// use dioxus_use_mounted::use_mounted;
///
/// fn app(cx: Scope) -> Element {
///     let mounted = use_mounted(cx);
///     let (width, height) = use_size(cx, mounted);
///
///     render!(div {
///         onmounted: move |event| mounted.onmounted(event),
///         "Size: {width} x {height}"
///     })
/// }
/// ```
pub fn use_size<T>(cx: Scope<T>, mounted: UseMounted) -> Rect {
    let resize = use_resize(cx, mounted);
    let resize_ref = resize.read();
    resize_ref.clone().unwrap_or_else(|| DomRectReadOnly::new().unwrap())
}

/// Hook to get an element's resize events as a signal.
pub fn use_resize<T>(cx: Scope<T>, mounted: UseMounted) -> Signal<Option<Rect>> {
    let state_ref: Signal<Option<State>> = use_signal(cx, || None);
    let size_ref = use_signal(cx, || None);

    dioxus_signals::use_effect(cx, move || {
        if let Some(mounted) = mounted.signal.read().clone() {
            // Unmount the previous element, if it exists
            maybe_unobserve(state_ref);

            // Create a new resize observer with an entry event handler.
            let on_resize = Closure::<dyn FnMut(Array)>::new(move |entries: Array| {
                let entry = entries.at(0);
                let entry: ResizeObserverEntry = entry.dyn_into().unwrap();
                size_ref.set(Some(entry.content_rect()));
            });
            let resize_observer = ResizeObserver::new(on_resize.as_ref().unchecked_ref()).unwrap();

            // Observe the raw element with the resize observer.
            let raw_elem = get_raw_element(&mounted);
            resize_observer.observe(raw_elem);

            // Update the current state.
            state_ref.set(Some(State {
                resize_observer,
                mounted,
                _on_resize: on_resize,
            }));
        } else {
            // Unmount the current element, if it exists
            maybe_unobserve(state_ref);
        }
    });

    size_ref
}

/// State of the hook.
struct State {
    /// JS resize observer.
    resize_observer: ResizeObserver,

    /// Currently mounted element data.
    mounted: Rc<MountedData>,

    /// Current closure handling resize observer events.
    _on_resize: Closure<dyn FnMut(Array)>,
}

/// Utility to get the raw element from its mounted data.
fn get_raw_element(mounted: &MountedData) -> &web_sys::Element {
    mounted
        .get_raw_element()
        .unwrap()
        .downcast_ref::<web_sys::Element>()
        .unwrap()
}

/// Attempt to unobserve an element, if it exists.
fn maybe_unobserve(state_ref: Signal<Option<State>>) {
    if let Some(state) = state_ref.write().take() {
        let raw_elem = get_raw_element(&state.mounted);
        state.resize_observer.unobserve(raw_elem);
    }
}