1use frus_core::{Rect, Scene, SizeClass};
11use frus_layout::Style;
12
13use crate::interaction::{Key, Status};
14use crate::portal::Placement;
15use crate::runtime::Edit;
16use crate::scroll::Axis;
17use crate::theme::Theme;
18use crate::widget::Widget;
19
20pub struct Responsive<Msg = crate::callback::Callback> {
22 class_rank: u8,
23 inner: Option<Box<dyn Widget<Msg>>>,
24 inner_rank: u8,
25}
26
27fn closeness(rank: u8, class: u8) -> (u8, bool) {
30 (rank.abs_diff(class), rank > class)
31}
32
33impl<Msg> Responsive<Msg> {
34 pub fn new(class: SizeClass) -> Self {
36 Self {
37 class_rank: class.rank(),
38 inner: None,
39 inner_rank: 0,
40 }
41 }
42
43 fn consider(mut self, rank: u8, widget: impl Widget<Msg> + 'static) -> Self {
46 let better = self.inner.is_none()
47 || closeness(rank, self.class_rank) < closeness(self.inner_rank, self.class_rank);
48 if better {
49 self.inner = Some(Box::new(widget));
50 self.inner_rank = rank;
51 }
52 self
53 }
54
55 pub fn compact(self, widget: impl Widget<Msg> + 'static) -> Self {
57 self.consider(SizeClass::Compact.rank(), widget)
58 }
59
60 pub fn medium(self, widget: impl Widget<Msg> + 'static) -> Self {
62 self.consider(SizeClass::Medium.rank(), widget)
63 }
64
65 pub fn expanded(self, widget: impl Widget<Msg> + 'static) -> Self {
67 self.consider(SizeClass::Expanded.rank(), widget)
68 }
69}
70
71pub fn responsive<Msg>(width: f32) -> Responsive<Msg> {
75 Responsive::new(SizeClass::from_width(width))
76}
77
78impl<Msg> Widget<Msg> for Responsive<Msg> {
79 fn style(&self) -> Style {
80 self.inner.as_ref().map(|w| w.style()).unwrap_or_default()
81 }
82
83 fn style_themed(&self, theme: &Theme) -> Style {
84 self.inner
85 .as_ref()
86 .map(|w| w.style_themed(theme))
87 .unwrap_or_default()
88 }
89
90 fn build_themed(&self, theme: &Theme) {
96 if let Some(w) = &self.inner {
97 w.build_themed(theme);
98 }
99 }
100
101 fn build_in(
102 &self,
103 id: crate::interaction::WidgetId,
104 runtime: &crate::runtime::Runtime,
105 theme: &Theme,
106 ) {
107 if let Some(w) = &self.inner {
108 w.build_in(id, runtime, theme);
109 }
110 }
111
112 fn expand(
113 &self,
114 id: crate::interaction::WidgetId,
115 runtime: &crate::runtime::Runtime,
116 theme: &Theme,
117 ) {
118 if let Some(w) = &self.inner {
119 w.expand(id, runtime, theme);
120 }
121 }
122
123 fn switches(&self) -> bool {
124 self.inner.as_ref().is_some_and(|w| w.switches())
125 }
126
127 fn system_ui_style(&self) -> Option<crate::SystemUiOverlayStyle> {
128 self.inner.as_ref().and_then(|w| w.system_ui_style())
129 }
130
131 fn debug_name(&self) -> &'static str {
132 self.inner
134 .as_ref()
135 .map(|w| w.debug_name())
136 .unwrap_or("Responsive")
137 }
138
139 fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
140 self.inner.as_ref().and_then(|w| w.semantics())
141 }
142
143 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
144 self.inner.as_ref().map(|w| w.children()).unwrap_or(&[])
145 }
146
147 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
148 if let Some(w) = &self.inner {
149 w.paint(bounds, status, theme, scene);
150 }
151 }
152
153 fn on_click(&self) -> Option<Msg> {
154 self.inner.as_ref().and_then(|w| w.on_click())
155 }
156
157 fn opaque(&self) -> bool {
158 self.inner.as_ref().is_some_and(|w| w.opaque())
159 }
160
161 fn positional_click(&self, local_x: f32, local_y: f32, width: f32, height: f32) -> Option<Msg> {
162 self.inner
163 .as_ref()
164 .and_then(|w| w.positional_click(local_x, local_y, width, height))
165 }
166
167 fn cursor_icon(
168 &self,
169 local_x: f32,
170 local_y: f32,
171 width: f32,
172 height: f32,
173 ) -> Option<crate::interaction::Cursor> {
174 self.inner
175 .as_ref()
176 .and_then(|w| w.cursor_icon(local_x, local_y, width, height))
177 }
178
179 fn key(&self) -> Option<u64> {
180 self.inner.as_ref().and_then(|w| w.key())
181 }
182
183 fn on_edit(&self, edit: &mut Edit, key: &Key) -> Option<Msg> {
184 self.inner.as_ref().and_then(|w| w.on_edit(edit, key))
185 }
186
187 fn cursor_at(
188 &self,
189 local_x: f32,
190 local_y: f32,
191 width: f32,
192 scroll_cursor: usize,
193 ) -> Option<usize> {
194 self.inner
195 .as_ref()
196 .and_then(|w| w.cursor_at(local_x, local_y, width, scroll_cursor))
197 }
198
199 fn text_metrics(&self, width: f32, cursor: usize) -> Option<(f32, f32, f32, f32)> {
200 self.inner
201 .as_ref()
202 .and_then(|w| w.text_metrics(width, cursor))
203 }
204
205 fn text_viewport(&self, rect: frus_core::Rect) -> Option<frus_core::Rect> {
206 self.inner.as_ref().and_then(|w| w.text_viewport(rect))
207 }
208
209 fn caret_vertical(
210 &self,
211 width: f32,
212 cursor: usize,
213 down: bool,
214 page: bool,
215 goal_x: Option<f32>,
216 ) -> Option<(usize, f32)> {
217 self.inner
218 .as_ref()
219 .and_then(|w| w.caret_vertical(width, cursor, down, page, goal_x))
220 }
221
222 fn selected_text(&self, edit: &Edit) -> Option<String> {
223 self.inner.as_ref().and_then(|w| w.selected_text(edit))
224 }
225
226 fn text_value(&self) -> Option<&str> {
227 self.inner.as_ref().and_then(|w| w.text_value())
228 }
229
230 fn word_at(&self, index: usize) -> Option<(usize, usize)> {
231 self.inner.as_ref().and_then(|w| w.word_at(index))
232 }
233
234 fn selection_handles(
235 &self,
236 width: f32,
237 edit: &Edit,
238 scroll_y: f32,
239 ) -> Option<[crate::SelectionHandle; 2]> {
240 self.inner
241 .as_ref()
242 .and_then(|w| w.selection_handles(width, edit, scroll_y))
243 }
244
245 fn autofill_hints(&self) -> &[crate::AutofillHint] {
246 self.inner
247 .as_ref()
248 .map(|w| w.autofill_hints())
249 .unwrap_or(&[])
250 }
251
252 fn autofill_group(&self) -> bool {
253 self.inner.as_ref().is_some_and(|w| w.autofill_group())
254 }
255
256 fn focusable(&self) -> bool {
257 self.inner.as_ref().is_some_and(|w| w.focusable())
258 }
259
260 fn draws_own_focus(&self) -> bool {
261 self.inner.as_ref().is_some_and(|w| w.draws_own_focus())
262 }
263
264 fn stack(&self) -> bool {
265 self.inner.as_ref().is_some_and(|w| w.stack())
266 }
267
268 fn continuous(&self) -> bool {
269 self.inner.as_ref().is_some_and(|w| w.continuous())
270 }
271
272 fn virtual_list(&self, viewport: frus_core::Size) -> Option<crate::list::VirtualList<'_, Msg>> {
273 self.inner.as_ref().and_then(|w| w.virtual_list(viewport))
274 }
275
276 fn page_view(&self) -> Option<crate::pageview::PagedView<'_, Msg>> {
277 self.inner.as_ref().and_then(|w| w.page_view())
278 }
279
280 fn intrinsic(&self) -> Option<(crate::constraints::IntrinsicAxis, Option<f32>)> {
281 self.inner.as_ref().and_then(|w| w.intrinsic())
282 }
283
284 fn overflow_box(&self) -> Option<crate::constraints::Overflow> {
285 self.inner.as_ref().and_then(|w| w.overflow_box())
286 }
287
288 fn hero_tag(&self) -> Option<u64> {
289 self.inner.as_ref().and_then(|w| w.hero_tag())
290 }
291
292 fn drag_payload(&self) -> Option<u64> {
293 self.inner.as_ref().and_then(|w| w.drag_payload())
294 }
295
296 fn drag_needs_long_press(&self) -> bool {
297 self.inner
298 .as_ref()
299 .is_some_and(|w| w.drag_needs_long_press())
300 }
301
302 fn drag_ghost_opacity(&self) -> f32 {
303 self.inner.as_ref().map_or(1.0, |w| w.drag_ghost_opacity())
304 }
305
306 fn on_dropped(&self, accepted: bool) -> Option<Msg> {
307 self.inner.as_ref().and_then(|w| w.on_dropped(accepted))
308 }
309
310 fn drop_zone(&self) -> bool {
311 self.inner.as_ref().is_some_and(|w| w.drop_zone())
312 }
313
314 fn accepts_drag(&self, payload: u64) -> bool {
315 self.inner.as_ref().is_none_or(|w| w.accepts_drag(payload))
316 }
317
318 fn on_drop(&self, payload: u64) -> Option<Msg> {
319 self.inner.as_ref().and_then(|w| w.on_drop(payload))
320 }
321
322 fn on_page_changed(&self, page: usize) -> Option<Msg> {
323 self.inner.as_ref().and_then(|w| w.on_page_changed(page))
324 }
325
326 fn on_scroll(&self, position: crate::ScrollPosition) -> Option<Msg> {
327 self.inner.as_ref().and_then(|w| w.on_scroll(position))
328 }
329
330 fn scroll_grain(&self) -> f32 {
331 self.inner.as_ref().map_or(0.0, |w| w.scroll_grain())
332 }
333
334 fn layout_builder(&self) -> Option<&dyn Fn(frus_core::Size) -> Box<dyn Widget<Msg>>> {
335 self.inner.as_ref().and_then(|w| w.layout_builder())
336 }
337
338 fn scroll_overlay(&self) -> Option<&crate::scrolloverlay::OverlayBuilder<Msg>> {
339 self.inner.as_ref().and_then(|w| w.scroll_overlay())
340 }
341
342 fn draggable(&self) -> bool {
343 self.inner.as_ref().is_some_and(|w| w.draggable())
344 }
345
346 fn on_drag(&self, fraction: f32) -> Option<Msg> {
347 self.inner.as_ref().and_then(|w| w.on_drag(fraction))
348 }
349
350 fn on_drag_start(&self, fraction: f32) -> Option<Msg> {
351 self.inner.as_ref().and_then(|w| w.on_drag_start(fraction))
352 }
353
354 fn on_drag_end(&self, fraction: f32) -> Option<Msg> {
355 self.inner.as_ref().and_then(|w| w.on_drag_end(fraction))
356 }
357
358 fn on_drag_delta(&self, dx: f32) -> Option<Msg> {
359 self.inner.as_ref().and_then(|w| w.on_drag_delta(dx))
360 }
361
362 fn reorder_index(&self) -> Option<usize> {
363 self.inner.as_ref().and_then(|w| w.reorder_index())
364 }
365
366 fn on_reorder(&self, to: usize) -> Option<Msg> {
367 self.inner.as_ref().and_then(|w| w.on_reorder(to))
368 }
369
370 fn announce(&self) -> Option<String> {
371 self.inner.as_ref().and_then(|w| w.announce())
372 }
373
374 fn overlay_traps_focus(&self) -> bool {
375 self.inner.as_ref().is_some_and(|w| w.overlay_traps_focus())
376 }
377
378 fn scroll_content(&self) -> Option<&dyn Widget<Msg>> {
379 self.inner.as_ref().and_then(|w| w.scroll_content())
380 }
381
382 fn scroll_axis(&self) -> Axis {
383 self.inner
384 .as_ref()
385 .map(|w| w.scroll_axis())
386 .unwrap_or(Axis::Vertical)
387 }
388
389 fn scroll_physics(&self) -> Option<crate::physics::ScrollPhysics> {
390 self.inner.as_ref().and_then(|w| w.scroll_physics())
391 }
392
393 fn keep_visible(
394 &self,
395 size: frus_core::Size,
396 theme: &crate::theme::Theme,
397 ) -> Option<crate::ui::KeepVisible> {
398 self.inner
399 .as_ref()
400 .and_then(|w| w.keep_visible(size, theme))
401 }
402
403 fn ime(&self) -> crate::ime::Ime {
404 self.inner.as_ref().map(|w| w.ime()).unwrap_or_default()
405 }
406
407 fn overlay(&self) -> Option<(&dyn Widget<Msg>, Placement)> {
408 self.inner.as_ref().and_then(|w| w.overlay())
409 }
410
411 fn overlay_dismiss(&self) -> Option<Msg> {
412 self.inner.as_ref().and_then(|w| w.overlay_dismiss())
413 }
414
415 fn overlay_scrim(&self, theme: &Theme) -> Option<frus_core::Color> {
416 self.inner.as_ref().and_then(|w| w.overlay_scrim(theme))
417 }
418
419 fn anim_target(&self) -> Option<f32> {
420 self.inner.as_ref().and_then(|w| w.anim_target())
421 }
422
423 fn anim_duration(&self) -> f32 {
424 self.inner
425 .as_ref()
426 .map(|w| w.anim_duration())
427 .unwrap_or(crate::runtime::ANIM_DURATION)
428 }
429
430 fn anim_curve(&self) -> frus_core::Curve {
431 self.inner
432 .as_ref()
433 .map(|w| w.anim_curve())
434 .unwrap_or(frus_core::Curve::Linear)
435 }
436
437 fn opacity_group(&self) -> Option<f32> {
438 self.inner.as_ref().and_then(|w| w.opacity_group())
439 }
440
441 fn anim_color(&self) -> Option<frus_core::Color> {
442 self.inner.as_ref().and_then(|w| w.anim_color())
443 }
444
445 fn anim_size(&self) -> Option<frus_core::Size> {
446 self.inner.as_ref().and_then(|w| w.anim_size())
447 }
448
449 fn anim_radius(&self) -> Option<frus_core::BorderRadius> {
450 self.inner.as_ref().and_then(|w| w.anim_radius())
451 }
452
453 fn anim_padding(&self) -> Option<frus_core::Insets> {
454 self.inner.as_ref().and_then(|w| w.anim_padding())
455 }
456
457 fn anim_transform(&self) -> Option<crate::runtime::TransformValues> {
458 self.inner.as_ref().and_then(|w| w.anim_transform())
459 }
460
461 fn anim_offset(&self) -> Option<(f32, f32)> {
467 self.inner.as_ref().and_then(|w| w.anim_offset())
468 }
469
470 fn anim_pins(&self) -> Option<crate::positioned::Positioning> {
471 self.inner.as_ref().and_then(|w| w.anim_pins())
472 }
473
474 fn anim_fractions(&self) -> Option<(Option<f32>, Option<f32>)> {
475 self.inner.as_ref().and_then(|w| w.anim_fractions())
476 }
477 fn anim_text_style(&self) -> Option<frus_core::TextStyle> {
478 self.inner.as_ref().and_then(|w| w.anim_text_style())
479 }
480
481 fn positioned(&self) -> Option<crate::positioned::Positioning> {
482 self.inner.as_ref().and_then(|w| w.positioned())
483 }
484
485 fn alignment_geometry(&self) -> Option<frus_core::AlignmentGeometry> {
486 self.inner.as_ref().and_then(|w| w.alignment_geometry())
487 }
488
489 fn transform_translate(&self) -> Option<(f32, f32)> {
490 self.inner.as_ref().and_then(|w| w.transform_translate())
491 }
492
493 fn transform_scale(&self) -> Option<(f32, f32, frus_core::Alignment)> {
494 self.inner.as_ref().and_then(|w| w.transform_scale())
495 }
496
497 fn transform_rotate(&self) -> Option<(f32, frus_core::Alignment)> {
498 self.inner.as_ref().and_then(|w| w.transform_rotate())
499 }
500
501 fn clip_shape(&self) -> Option<frus_core::ClipShape> {
502 self.inner.as_ref().and_then(|w| w.clip_shape())
503 }
504
505 fn clip_path(&self) -> Option<&frus_core::Path> {
506 self.inner.as_ref().and_then(|w| w.clip_path())
507 }
508
509 fn barrier(&self) -> Option<crate::barrier::ModalBarrier> {
510 self.inner.as_ref().and_then(|w| w.barrier())
511 }
512
513 fn dismissible(&self) -> Option<crate::dismiss::DismissSpec> {
514 self.inner.as_ref().and_then(|w| w.dismissible())
515 }
516
517 fn on_dismissed(&self, direction: crate::dismiss::DismissDirection) -> Option<Msg> {
518 self.inner.as_ref().and_then(|w| w.on_dismissed(direction))
519 }
520
521 fn sheet(&self) -> Option<&crate::sheet::SheetSpec> {
522 self.inner.as_ref().and_then(|w| w.sheet())
523 }
524
525 fn on_sheet_dismissed(&self) -> Option<Msg> {
526 self.inner.as_ref().and_then(|w| w.on_sheet_dismissed())
527 }
528
529 fn refresh(&self) -> Option<crate::refresh::RefreshSpec> {
530 self.inner.as_ref().and_then(|w| w.refresh())
531 }
532
533 fn on_refresh(&self) -> Option<Msg> {
534 self.inner.as_ref().and_then(|w| w.on_refresh())
535 }
536
537 fn interactive(&self) -> Option<(f32, f32)> {
538 self.inner.as_ref().and_then(|w| w.interactive())
539 }
540
541 fn fitted(&self) -> Option<frus_core::BoxFit> {
542 self.inner.as_ref().and_then(|w| w.fitted())
543 }
544
545 fn rotated_quarter_turns(&self) -> Option<i32> {
546 self.inner.as_ref().and_then(|w| w.rotated_quarter_turns())
547 }
548
549 fn navigator(&self) -> Option<(f32, bool)> {
550 self.inner.as_ref().and_then(|w| w.navigator())
551 }
552
553 fn navigator_retained(&self) -> usize {
554 self.inner.as_ref().map_or(0, |w| w.navigator_retained())
555 }
556
557 fn measure(&self, theme: &Theme) -> Option<frus_layout::MeasureFn<'_>> {
558 self.inner.as_ref().and_then(|w| w.measure(theme))
559 }
560
561 fn measure_key(&self, theme: &Theme) -> Option<u64> {
562 self.inner.as_ref().and_then(|w| w.measure_key(theme))
563 }
564
565 fn on_long_press(&self) -> Option<Msg> {
566 self.inner.as_ref().and_then(|w| w.on_long_press())
567 }
568
569 fn on_key(&self, key: &crate::Key) -> crate::KeyResponse<Msg> {
570 self.inner
571 .as_ref()
572 .map(|w| w.on_key(key))
573 .unwrap_or(crate::KeyResponse::Ignored)
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580 use crate::Container;
581 use frus_layout::Dimension;
582
583 fn chosen_width(r: &Responsive<()>) -> Dimension {
586 Widget::<()>::style(r).width
587 }
588
589 fn tagged(w: f32) -> Responsive<()> {
590 responsive(w)
591 .compact(Container::new().width(100.0))
592 .medium(Container::new().width(200.0))
593 .expanded(Container::new().width(300.0))
594 }
595
596 #[test]
597 fn picks_variant_for_width() {
598 assert_eq!(chosen_width(&tagged(400.0)), Dimension::Length(100.0));
599 assert_eq!(chosen_width(&tagged(700.0)), Dimension::Length(200.0));
600 assert_eq!(chosen_width(&tagged(1200.0)), Dimension::Length(300.0));
601 }
602
603 #[test]
604 fn falls_back_to_nearest_when_missing() {
605 let only_expanded = |w: f32| responsive::<()>(w).expanded(Container::new().width(300.0));
607 assert_eq!(
608 chosen_width(&only_expanded(300.0)),
609 Dimension::Length(300.0)
610 );
611
612 let two = responsive::<()>(700.0)
615 .compact(Container::new().width(100.0))
616 .expanded(Container::new().width(300.0));
617 assert_eq!(chosen_width(&two), Dimension::Length(100.0));
618 }
619
620 #[test]
629 fn a_selector_keeps_the_layers_place_in_the_stack() {
630 let pinned = responsive::<()>(400.0).compact(
631 crate::Positioned::new(Container::new().width(20.0).height(20.0))
632 .top(8.0)
633 .right(12.0),
634 );
635 let spec = Widget::<()>::positioned(&pinned).expect("the pins reached through");
636 assert_eq!((spec.top, spec.right), (Some(8.0), Some(12.0)));
637 }
638
639 #[test]
643 fn a_selector_keeps_the_childs_animated_offset() {
644 let sliding = responsive::<()>(400.0).compact(crate::AnimatedAlign::new(
645 frus_core::Alignment::CENTER_RIGHT,
646 0.2,
647 frus_core::Curve::Linear,
648 Container::new().width(20.0).height(20.0),
649 ));
650 assert_eq!(
651 Widget::<()>::anim_offset(&sliding),
652 Some((1.0, 0.0)),
655 "the anchor's two fractions reached through"
656 );
657 }
658
659 #[test]
664 fn a_selector_builds_a_deferred_variant() {
665 let chosen = responsive::<()>(400.0).compact(crate::ThemeBuilder::new(|_: &Theme| {
666 Container::new()
667 .width(30.0)
668 .height(10.0)
669 .color(frus_core::Color::rgb(1.0, 0.0, 0.0))
670 }));
671 let ui = crate::ui::build_ui(
672 &chosen,
673 frus_core::Size::new(100.0, 100.0),
674 &crate::Runtime::default(),
675 &Theme::default(),
676 );
677 assert!(
678 ui.scene()
679 .primitives()
680 .iter()
681 .any(|p| matches!(p, frus_core::Primitive::Rect { color, .. } if color.r > 0.5)),
682 "the variant was built, and painted"
683 );
684 }
685}