Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d6d4a1d
wip: initial leptos port for use-rect
omar-mohamed-khallaf Jun 16, 2025
2084f92
wip: complete implementation
omar-mohamed-khallaf Jun 16, 2025
7abb9a8
Merge branch 'RustForWeb:main' into main
omar-mohamed-khallaf Jun 16, 2025
4b3a8db
Don't call callbacks with the lock held
omar-mohamed-khallaf Jun 16, 2025
641e780
Optimize searching for observed element
omar-mohamed-khallaf Jun 16, 2025
d7e5fdf
Merge branch 'RustForWeb:main' into main
omar-mohamed-khallaf Jun 16, 2025
c2bedcd
replace Arc with Rc
omar-mohamed-khallaf Jun 16, 2025
39fe6a2
use borderbox for ResizeObserver option
omar-mohamed-khallaf Jun 16, 2025
539fb3d
rename variable
omar-mohamed-khallaf Jun 17, 2025
f4b4bd6
Hashmap implementation for use-rect
omar-mohamed-khallaf Jun 17, 2025
01ff2d1
refactor: new implementation using js_sys
omar-mohamed-khallaf Jun 17, 2025
3a56d34
fix: dependencies
omar-mohamed-khallaf Jun 17, 2025
336b59b
fix: use_rect
omar-mohamed-khallaf Jun 17, 2025
a6e22b1
fix: use_rect
omar-mohamed-khallaf Jun 17, 2025
c5b0260
Revert "fix: use_rect"
omar-mohamed-khallaf Jun 17, 2025
da54910
fix: call callbacks when the element is found
omar-mohamed-khallaf Jun 17, 2025
c57d8ee
feat: generalize observe_element and use it for use_size
omar-mohamed-khallaf Jun 17, 2025
2bc4835
use dyn_ref when possible
omar-mohamed-khallaf Jun 17, 2025
4fa5c81
fix typo
omar-mohamed-khallaf Jun 17, 2025
a354d34
fix: logical error
omar-mohamed-khallaf Jun 17, 2025
511d127
chores
omar-mohamed-khallaf Jun 17, 2025
769b865
fix: remove duplicate search for element in map
omar-mohamed-khallaf Jun 18, 2025
a566886
refactor
omar-mohamed-khallaf Jun 18, 2025
4440706
fix: remove duplicate set of size on initial observation
omar-mohamed-khallaf Jun 18, 2025
3892a47
Merge branch 'main' into main
omar-mohamed-khallaf Jun 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
506 changes: 260 additions & 246 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ members = [
"book-examples/*/*",
"packages/colors",
"packages/icons/*",
"packages/primitives/core/observe",
"packages/primitives/leptos/accessible-icon",
"packages/primitives/leptos/arrow",
"packages/primitives/leptos/aspect-ratio",
Expand All @@ -28,6 +29,7 @@ members = [
"packages/primitives/leptos/use-controllable-state",
"packages/primitives/leptos/use-escape-keydown",
"packages/primitives/leptos/use-previous",
"packages/primitives/leptos/use-rect",
"packages/primitives/leptos/use-size",
"packages/primitives/leptos/visually-hidden",
"packages/primitives/yew/*",
Expand Down Expand Up @@ -78,8 +80,10 @@ radix-leptos-separator = { path = "./packages/primitives/leptos/separator", vers
radix-leptos-visually-hidden = { path = "./packages/primitives/leptos/visually-hidden", version = "0.0.2" }
radix-leptos-use-controllable-state = { path = "./packages/primitives/leptos/use-controllable-state", version = "0.0.2" }
radix-leptos-use-previous = { path = "./packages/primitives/leptos/use-previous", version = "0.0.2" }
radix-leptos-use-rect = { path = "./packages/primitives/leptos/use-rect", version = "0.0.2" }
radix-leptos-use-size = { path = "./packages/primitives/leptos/use-size", version = "0.0.2" }
radix-number = { path = "./packages/primitives/core/number", version = "0.0.2" }
radix-observe = { path = "./packages/primitives/core/observe", version = "0.0.2" }
radix-yew-arrow = { path = "./packages/primitives/yew/arrow", version = "0.0.2" }
radix-yew-aspect-ratio = { path = "./packages/primitives/yew/aspect-ratio", version = "0.0.2" }
radix-yew-avatar = { path = "./packages/primitives/yew/avatar", version = "0.0.2" }
Expand Down
21 changes: 21 additions & 0 deletions packages/primitives/core/observe/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "radix-observe"
description = "Rust alternative of Radix Core Rect."

authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true

[dependencies]
web-sys = { workspace = true, features = [
"DomRect",
"Element",
"ResizeObserver",
"ResizeObserverBoxOptions",
"ResizeObserverEntry",
"ResizeObserverOptions",
"ResizeObserverSize",
] }
send_wrapper.workspace = true
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ See [the Rust Radix book](https://radix.rustforweb.org/) for documentation.
The Rust Radix project is part of [Rust for Web](https://github.com/RustForWeb).

[Rust for Web](https://github.com/RustForWeb) creates and ports web UI libraries for Rust. All projects are free and open source.

## Modifications

This implementation is generalized where the callbacks are provided with the `ResizeObserverEntry`
directly so any dependants that need to observe changes can use it.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
//!
//! See [`@radix-ui/rect`](https://www.npmjs.com/package/@radix-ui/rect) for the original package.

mod observe_element_rect;
mod observe_element;

pub use observe_element_rect::*;
pub use observe_element::*;
144 changes: 144 additions & 0 deletions packages/primitives/core/observe/src/observe_element.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use std::sync::LazyLock;

use send_wrapper::SendWrapper;
use web_sys::js_sys;
use web_sys::wasm_bindgen::prelude::*;
use web_sys::{
Element, ResizeObserver, ResizeObserverBoxOptions, ResizeObserverEntry, ResizeObserverOptions,
};

pub type UnobserveCallback = Box<dyn Fn() + Send + Sync>;

static OBSERVED_ELEMENTS: LazyLock<SendWrapper<js_sys::Map>> =
LazyLock::new(|| SendWrapper::new(js_sys::Map::new()));

static RESIZE_OBSERVER: LazyLock<SendWrapper<ResizeObserver>> = LazyLock::new(|| {
let callback: Closure<dyn Fn(Vec<ResizeObserverEntry>)> =
Closure::new(|entries: Vec<ResizeObserverEntry>| {
for entry in entries {
let target = entry.target();

let observed_data = OBSERVED_ELEMENTS.get(&target);

// cannot happen
if observed_data == JsValue::UNDEFINED {
// RESIZE_OBSERVER.unobserve(&target);
} else {
let observed_data = observed_data
.dyn_ref::<js_sys::Object>()
.expect("Object should be of type ObservedData");

js_sys::Reflect::set(observed_data, &"entry".into(), &entry)
.expect("Should be able to set entry");

let callbacks = js_sys::Reflect::get(observed_data, &"callbacks".into())
.expect("Object should have callbacks array")
.dyn_into::<js_sys::Array>()
.expect("Object should be of type array");

callbacks.for_each(&mut |obj, _, _| {
if let Some(callback) = obj.dyn_ref::<js_sys::Function>() {
callback
.call1(&JsValue::NULL, &entry)
.expect("callback should be called");
}
});
}
}
});

SendWrapper::new(
ResizeObserver::new(callback.into_js_value().unchecked_ref())
.expect("Resize observer should be created."),
)
});

pub fn observe_element<C>(element_to_observe: &Element, callback: C) -> UnobserveCallback
where
C: Fn(ResizeObserverEntry) + 'static + Send + Sync,
{
let callback: Closure<dyn Fn(ResizeObserverEntry)> =
Closure::new(move |entry: ResizeObserverEntry| callback(entry));
let callback = callback.into_js_value();

let observed_data = OBSERVED_ELEMENTS.get(element_to_observe);
if observed_data == JsValue::UNDEFINED {
let obj = js_sys::Object::new();
let callbacks = js_sys::Array::new();
callbacks.push(callback.unchecked_ref());

js_sys::Reflect::set(&obj, &"entry".into(), &JsValue::NULL)
.expect("Should be able to set entry");
js_sys::Reflect::set(&obj, &"callbacks".into(), &callbacks.into())
.expect("Should be able to set callbacks");

OBSERVED_ELEMENTS.set(element_to_observe, &obj);

let options = ResizeObserverOptions::new();
options.set_box(ResizeObserverBoxOptions::BorderBox);

RESIZE_OBSERVER.observe_with_options(element_to_observe, &options);
} else {
let observed_data = observed_data
.dyn_ref::<js_sys::Object>()
.expect("observed data type should be object");

let callbacks = js_sys::Reflect::get(observed_data, &"callbacks".into())
.expect("Object should have callbacks array")
.dyn_into::<js_sys::Array>()
.expect("Object should be of type array");

callbacks.push(callback.unchecked_ref());

if let Some(callback) = callbacks
.get(callbacks.length() - 1)
.dyn_ref::<js_sys::Function>()
{
let entry = js_sys::Reflect::get(observed_data, &"entry".into())
.expect("ObservedData should have entry");

if entry != JsValue::NULL {
callback
.call1(
&JsValue::NULL,
entry
.dyn_ref::<ResizeObserverEntry>()
.expect("entry should be of type ResizeObserverEntry"),
)
.expect("callback should be called");
}
}
}

let wrapped_element = SendWrapper::new(element_to_observe.clone());
let wrapped_closure = SendWrapper::new(callback);
Box::new(move || {
let element = &*wrapped_element;
let callback = &*wrapped_closure;
let observed_data = OBSERVED_ELEMENTS.get(element);

if observed_data == JsValue::UNDEFINED {
return;
}

let observed_data = observed_data
.dyn_ref::<js_sys::Object>()
.expect("observed data type should be object");

let callbacks = js_sys::Reflect::get(observed_data, &"callbacks".into())
.expect("Object should have callbacks array")
.dyn_into::<js_sys::Array>()
.expect("Object should be of type array");

let index = callbacks.index_of(callback.unchecked_ref(), 0);

if let Ok(index) = u32::try_from(index) {
callbacks.splice(index, 1, &JsValue::NULL);
}

if callbacks.length() == 0 {
RESIZE_OBSERVER.unobserve(element);
OBSERVED_ELEMENTS.delete(element);
}
})
}
11 changes: 0 additions & 11 deletions packages/primitives/core/rect/Cargo.toml

This file was deleted.

1 change: 0 additions & 1 deletion packages/primitives/core/rect/src/observe_element_rect.rs

This file was deleted.

16 changes: 16 additions & 0 deletions packages/primitives/leptos/use-rect/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "radix-leptos-use-rect"
description = "Leptos port of Radix Use Rect."

authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true

[dependencies]
leptos.workspace = true
leptos-node-ref.workspace = true
send_wrapper.workspace = true
web-sys.workspace = true
radix-observe.workspace = true
49 changes: 49 additions & 0 deletions packages/primitives/leptos/use-rect/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use std::sync::{Arc, Mutex};

use leptos::prelude::*;
use leptos_node_ref::AnyNodeRef;
use radix_observe::observe_element;
use send_wrapper::SendWrapper;
use web_sys::{DomRect, wasm_bindgen::JsCast};

/// Provides a signal that monitors a node size changes
///
/// # Panics
///
/// Panics if failed to acquire the lock
#[must_use]
pub fn use_rect(element_ref: AnyNodeRef) -> ReadSignal<Option<SendWrapper<DomRect>>> {
let (rect, set_rect) = signal::<Option<SendWrapper<DomRect>>>(None);
let unobserve = Arc::new(Mutex::new(None));
let unobserve_clone = unobserve.clone();

Effect::new(move |_| {
if let Some(element) = element_ref
.get()
.and_then(|element| element.dyn_into::<web_sys::HtmlElement>().ok())
{
let cleanup = observe_element(&element, move |entry| {
set_rect.set(Some(SendWrapper::new(
entry.target().get_bounding_client_rect(),
)));
});

unobserve
.lock()
.expect("Lock should be acquired.")
.replace(cleanup);
}
});

on_cleanup(move || {
if let Some(cleanup) = unobserve_clone
.lock()
.expect("Lock should be acquired.")
.as_ref()
{
cleanup();
}
});

rect
}
9 changes: 2 additions & 7 deletions packages/primitives/leptos/use-size/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,5 @@ version.workspace = true
leptos.workspace = true
leptos-node-ref.workspace = true
send_wrapper.workspace = true
web-sys = { workspace = true, features = [
"ResizeObserver",
"ResizeObserverBoxOptions",
"ResizeObserverEntry",
"ResizeObserverOptions",
"ResizeObserverSize",
] }
web-sys.workspace = true
radix-observe.workspace = true
Loading
Loading