1use crate::schema::{WindowHeightClass, WindowSizeClass};
9use std::cell::{Cell, RefCell};
10use std::rc::Rc;
11use std::sync::Arc;
12
13#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct WindowSize {
21 pub width_dp: f32,
22 pub height_dp: f32,
23 pub class: WindowSizeClass,
24 pub height_class: WindowHeightClass,
25}
26
27impl WindowSize {
28 pub const fn new(
29 width_dp: f32,
30 height_dp: f32,
31 class: WindowSizeClass,
32 height_class: WindowHeightClass,
33 ) -> Self {
34 Self {
35 width_dp,
36 height_dp,
37 class,
38 height_class,
39 }
40 }
41
42 pub fn is_compact(&self) -> bool {
44 matches!(self.class, WindowSizeClass::Compact)
45 }
46
47 pub fn is_medium(&self) -> bool {
49 matches!(self.class, WindowSizeClass::Medium)
50 }
51
52 pub fn is_expanded(&self) -> bool {
54 matches!(self.class, WindowSizeClass::Expanded)
55 }
56
57 pub fn is_short(&self) -> bool {
60 matches!(self.height_class, WindowHeightClass::Compact)
61 }
62
63 pub fn is_medium_height(&self) -> bool {
65 matches!(self.height_class, WindowHeightClass::Medium)
66 }
67
68 pub fn is_tall(&self) -> bool {
70 matches!(self.height_class, WindowHeightClass::Expanded)
71 }
72}
73
74impl Default for WindowSize {
75 fn default() -> Self {
81 Self::new(
82 0.0,
83 0.0,
84 WindowSizeClass::Compact,
85 WindowHeightClass::Compact,
86 )
87 }
88}
89
90struct Subscriber {
91 id: u64,
92 notify: Arc<dyn Fn() + Send + Sync>,
93}
94
95thread_local! {
96 static NODES: RefCell<Vec<(u32, WindowSize)>> = const { RefCell::new(Vec::new()) };
101 static NODE_SUBSCRIBERS: RefCell<Vec<(u32, Subscriber)>> = const { RefCell::new(Vec::new()) };
102 static CURRENT: Cell<WindowSize> = const { Cell::new(WindowSize {
103 width_dp: 0.0,
104 height_dp: 0.0,
105 class: WindowSizeClass::Compact,
106 height_class: WindowHeightClass::Compact,
107 }) };
108 static SUBSCRIBERS: RefCell<Vec<Subscriber>> = const { RefCell::new(Vec::new()) };
109 static NEXT_SUBSCRIBER_ID: Cell<u64> = const { Cell::new(1) };
110}
111
112pub fn window_size() -> WindowSize {
114 CURRENT.with(Cell::get)
115}
116
117pub(crate) fn publish(size: WindowSize) -> bool {
122 if CURRENT.with(Cell::get) == size {
123 return false;
124 }
125 CURRENT.with(|current| current.set(size));
126 SUBSCRIBERS.with_borrow(|subscribers| {
127 for subscriber in subscribers {
128 (subscriber.notify)();
129 }
130 });
131 true
132}
133
134pub fn node_size(token: u32) -> WindowSize {
136 NODES.with_borrow(|nodes| {
137 nodes
138 .iter()
139 .find(|(name, _)| *name == token)
140 .map(|(_, size)| *size)
141 .unwrap_or_default()
142 })
143}
144
145pub(crate) fn publish_node(token: u32, size: WindowSize) -> bool {
150 let changed =
151 NODES.with_borrow_mut(
152 |nodes| match nodes.iter_mut().find(|(name, _)| *name == token) {
153 Some(entry) => {
154 if entry.1 == size {
155 return false;
156 }
157 entry.1 = size;
158 true
159 }
160 None => {
161 nodes.push((token, size));
162 true
163 }
164 },
165 );
166 if !changed {
167 return false;
168 }
169 NODE_SUBSCRIBERS.with_borrow(|subscribers| {
170 let mut woke = false;
171 for (name, subscriber) in subscribers {
172 if *name == token {
173 (subscriber.notify)();
174 woke = true;
175 }
176 }
177 woke
178 })
179}
180
181#[doc(hidden)]
183pub fn reset_window_size() {
184 CURRENT.with(|current| current.set(WindowSize::default()));
185 SUBSCRIBERS.with_borrow_mut(Vec::clear);
186 NODES.with_borrow_mut(Vec::clear);
187 NODE_SUBSCRIBERS.with_borrow_mut(Vec::clear);
188}
189
190struct WindowSizeSubscription {
192 id: u64,
193}
194
195impl WindowSizeSubscription {
196 fn new(notify: Arc<dyn Fn() + Send + Sync>) -> Self {
197 let id = NEXT_SUBSCRIBER_ID.with(|next| {
198 let id = next.get();
199 next.set(id + 1);
200 id
201 });
202 SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.push(Subscriber { id, notify }));
203 Self { id }
204 }
205}
206
207impl Drop for WindowSizeSubscription {
208 fn drop(&mut self) {
209 SUBSCRIBERS.with_borrow_mut(|subscribers| {
210 subscribers.retain(|subscriber| subscriber.id != self.id);
211 });
212 }
213}
214
215#[derive(Clone, Copy, Debug, PartialEq)]
250pub struct NodeSize {
251 token: u32,
252 size: WindowSize,
253}
254
255impl NodeSize {
256 pub const fn token(&self) -> i64 {
258 self.token as i64
259 }
260
261 pub fn is_compact(&self) -> bool {
263 self.size.is_compact()
264 }
265
266 pub fn is_medium(&self) -> bool {
268 self.size.is_medium()
269 }
270
271 pub fn is_expanded(&self) -> bool {
273 self.size.is_expanded()
274 }
275
276 pub const fn measured(&self) -> WindowSize {
278 self.size
279 }
280}
281
282thread_local! {
283 static NEXT_TOKEN: Cell<u32> = const { Cell::new(1) };
284}
285
286pub fn use_node_size() -> NodeSize {
288 let token = dioxus_core::use_hook(|| {
289 let token = NEXT_TOKEN.with(|next| {
290 let token = next.get();
291 next.set(token + 1);
292 token
293 });
294 Rc::new(NodeSizeSubscription::new(
295 token,
296 dioxus_core::schedule_update(),
297 ))
298 });
299 NodeSize {
300 token: token.token,
301 size: node_size(token.token),
302 }
303}
304
305struct NodeSizeSubscription {
307 token: u32,
308 id: u64,
309}
310
311impl NodeSizeSubscription {
312 fn new(token: u32, notify: Arc<dyn Fn() + Send + Sync>) -> Self {
313 let id = NEXT_SUBSCRIBER_ID.with(|next| {
314 let id = next.get();
315 next.set(id + 1);
316 id
317 });
318 NODE_SUBSCRIBERS.with_borrow_mut(|subscribers| {
319 subscribers.push((token, Subscriber { id, notify }));
320 });
321 Self { token, id }
322 }
323}
324
325impl Drop for NodeSizeSubscription {
326 fn drop(&mut self) {
327 NODE_SUBSCRIBERS.with_borrow_mut(|subscribers| {
330 subscribers.retain(|(_, subscriber)| subscriber.id != self.id);
331 });
332 NODES.with_borrow_mut(|nodes| nodes.retain(|(token, _)| *token != self.token));
333 }
334}
335
336pub fn use_window_size() -> WindowSize {
337 dioxus_core::use_hook(|| Rc::new(WindowSizeSubscription::new(dioxus_core::schedule_update())));
341 window_size()
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use crate::boundary::Host;
348 use crate::prelude::*;
349 use crate::protocol::{HostEvent, Mutation, PropertyValue, decode_batch};
350 use crate::schema::{EventPayload, PropertyKind};
351 use std::sync::atomic::{AtomicUsize, Ordering};
352
353 static RESPONSIVE_RENDERS: AtomicUsize = AtomicUsize::new(0);
354 static SIBLING_RENDERS: AtomicUsize = AtomicUsize::new(0);
355
356 #[component]
357 fn Responsive() -> Element {
358 RESPONSIVE_RENDERS.fetch_add(1, Ordering::SeqCst);
359 let window = use_window_size();
360 let label = if window.is_expanded() {
361 "sidebar"
362 } else if window.is_medium() {
363 "two columns"
364 } else {
365 "one column"
366 };
367 rsx! { Text { text: label } }
368 }
369
370 #[component]
371 fn ShortOrTall() -> Element {
372 let window = use_window_size();
373 let label = if window.is_tall() {
374 "tall"
375 } else if window.is_medium_height() {
376 "ordinary"
377 } else {
378 "short"
379 };
380 rsx! { Text { text: label } }
381 }
382
383 fn height_app() -> Element {
384 rsx! { Column { ShortOrTall {} } }
385 }
386
387 #[component]
388 fn Sibling() -> Element {
389 SIBLING_RENDERS.fetch_add(1, Ordering::SeqCst);
390 rsx! { Text { text: "fixed" } }
391 }
392
393 fn responsive_app() -> Element {
394 rsx! {
395 Column {
396 Responsive {}
397 Sibling {}
398 }
399 }
400 }
401
402 fn resize(host: &mut Host, width_dp: f32) -> Vec<String> {
403 resize_to(host, width_dp, 800.0)
404 }
405
406 fn resize_to(host: &mut Host, width_dp: f32, height_dp: f32) -> Vec<String> {
407 let event = HostEvent {
408 node_id: 0,
409 handler_id: 0,
410 payload: EventPayload::WindowSizeChanged {
411 width_dp,
412 height_dp,
413 class: WindowSizeClass::from_width_dp(width_dp),
414 height_class: WindowHeightClass::from_height_dp(height_dp),
415 },
416 };
417 let (batch, _) = host.dispatch(event).unwrap();
418 decode_batch(batch)
419 .unwrap()
420 .iter()
421 .filter_map(|mutation| match mutation {
422 Mutation::SetProp {
423 property: PropertyKind::Text,
424 value: PropertyValue::String(value),
425 ..
426 } => Some((*value).to_owned()),
427 _ => None,
428 })
429 .collect()
430 }
431
432 #[test]
433 fn fr20_crossing_a_boundary_rerenders_the_hook_and_resizing_within_a_class_does_not() {
434 reset_window_size();
435 RESPONSIVE_RENDERS.store(0, Ordering::SeqCst);
436 SIBLING_RENDERS.store(0, Ordering::SeqCst);
437 let mut host = Host::new(responsive_app);
438 host.rebuild().unwrap();
439 assert_eq!(RESPONSIVE_RENDERS.load(Ordering::SeqCst), 1);
440 assert_eq!(SIBLING_RENDERS.load(Ordering::SeqCst), 1);
441
442 assert_eq!(resize(&mut host, 700.0), vec!["two columns".to_owned()]);
444 assert_eq!(RESPONSIVE_RENDERS.load(Ordering::SeqCst), 2);
445 assert_eq!(SIBLING_RENDERS.load(Ordering::SeqCst), 1);
446
447 assert!(resize(&mut host, 700.0).is_empty());
450 assert_eq!(RESPONSIVE_RENDERS.load(Ordering::SeqCst), 2);
451
452 assert_eq!(resize(&mut host, 900.0), vec!["sidebar".to_owned()]);
453 assert_eq!(RESPONSIVE_RENDERS.load(Ordering::SeqCst), 3);
454 assert_eq!(SIBLING_RENDERS.load(Ordering::SeqCst), 1);
455 reset_window_size();
456 }
457
458 #[test]
459 fn fr20_crossing_a_height_boundary_rerenders_and_resizing_within_one_does_not() {
460 reset_window_size();
461 let mut host = Host::new(height_app);
462 host.rebuild().unwrap();
463
464 assert!(resize_to(&mut host, 400.0, 479.0).is_empty());
467
468 assert_eq!(
471 resize_to(&mut host, 400.0, 480.0),
472 vec!["ordinary".to_owned()]
473 );
474 assert!(resize_to(&mut host, 400.0, 899.0).is_empty());
475 assert_eq!(resize_to(&mut host, 400.0, 900.0), vec!["tall".to_owned()]);
476 reset_window_size();
477 }
478
479 #[test]
480 fn fr20_window_size_starts_compact_before_the_renderer_measures_anything() {
481 reset_window_size();
482 assert_eq!(window_size(), WindowSize::default());
483 assert!(window_size().is_compact());
484 }
485
486 #[test]
487 fn fr20_publishing_the_same_size_twice_wakes_nobody() {
488 reset_window_size();
489 let size = WindowSize::new(
490 700.0,
491 800.0,
492 WindowSizeClass::Medium,
493 WindowHeightClass::Medium,
494 );
495 assert!(publish(size));
496 assert!(!publish(size));
497 reset_window_size();
498 }
499}