Skip to main content

frus_widgets/
responsive.rs

1//! [`Responsive`]: picks a subtree from the current [`SizeClass`].
2//!
3//! `responsive(width).compact(a).medium(b).expanded(c)` selects the variant matching
4//! the width breakpoint, with a **graceful fallback**: when the exact breakpoint is not
5//! supplied, the nearest is used, preferring the smaller one. The resulting widget
6//! **delegates everything** to the chosen variant, as [`Keyed`] does.
7//!
8//! [`Keyed`]: crate::Keyed
9
10use 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
20/// Selects a subtree from the size class, delegating to whichever is chosen.
21pub struct Responsive<Msg = crate::callback::Callback> {
22    class_rank: u8,
23    inner: Option<Box<dyn Widget<Msg>>>,
24    inner_rank: u8,
25}
26
27/// A breakpoint's distance from the target class: minimising `(gap, is_above)` picks
28/// the nearest, preferring a smaller breakpoint when the gaps are equal.
29fn closeness(rank: u8, class: u8) -> (u8, bool) {
30    (rank.abs_diff(class), rank > class)
31}
32
33impl<Msg> Responsive<Msg> {
34    /// Builds a selector for the class `class`.
35    pub fn new(class: SizeClass) -> Self {
36        Self {
37            class_rank: class.rank(),
38            inner: None,
39            inner_rank: 0,
40        }
41    }
42
43    /// Considers `widget` for the breakpoint of rank `rank`, keeping the best
44    /// candidate seen so far, independently of the order of the calls.
45    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    /// Variant for the `Compact` breakpoint (< 600 px).
56    pub fn compact(self, widget: impl Widget<Msg> + 'static) -> Self {
57        self.consider(SizeClass::Compact.rank(), widget)
58    }
59
60    /// Variant for the `Medium` breakpoint (600–840 px).
61    pub fn medium(self, widget: impl Widget<Msg> + 'static) -> Self {
62        self.consider(SizeClass::Medium.rank(), widget)
63    }
64
65    /// Variant for the `Expanded` breakpoint (≥ 840 px).
66    pub fn expanded(self, widget: impl Widget<Msg> + 'static) -> Self {
67        self.consider(SizeClass::Expanded.rank(), widget)
68    }
69}
70
71/// A responsive selector for a given width, in logical px.
72///
73/// `responsive(w).compact(a).medium(b).expanded(c)` — see [`Responsive`].
74pub 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    // Both were missing, the first since `ThemeBuilder` existed: the walk asks this node to
91    // compose itself, the default did nothing, and `children` then read straight through to
92    // a deferred subtree nobody had built. An application bar is one, so
93    // `responsive(w).compact(bar)` was enough; the test below fails without these. The
94    // fourth hook this hand-written forwarder has missed.
95    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        // A transparent wrapper: the inspector shows the realised widget.
133        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 paints_over(&self) -> bool {
154        self.inner.as_ref().is_some_and(|w| w.paints_over())
155    }
156
157    fn paint_over(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
158        if let Some(w) = &self.inner {
159            w.paint_over(bounds, status, theme, scene);
160        }
161    }
162
163    fn on_click(&self) -> Option<Msg> {
164        self.inner.as_ref().and_then(|w| w.on_click())
165    }
166
167    fn on_focus_change(&self, focused: bool) -> Option<Msg> {
168        self.inner.as_ref().and_then(|w| w.on_focus_change(focused))
169    }
170
171    fn on_tap_outside(&self) -> Option<Msg> {
172        self.inner.as_ref().and_then(|w| w.on_tap_outside())
173    }
174
175    fn opaque(&self) -> bool {
176        self.inner.as_ref().is_some_and(|w| w.opaque())
177    }
178
179    fn positional_click(&self, local_x: f32, local_y: f32, width: f32, height: f32) -> Option<Msg> {
180        self.inner
181            .as_ref()
182            .and_then(|w| w.positional_click(local_x, local_y, width, height))
183    }
184
185    fn cursor_icon(
186        &self,
187        local_x: f32,
188        local_y: f32,
189        width: f32,
190        height: f32,
191    ) -> Option<crate::interaction::Cursor> {
192        self.inner
193            .as_ref()
194            .and_then(|w| w.cursor_icon(local_x, local_y, width, height))
195    }
196
197    fn key(&self) -> Option<u64> {
198        self.inner.as_ref().and_then(|w| w.key())
199    }
200
201    fn on_edit(&self, edit: &mut Edit, key: &Key) -> Option<Msg> {
202        self.inner.as_ref().and_then(|w| w.on_edit(edit, key))
203    }
204
205    fn cursor_at(
206        &self,
207        local_x: f32,
208        local_y: f32,
209        width: f32,
210        scroll_cursor: usize,
211    ) -> Option<usize> {
212        self.inner
213            .as_ref()
214            .and_then(|w| w.cursor_at(local_x, local_y, width, scroll_cursor))
215    }
216
217    fn text_metrics(&self, width: f32, cursor: usize) -> Option<(f32, f32, f32, f32)> {
218        self.inner
219            .as_ref()
220            .and_then(|w| w.text_metrics(width, cursor))
221    }
222
223    fn text_viewport(&self, rect: frus_core::Rect) -> Option<frus_core::Rect> {
224        self.inner.as_ref().and_then(|w| w.text_viewport(rect))
225    }
226
227    fn caret_vertical(
228        &self,
229        width: f32,
230        cursor: usize,
231        down: bool,
232        page: bool,
233        goal_x: Option<f32>,
234    ) -> Option<(usize, f32)> {
235        self.inner
236            .as_ref()
237            .and_then(|w| w.caret_vertical(width, cursor, down, page, goal_x))
238    }
239
240    fn selected_text(&self, edit: &Edit) -> Option<String> {
241        self.inner.as_ref().and_then(|w| w.selected_text(edit))
242    }
243
244    fn text_value(&self) -> Option<&str> {
245        self.inner.as_ref().and_then(|w| w.text_value())
246    }
247
248    fn takes_typing(&self) -> bool {
249        self.inner.as_ref().is_some_and(|w| w.takes_typing())
250    }
251
252    fn selection_text(&self) -> Option<&str> {
253        self.inner.as_ref().and_then(|w| w.selection_text())
254    }
255
256    fn region_toolbar(
257        &self,
258        context: crate::ToolbarContext,
259        width: f32,
260        range: (usize, usize),
261    ) -> Option<(Rect, &dyn Widget<Msg>)> {
262        self.inner
263            .as_ref()
264            .and_then(|w| w.region_toolbar(context, width, range))
265    }
266
267    fn selection_grip(
268        &self,
269        width: f32,
270        index: usize,
271        start: bool,
272    ) -> Option<crate::SelectionHandle> {
273        self.inner
274            .as_ref()
275            .and_then(|w| w.selection_grip(width, index, start))
276    }
277
278    fn selection_hit(&self, local_x: f32, local_y: f32, width: f32) -> Option<usize> {
279        self.inner
280            .as_ref()
281            .and_then(|w| w.selection_hit(local_x, local_y, width))
282    }
283
284    fn word_at(&self, index: usize) -> Option<(usize, usize)> {
285        self.inner.as_ref().and_then(|w| w.word_at(index))
286    }
287
288    fn selection_handles(
289        &self,
290        width: f32,
291        edit: &Edit,
292        scroll_y: f32,
293    ) -> Option<[crate::SelectionHandle; 2]> {
294        self.inner
295            .as_ref()
296            .and_then(|w| w.selection_handles(width, edit, scroll_y))
297    }
298
299    fn selection_toolbar(&self, context: crate::ToolbarContext) -> Option<&dyn Widget<Msg>> {
300        self.inner
301            .as_ref()
302            .and_then(|w| w.selection_toolbar(context))
303    }
304
305    fn selection_anchor(&self, width: f32, edit: &Edit, scroll_y: f32) -> Option<Rect> {
306        self.inner
307            .as_ref()
308            .and_then(|w| w.selection_anchor(width, edit, scroll_y))
309    }
310
311    fn edit_action(&self) -> Option<crate::EditAction> {
312        self.inner.as_ref().and_then(|w| w.edit_action())
313    }
314
315    fn autofill_hints(&self) -> &[crate::AutofillHint] {
316        self.inner
317            .as_ref()
318            .map(|w| w.autofill_hints())
319            .unwrap_or(&[])
320    }
321
322    fn autofill_group(&self) -> bool {
323        self.inner.as_ref().is_some_and(|w| w.autofill_group())
324    }
325
326    fn area_toolbar(&self, context: crate::ToolbarContext) -> Option<Option<&dyn Widget<Msg>>> {
327        self.inner.as_ref().and_then(|w| w.area_toolbar(context))
328    }
329
330    fn focusable(&self) -> bool {
331        self.inner.as_ref().is_some_and(|w| w.focusable())
332    }
333
334    fn draws_own_focus(&self) -> bool {
335        self.inner.as_ref().is_some_and(|w| w.draws_own_focus())
336    }
337
338    fn stack(&self) -> bool {
339        self.inner.as_ref().is_some_and(|w| w.stack())
340    }
341
342    fn stack_fit(&self) -> crate::StackFit {
343        self.inner
344            .as_ref()
345            .map_or(crate::StackFit::Passthrough, |w| w.stack_fit())
346    }
347
348    fn stack_measured(&self) -> bool {
349        self.inner.as_ref().is_some_and(|w| w.stack_measured())
350    }
351
352    fn stack_clips(&self) -> bool {
353        self.inner.as_ref().is_none_or(|w| w.stack_clips())
354    }
355
356    fn stack_direction(&self) -> Option<frus_core::TextDirection> {
357        self.inner.as_ref().and_then(|w| w.stack_direction())
358    }
359
360    fn stack_visible(&self) -> Option<usize> {
361        self.inner.as_ref().and_then(|w| w.stack_visible())
362    }
363
364    fn continuous(&self) -> bool {
365        self.inner.as_ref().is_some_and(|w| w.continuous())
366    }
367
368    fn virtual_list(&self, viewport: frus_core::Size) -> Option<crate::list::VirtualList<'_, Msg>> {
369        self.inner.as_ref().and_then(|w| w.virtual_list(viewport))
370    }
371
372    fn page_view(&self) -> Option<crate::pageview::PagedView<'_, Msg>> {
373        self.inner.as_ref().and_then(|w| w.page_view())
374    }
375
376    fn intrinsic(&self) -> Option<(crate::constraints::IntrinsicAxis, Option<f32>)> {
377        self.inner.as_ref().and_then(|w| w.intrinsic())
378    }
379
380    fn overflow_box(&self) -> Option<crate::constraints::Overflow> {
381        self.inner.as_ref().and_then(|w| w.overflow_box())
382    }
383
384    fn hero_tag(&self) -> Option<u64> {
385        self.inner.as_ref().and_then(|w| w.hero_tag())
386    }
387
388    fn drag_payload(&self) -> Option<u64> {
389        self.inner.as_ref().and_then(|w| w.drag_payload())
390    }
391
392    fn drag_needs_long_press(&self) -> bool {
393        self.inner
394            .as_ref()
395            .is_some_and(|w| w.drag_needs_long_press())
396    }
397
398    fn drag_ghost_opacity(&self) -> f32 {
399        self.inner.as_ref().map_or(1.0, |w| w.drag_ghost_opacity())
400    }
401
402    fn on_dropped(&self, accepted: bool) -> Option<Msg> {
403        self.inner.as_ref().and_then(|w| w.on_dropped(accepted))
404    }
405
406    fn drop_zone(&self) -> bool {
407        self.inner.as_ref().is_some_and(|w| w.drop_zone())
408    }
409
410    fn accepts_drag(&self, payload: u64) -> bool {
411        self.inner.as_ref().is_none_or(|w| w.accepts_drag(payload))
412    }
413
414    fn on_drop(&self, payload: u64) -> Option<Msg> {
415        self.inner.as_ref().and_then(|w| w.on_drop(payload))
416    }
417
418    fn accepts_files(&self, paths: &[std::path::PathBuf]) -> bool {
419        self.inner.as_ref().is_some_and(|w| w.accepts_files(paths))
420    }
421
422    fn on_files_dropped(&self, paths: Vec<std::path::PathBuf>) -> Option<Msg> {
423        self.inner.as_ref().and_then(|w| w.on_files_dropped(paths))
424    }
425
426    fn on_page_changed(&self, page: usize) -> Option<Msg> {
427        self.inner.as_ref().and_then(|w| w.on_page_changed(page))
428    }
429
430    fn on_scroll(&self, position: crate::ScrollPosition) -> Option<Msg> {
431        self.inner.as_ref().and_then(|w| w.on_scroll(position))
432    }
433
434    fn scroll_grain(&self) -> f32 {
435        self.inner.as_ref().map_or(0.0, |w| w.scroll_grain())
436    }
437
438    fn layout_builder(&self) -> Option<&dyn Fn(frus_core::Size) -> Box<dyn Widget<Msg>>> {
439        self.inner.as_ref().and_then(|w| w.layout_builder())
440    }
441
442    fn scroll_overlay(&self) -> Option<&crate::scrolloverlay::OverlayBuilder<Msg>> {
443        self.inner.as_ref().and_then(|w| w.scroll_overlay())
444    }
445
446    fn draggable(&self) -> bool {
447        self.inner.as_ref().is_some_and(|w| w.draggable())
448    }
449    fn drag_track(&self) -> crate::widget::DragTrack {
450        self.inner
451            .as_ref()
452            .map_or(crate::widget::DragTrack::WHOLE, |w| w.drag_track())
453    }
454    fn on_thumb_drag(
455        &self,
456        thumb: usize,
457        moment: crate::widget::ThumbDrag,
458        fraction: f32,
459    ) -> Option<Msg> {
460        self.inner
461            .as_ref()
462            .and_then(|w| w.on_thumb_drag(thumb, moment, fraction))
463    }
464
465    fn on_drag(&self, fraction: f32) -> Option<Msg> {
466        self.inner.as_ref().and_then(|w| w.on_drag(fraction))
467    }
468
469    fn on_drag_start(&self, fraction: f32) -> Option<Msg> {
470        self.inner.as_ref().and_then(|w| w.on_drag_start(fraction))
471    }
472
473    fn on_drag_end(&self, fraction: f32) -> Option<Msg> {
474        self.inner.as_ref().and_then(|w| w.on_drag_end(fraction))
475    }
476
477    fn on_drag_delta(&self, dx: f32) -> Option<Msg> {
478        self.inner.as_ref().and_then(|w| w.on_drag_delta(dx))
479    }
480
481    fn reorder_index(&self) -> Option<usize> {
482        self.inner.as_ref().and_then(|w| w.reorder_index())
483    }
484
485    fn on_reorder(&self, to: usize) -> Option<Msg> {
486        self.inner.as_ref().and_then(|w| w.on_reorder(to))
487    }
488
489    fn announce(&self) -> Option<String> {
490        self.inner.as_ref().and_then(|w| w.announce())
491    }
492
493    fn overlay_traps_focus(&self) -> bool {
494        self.inner.as_ref().is_some_and(|w| w.overlay_traps_focus())
495    }
496
497    fn scroll_content(&self) -> Option<&dyn Widget<Msg>> {
498        self.inner.as_ref().and_then(|w| w.scroll_content())
499    }
500
501    fn scroll_axis(&self) -> Axis {
502        self.inner
503            .as_ref()
504            .map(|w| w.scroll_axis())
505            .unwrap_or(Axis::Vertical)
506    }
507
508    fn scroll_physics(&self) -> Option<crate::physics::ScrollPhysics> {
509        self.inner.as_ref().and_then(|w| w.scroll_physics())
510    }
511
512    fn keep_visible(
513        &self,
514        size: frus_core::Size,
515        theme: &crate::theme::Theme,
516    ) -> Option<crate::ui::KeepVisible> {
517        self.inner
518            .as_ref()
519            .and_then(|w| w.keep_visible(size, theme))
520    }
521
522    fn ime(&self) -> crate::ime::Ime {
523        self.inner.as_ref().map(|w| w.ime()).unwrap_or_default()
524    }
525
526    fn overlay(&self) -> Option<(&dyn Widget<Msg>, Placement)> {
527        self.inner.as_ref().and_then(|w| w.overlay())
528    }
529
530    fn overlay_dismiss(&self) -> Option<Msg> {
531        self.inner.as_ref().and_then(|w| w.overlay_dismiss())
532    }
533
534    fn overlay_scrim(&self, theme: &Theme) -> Option<frus_core::Color> {
535        self.inner.as_ref().and_then(|w| w.overlay_scrim(theme))
536    }
537
538    fn anim_target(&self) -> Option<f32> {
539        self.inner.as_ref().and_then(|w| w.anim_target())
540    }
541
542    fn anim_initial(&self) -> Option<f32> {
543        self.inner.as_ref().and_then(|w| w.anim_initial())
544    }
545
546    fn anim_duration(&self) -> f32 {
547        self.inner
548            .as_ref()
549            .map(|w| w.anim_duration())
550            .unwrap_or(crate::runtime::ANIM_DURATION)
551    }
552
553    fn anim_curve(&self) -> frus_core::Curve {
554        self.inner
555            .as_ref()
556            .map(|w| w.anim_curve())
557            .unwrap_or(frus_core::Curve::Linear)
558    }
559
560    fn opacity_group(&self) -> Option<f32> {
561        self.inner.as_ref().and_then(|w| w.opacity_group())
562    }
563
564    fn anim_color(&self) -> Option<frus_core::Color> {
565        self.inner.as_ref().and_then(|w| w.anim_color())
566    }
567
568    fn anim_size(&self) -> Option<frus_core::Size> {
569        self.inner.as_ref().and_then(|w| w.anim_size())
570    }
571
572    fn anim_radius(&self) -> Option<frus_core::BorderRadius> {
573        self.inner.as_ref().and_then(|w| w.anim_radius())
574    }
575
576    fn anim_padding(&self) -> Option<frus_core::Insets> {
577        self.inner.as_ref().and_then(|w| w.anim_padding())
578    }
579
580    fn anim_transform(&self) -> Option<crate::runtime::TransformValues> {
581        self.inner.as_ref().and_then(|w| w.anim_transform())
582    }
583
584    // The three below were **missing**, and milestone 477 predicted exactly this: a hook
585    // this forwarder does not list is a value the runtime never finds, on a widget that
586    // passes every test it has because nobody put it inside a `Responsive`. An
587    // `AnimatedAlign` behind one jumped instead of sliding, and a `Positioned` behind one
588    // lost its place in the stack entirely.
589    fn anim_offset(&self) -> Option<(f32, f32)> {
590        self.inner.as_ref().and_then(|w| w.anim_offset())
591    }
592
593    fn anim_pins(&self) -> Option<crate::positioned::Positioning> {
594        self.inner.as_ref().and_then(|w| w.anim_pins())
595    }
596
597    fn anim_fractions(&self) -> Option<(Option<f32>, Option<f32>)> {
598        self.inner.as_ref().and_then(|w| w.anim_fractions())
599    }
600    fn anim_text_style(&self) -> Option<frus_core::TextStyle> {
601        self.inner.as_ref().and_then(|w| w.anim_text_style())
602    }
603
604    fn positioned(&self) -> Option<crate::positioned::Positioning> {
605        self.inner.as_ref().and_then(|w| w.positioned())
606    }
607
608    fn alignment_geometry(&self) -> Option<frus_core::AlignmentGeometry> {
609        self.inner.as_ref().and_then(|w| w.alignment_geometry())
610    }
611
612    fn transform_translate(&self) -> Option<(f32, f32)> {
613        self.inner.as_ref().and_then(|w| w.transform_translate())
614    }
615
616    fn transform_scale(&self) -> Option<(f32, f32, frus_core::Alignment)> {
617        self.inner.as_ref().and_then(|w| w.transform_scale())
618    }
619
620    fn transform_rotate(&self) -> Option<(f32, frus_core::Alignment)> {
621        self.inner.as_ref().and_then(|w| w.transform_rotate())
622    }
623
624    fn clip_shape(&self) -> Option<frus_core::ClipShape> {
625        self.inner.as_ref().and_then(|w| w.clip_shape())
626    }
627
628    fn clip_path(&self) -> Option<&frus_core::Path> {
629        self.inner.as_ref().and_then(|w| w.clip_path())
630    }
631
632    fn barrier(&self) -> Option<crate::barrier::ModalBarrier> {
633        self.inner.as_ref().and_then(|w| w.barrier())
634    }
635
636    fn dismissible(&self) -> Option<crate::dismiss::DismissSpec> {
637        self.inner.as_ref().and_then(|w| w.dismissible())
638    }
639
640    fn on_dismissed(&self, direction: crate::dismiss::DismissDirection) -> Option<Msg> {
641        self.inner.as_ref().and_then(|w| w.on_dismissed(direction))
642    }
643
644    fn sheet(&self) -> Option<&crate::sheet::SheetSpec> {
645        self.inner.as_ref().and_then(|w| w.sheet())
646    }
647
648    fn on_sheet_dismissed(&self) -> Option<Msg> {
649        self.inner.as_ref().and_then(|w| w.on_sheet_dismissed())
650    }
651
652    fn refresh(&self) -> Option<crate::refresh::RefreshSpec> {
653        self.inner.as_ref().and_then(|w| w.refresh())
654    }
655
656    fn on_refresh(&self) -> Option<Msg> {
657        self.inner.as_ref().and_then(|w| w.on_refresh())
658    }
659
660    fn interactive(&self) -> Option<(f32, f32)> {
661        self.inner.as_ref().and_then(|w| w.interactive())
662    }
663
664    fn fitted(&self) -> Option<frus_core::BoxFit> {
665        self.inner.as_ref().and_then(|w| w.fitted())
666    }
667
668    fn rotated_quarter_turns(&self) -> Option<i32> {
669        self.inner.as_ref().and_then(|w| w.rotated_quarter_turns())
670    }
671
672    fn navigator(&self) -> Option<(f32, bool)> {
673        self.inner.as_ref().and_then(|w| w.navigator())
674    }
675
676    fn navigator_gesture(&self) -> bool {
677        self.inner.as_ref().is_some_and(|w| w.navigator_gesture())
678    }
679
680    fn navigator_retained(&self) -> usize {
681        self.inner.as_ref().map_or(0, |w| w.navigator_retained())
682    }
683
684    fn measure(&self, theme: &Theme) -> Option<frus_layout::MeasureFn<'_>> {
685        self.inner.as_ref().and_then(|w| w.measure(theme))
686    }
687
688    fn measure_key(&self, theme: &Theme) -> Option<u64> {
689        self.inner.as_ref().and_then(|w| w.measure_key(theme))
690    }
691
692    fn on_long_press(&self) -> Option<Msg> {
693        self.inner.as_ref().and_then(|w| w.on_long_press())
694    }
695
696    fn on_double_tap(&self) -> Option<Msg> {
697        self.inner.as_ref().and_then(|w| w.on_double_tap())
698    }
699
700    fn on_secondary_tap(&self) -> Option<Msg> {
701        self.inner.as_ref().and_then(|w| w.on_secondary_tap())
702    }
703
704    fn pan_axis(&self) -> Option<crate::PanAxis> {
705        self.inner.as_ref().and_then(|w| w.pan_axis())
706    }
707
708    fn hover_region(&self) -> Option<crate::HoverRegion> {
709        self.inner.as_ref().and_then(|w| w.hover_region())
710    }
711
712    fn pointer_listener(&self) -> bool {
713        self.inner.as_ref().is_some_and(|w| w.pointer_listener())
714    }
715
716    fn on_pointer_event(&self, event: crate::ListenerEvent) -> Vec<Msg> {
717        self.inner
718            .as_ref()
719            .map(|w| w.on_pointer_event(event))
720            .unwrap_or_default()
721    }
722
723    fn on_hover_event(&self, event: crate::HoverEvent) -> Option<Msg> {
724        self.inner.as_ref().and_then(|w| w.on_hover_event(event))
725    }
726
727    fn on_pan(&self, event: crate::PanEvent) -> Option<Msg> {
728        self.inner.as_ref().and_then(|w| w.on_pan(event))
729    }
730
731    fn pan_value(&self, event: crate::PanEvent, value: f32, rtl: bool) -> Option<f32> {
732        self.inner
733            .as_ref()
734            .and_then(|w| w.pan_value(event, value, rtl))
735    }
736
737    fn on_value_release(&self, value: f32) -> Option<Msg> {
738        self.inner.as_ref().and_then(|w| w.on_value_release(value))
739    }
740
741    fn on_key(&self, key: &crate::Key) -> crate::KeyResponse<Msg> {
742        self.inner
743            .as_ref()
744            .map(|w| w.on_key(key))
745            .unwrap_or(crate::KeyResponse::Ignored)
746    }
747}
748
749#[cfg(test)]
750mod tests {
751    use super::*;
752    use crate::Container;
753    use frus_layout::Dimension;
754
755    /// Style width of the chosen variant (each breakpoint gets a distinct
756    /// width so it can be identified).
757    fn chosen_width(r: &Responsive<()>) -> Dimension {
758        Widget::<()>::style(r).width
759    }
760
761    fn tagged(w: f32) -> Responsive<()> {
762        responsive(w)
763            .compact(Container::new().width(100.0))
764            .medium(Container::new().width(200.0))
765            .expanded(Container::new().width(300.0))
766    }
767
768    /// **A wrapper paints over its child's children as the child would** (milestone 652):
769    /// a range slider chosen by a selector, or given a key, still paints its thumbs over
770    /// their stops.
771    #[test]
772    fn wrappers_forward_painting_over_the_children() {
773        let slider = || crate::RangeSlider::<()>::new(0.2, 0.8);
774        let chosen = responsive::<()>(400.0).compact(slider());
775        assert!(Widget::<()>::paints_over(&chosen));
776        assert!(!Widget::<()>::paints_over(
777            &responsive::<()>(400.0).compact(Container::new())
778        ));
779        let keyed = crate::Keyed::new(7, slider());
780        assert!(Widget::<()>::paints_over(&keyed));
781        let mut over = frus_core::Scene::new();
782        Widget::<()>::paint_over(
783            &keyed,
784            frus_core::Rect::new(0.0, 0.0, 248.0, 48.0),
785            Status::default(),
786            &crate::Theme::default(),
787            &mut over,
788        );
789        let mut chose = frus_core::Scene::new();
790        Widget::<()>::paint_over(
791            &chosen,
792            frus_core::Rect::new(0.0, 0.0, 248.0, 48.0),
793            Status::default(),
794            &crate::Theme::default(),
795            &mut chose,
796        );
797        assert!(!over.primitives().is_empty(), "the thumbs, through the key");
798        assert_eq!(over.primitives().len(), chose.primitives().len());
799    }
800
801    #[test]
802    fn picks_variant_for_width() {
803        assert_eq!(chosen_width(&tagged(400.0)), Dimension::Length(100.0));
804        assert_eq!(chosen_width(&tagged(700.0)), Dimension::Length(200.0));
805        assert_eq!(chosen_width(&tagged(1200.0)), Dimension::Length(300.0));
806    }
807
808    #[test]
809    fn falls_back_to_nearest_when_missing() {
810        // Only `expanded` is supplied, so it is used at every width.
811        let only_expanded = |w: f32| responsive::<()>(w).expanded(Container::new().width(300.0));
812        assert_eq!(
813            chosen_width(&only_expanded(300.0)),
814            Dimension::Length(300.0)
815        );
816
817        // compact plus expanded at a medium width (rank 1): compact (rank 0, gap 1,
818        // below) wins over expanded (rank 2, gap 1, above).
819        let two = responsive::<()>(700.0)
820            .compact(Container::new().width(100.0))
821            .expanded(Container::new().width(300.0));
822        assert_eq!(chosen_width(&two), Dimension::Length(100.0));
823    }
824
825    /// **A selector must not swallow a place.** A `Responsive` inside a stack answers for
826    /// the variant it chose, and a layer's pins are the first thing the stack asks about:
827    /// a selector that did not forward them turned every pinned layer behind one into an
828    /// ordinary layer filling the stack, silently.
829    ///
830    /// This was missing until milestone 495, and had never been noticed because nothing
831    /// in the framework or the demo had put a `Positioned` behind a `Responsive`. The
832    /// same shape as the transparent wrapper's own rule, and the same failure.
833    #[test]
834    fn a_selector_keeps_the_layers_place_in_the_stack() {
835        let pinned = responsive::<()>(400.0).compact(
836            crate::Positioned::new(Container::new().width(20.0).height(20.0))
837                .top(8.0)
838                .right(12.0),
839        );
840        let spec = Widget::<()>::positioned(&pinned).expect("the pins reached through");
841        assert_eq!((spec.top, spec.right), (Some(8.0), Some(12.0)));
842    }
843
844    /// **And it must not swallow a movement.** The same hole, one hook along: an
845    /// `AnimatedAlign` behind a selector declared its target to nobody, so the runtime
846    /// found nothing to drive and the child jumped between anchors instead of sliding.
847    #[test]
848    fn a_selector_keeps_the_childs_animated_offset() {
849        let sliding = responsive::<()>(400.0).compact(crate::AnimatedAlign::new(
850            frus_core::Alignment::CENTER_RIGHT,
851            0.2,
852            frus_core::Curve::Linear,
853            Container::new().width(20.0).height(20.0),
854        ));
855        assert_eq!(
856            Widget::<()>::anim_offset(&sliding),
857            // The anchor in its own coordinates, where the middle is nought and an edge
858            // is one — not a share of the box.
859            Some((1.0, 0.0)),
860            "the anchor's two fractions reached through"
861        );
862    }
863
864    /// **And it must not skip the build.** The walk asks each node to compose its subtree
865    /// before it reads the children, and a selector reads its children straight through
866    /// from the variant it chose: one that did not pass the question on handed the walk a
867    /// deferred subtree nobody had built.
868    #[test]
869    fn a_selector_builds_a_deferred_variant() {
870        let chosen = responsive::<()>(400.0).compact(crate::ThemeBuilder::new(|_: &Theme| {
871            Container::new()
872                .width(30.0)
873                .height(10.0)
874                .color(frus_core::Color::rgb(1.0, 0.0, 0.0))
875        }));
876        let ui = crate::ui::build_ui(
877            &chosen,
878            frus_core::Size::new(100.0, 100.0),
879            &crate::Runtime::default(),
880            &Theme::default(),
881        );
882        assert!(
883            ui.scene()
884                .primitives()
885                .iter()
886                .any(|p| matches!(p, frus_core::Primitive::Rect { color, .. } if color.r > 0.5)),
887            "the variant was built, and painted"
888        );
889    }
890}