use crate::schema::{WindowHeightClass, WindowSizeClass};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WindowSize {
pub width_dp: f32,
pub height_dp: f32,
pub class: WindowSizeClass,
pub height_class: WindowHeightClass,
}
impl WindowSize {
pub const fn new(
width_dp: f32,
height_dp: f32,
class: WindowSizeClass,
height_class: WindowHeightClass,
) -> Self {
Self {
width_dp,
height_dp,
class,
height_class,
}
}
pub fn is_compact(&self) -> bool {
matches!(self.class, WindowSizeClass::Compact)
}
pub fn is_medium(&self) -> bool {
matches!(self.class, WindowSizeClass::Medium)
}
pub fn is_expanded(&self) -> bool {
matches!(self.class, WindowSizeClass::Expanded)
}
pub fn is_short(&self) -> bool {
matches!(self.height_class, WindowHeightClass::Compact)
}
pub fn is_medium_height(&self) -> bool {
matches!(self.height_class, WindowHeightClass::Medium)
}
pub fn is_tall(&self) -> bool {
matches!(self.height_class, WindowHeightClass::Expanded)
}
}
impl Default for WindowSize {
fn default() -> Self {
Self::new(
0.0,
0.0,
WindowSizeClass::Compact,
WindowHeightClass::Compact,
)
}
}
struct Subscriber {
id: u64,
notify: Arc<dyn Fn() + Send + Sync>,
}
thread_local! {
static NODES: RefCell<Vec<(u32, WindowSize)>> = const { RefCell::new(Vec::new()) };
static NODE_SUBSCRIBERS: RefCell<Vec<(u32, Subscriber)>> = const { RefCell::new(Vec::new()) };
static CURRENT: Cell<WindowSize> = const { Cell::new(WindowSize {
width_dp: 0.0,
height_dp: 0.0,
class: WindowSizeClass::Compact,
height_class: WindowHeightClass::Compact,
}) };
static SUBSCRIBERS: RefCell<Vec<Subscriber>> = const { RefCell::new(Vec::new()) };
static NEXT_SUBSCRIBER_ID: Cell<u64> = const { Cell::new(1) };
}
pub fn window_size() -> WindowSize {
CURRENT.with(Cell::get)
}
pub(crate) fn publish(size: WindowSize) -> bool {
if CURRENT.with(Cell::get) == size {
return false;
}
CURRENT.with(|current| current.set(size));
SUBSCRIBERS.with_borrow(|subscribers| {
for subscriber in subscribers {
(subscriber.notify)();
}
});
true
}
pub fn node_size(token: u32) -> WindowSize {
NODES.with_borrow(|nodes| {
nodes
.iter()
.find(|(name, _)| *name == token)
.map(|(_, size)| *size)
.unwrap_or_default()
})
}
pub(crate) fn publish_node(token: u32, size: WindowSize) -> bool {
let changed =
NODES.with_borrow_mut(
|nodes| match nodes.iter_mut().find(|(name, _)| *name == token) {
Some(entry) => {
if entry.1 == size {
return false;
}
entry.1 = size;
true
}
None => {
nodes.push((token, size));
true
}
},
);
if !changed {
return false;
}
NODE_SUBSCRIBERS.with_borrow(|subscribers| {
let mut woke = false;
for (name, subscriber) in subscribers {
if *name == token {
(subscriber.notify)();
woke = true;
}
}
woke
})
}
#[doc(hidden)]
pub fn reset_window_size() {
CURRENT.with(|current| current.set(WindowSize::default()));
SUBSCRIBERS.with_borrow_mut(Vec::clear);
NODES.with_borrow_mut(Vec::clear);
NODE_SUBSCRIBERS.with_borrow_mut(Vec::clear);
}
struct WindowSizeSubscription {
id: u64,
}
impl WindowSizeSubscription {
fn new(notify: Arc<dyn Fn() + Send + Sync>) -> Self {
let id = NEXT_SUBSCRIBER_ID.with(|next| {
let id = next.get();
next.set(id + 1);
id
});
SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.push(Subscriber { id, notify }));
Self { id }
}
}
impl Drop for WindowSizeSubscription {
fn drop(&mut self) {
SUBSCRIBERS.with_borrow_mut(|subscribers| {
subscribers.retain(|subscriber| subscriber.id != self.id);
});
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NodeSize {
token: u32,
size: WindowSize,
}
impl NodeSize {
pub const fn token(&self) -> i64 {
self.token as i64
}
pub fn is_compact(&self) -> bool {
self.size.is_compact()
}
pub fn is_medium(&self) -> bool {
self.size.is_medium()
}
pub fn is_expanded(&self) -> bool {
self.size.is_expanded()
}
pub const fn measured(&self) -> WindowSize {
self.size
}
}
thread_local! {
static NEXT_TOKEN: Cell<u32> = const { Cell::new(1) };
}
pub fn use_node_size() -> NodeSize {
let token = dioxus_core::use_hook(|| {
let token = NEXT_TOKEN.with(|next| {
let token = next.get();
next.set(token + 1);
token
});
Rc::new(NodeSizeSubscription::new(
token,
dioxus_core::schedule_update(),
))
});
NodeSize {
token: token.token,
size: node_size(token.token),
}
}
struct NodeSizeSubscription {
token: u32,
id: u64,
}
impl NodeSizeSubscription {
fn new(token: u32, notify: Arc<dyn Fn() + Send + Sync>) -> Self {
let id = NEXT_SUBSCRIBER_ID.with(|next| {
let id = next.get();
next.set(id + 1);
id
});
NODE_SUBSCRIBERS.with_borrow_mut(|subscribers| {
subscribers.push((token, Subscriber { id, notify }));
});
Self { token, id }
}
}
impl Drop for NodeSizeSubscription {
fn drop(&mut self) {
NODE_SUBSCRIBERS.with_borrow_mut(|subscribers| {
subscribers.retain(|(_, subscriber)| subscriber.id != self.id);
});
NODES.with_borrow_mut(|nodes| nodes.retain(|(token, _)| *token != self.token));
}
}
pub fn use_window_size() -> WindowSize {
dioxus_core::use_hook(|| Rc::new(WindowSizeSubscription::new(dioxus_core::schedule_update())));
window_size()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::boundary::Host;
use crate::prelude::*;
use crate::protocol::{HostEvent, Mutation, PropertyValue, decode_batch};
use crate::schema::{EventPayload, PropertyKind};
use std::sync::atomic::{AtomicUsize, Ordering};
static RESPONSIVE_RENDERS: AtomicUsize = AtomicUsize::new(0);
static SIBLING_RENDERS: AtomicUsize = AtomicUsize::new(0);
#[component]
fn Responsive() -> Element {
RESPONSIVE_RENDERS.fetch_add(1, Ordering::SeqCst);
let window = use_window_size();
let label = if window.is_expanded() {
"sidebar"
} else if window.is_medium() {
"two columns"
} else {
"one column"
};
rsx! { Text { text: label } }
}
#[component]
fn ShortOrTall() -> Element {
let window = use_window_size();
let label = if window.is_tall() {
"tall"
} else if window.is_medium_height() {
"ordinary"
} else {
"short"
};
rsx! { Text { text: label } }
}
fn height_app() -> Element {
rsx! { Column { ShortOrTall {} } }
}
#[component]
fn Sibling() -> Element {
SIBLING_RENDERS.fetch_add(1, Ordering::SeqCst);
rsx! { Text { text: "fixed" } }
}
fn responsive_app() -> Element {
rsx! {
Column {
Responsive {}
Sibling {}
}
}
}
fn resize(host: &mut Host, width_dp: f32) -> Vec<String> {
resize_to(host, width_dp, 800.0)
}
fn resize_to(host: &mut Host, width_dp: f32, height_dp: f32) -> Vec<String> {
let event = HostEvent {
node_id: 0,
handler_id: 0,
payload: EventPayload::WindowSizeChanged {
width_dp,
height_dp,
class: WindowSizeClass::from_width_dp(width_dp),
height_class: WindowHeightClass::from_height_dp(height_dp),
},
};
let (batch, _) = host.dispatch(event).unwrap();
decode_batch(batch)
.unwrap()
.iter()
.filter_map(|mutation| match mutation {
Mutation::SetProp {
property: PropertyKind::Text,
value: PropertyValue::String(value),
..
} => Some((*value).to_owned()),
_ => None,
})
.collect()
}
#[test]
fn fr20_crossing_a_boundary_rerenders_the_hook_and_resizing_within_a_class_does_not() {
reset_window_size();
RESPONSIVE_RENDERS.store(0, Ordering::SeqCst);
SIBLING_RENDERS.store(0, Ordering::SeqCst);
let mut host = Host::new(responsive_app);
host.rebuild().unwrap();
assert_eq!(RESPONSIVE_RENDERS.load(Ordering::SeqCst), 1);
assert_eq!(SIBLING_RENDERS.load(Ordering::SeqCst), 1);
assert_eq!(resize(&mut host, 700.0), vec!["two columns".to_owned()]);
assert_eq!(RESPONSIVE_RENDERS.load(Ordering::SeqCst), 2);
assert_eq!(SIBLING_RENDERS.load(Ordering::SeqCst), 1);
assert!(resize(&mut host, 700.0).is_empty());
assert_eq!(RESPONSIVE_RENDERS.load(Ordering::SeqCst), 2);
assert_eq!(resize(&mut host, 900.0), vec!["sidebar".to_owned()]);
assert_eq!(RESPONSIVE_RENDERS.load(Ordering::SeqCst), 3);
assert_eq!(SIBLING_RENDERS.load(Ordering::SeqCst), 1);
reset_window_size();
}
#[test]
fn fr20_crossing_a_height_boundary_rerenders_and_resizing_within_one_does_not() {
reset_window_size();
let mut host = Host::new(height_app);
host.rebuild().unwrap();
assert!(resize_to(&mut host, 400.0, 479.0).is_empty());
assert_eq!(
resize_to(&mut host, 400.0, 480.0),
vec!["ordinary".to_owned()]
);
assert!(resize_to(&mut host, 400.0, 899.0).is_empty());
assert_eq!(resize_to(&mut host, 400.0, 900.0), vec!["tall".to_owned()]);
reset_window_size();
}
#[test]
fn fr20_window_size_starts_compact_before_the_renderer_measures_anything() {
reset_window_size();
assert_eq!(window_size(), WindowSize::default());
assert!(window_size().is_compact());
}
#[test]
fn fr20_publishing_the_same_size_twice_wakes_nobody() {
reset_window_size();
let size = WindowSize::new(
700.0,
800.0,
WindowSizeClass::Medium,
WindowHeightClass::Medium,
);
assert!(publish(size));
assert!(!publish(size));
reset_window_size();
}
}