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 takes_typing(&self) -> bool {
231 self.inner.as_ref().is_some_and(|w| w.takes_typing())
232 }
233
234 fn selection_text(&self) -> Option<&str> {
235 self.inner.as_ref().and_then(|w| w.selection_text())
236 }
237
238 fn region_toolbar(
239 &self,
240 context: crate::ToolbarContext,
241 width: f32,
242 range: (usize, usize),
243 ) -> Option<(Rect, &dyn Widget<Msg>)> {
244 self.inner
245 .as_ref()
246 .and_then(|w| w.region_toolbar(context, width, range))
247 }
248
249 fn selection_grip(
250 &self,
251 width: f32,
252 index: usize,
253 start: bool,
254 ) -> Option<crate::SelectionHandle> {
255 self.inner
256 .as_ref()
257 .and_then(|w| w.selection_grip(width, index, start))
258 }
259
260 fn selection_hit(&self, local_x: f32, local_y: f32, width: f32) -> Option<usize> {
261 self.inner
262 .as_ref()
263 .and_then(|w| w.selection_hit(local_x, local_y, width))
264 }
265
266 fn word_at(&self, index: usize) -> Option<(usize, usize)> {
267 self.inner.as_ref().and_then(|w| w.word_at(index))
268 }
269
270 fn selection_handles(
271 &self,
272 width: f32,
273 edit: &Edit,
274 scroll_y: f32,
275 ) -> Option<[crate::SelectionHandle; 2]> {
276 self.inner
277 .as_ref()
278 .and_then(|w| w.selection_handles(width, edit, scroll_y))
279 }
280
281 fn selection_toolbar(&self, context: crate::ToolbarContext) -> Option<&dyn Widget<Msg>> {
282 self.inner
283 .as_ref()
284 .and_then(|w| w.selection_toolbar(context))
285 }
286
287 fn selection_anchor(&self, width: f32, edit: &Edit, scroll_y: f32) -> Option<Rect> {
288 self.inner
289 .as_ref()
290 .and_then(|w| w.selection_anchor(width, edit, scroll_y))
291 }
292
293 fn edit_action(&self) -> Option<crate::EditAction> {
294 self.inner.as_ref().and_then(|w| w.edit_action())
295 }
296
297 fn autofill_hints(&self) -> &[crate::AutofillHint] {
298 self.inner
299 .as_ref()
300 .map(|w| w.autofill_hints())
301 .unwrap_or(&[])
302 }
303
304 fn autofill_group(&self) -> bool {
305 self.inner.as_ref().is_some_and(|w| w.autofill_group())
306 }
307
308 fn area_toolbar(&self, context: crate::ToolbarContext) -> Option<Option<&dyn Widget<Msg>>> {
309 self.inner.as_ref().and_then(|w| w.area_toolbar(context))
310 }
311
312 fn focusable(&self) -> bool {
313 self.inner.as_ref().is_some_and(|w| w.focusable())
314 }
315
316 fn draws_own_focus(&self) -> bool {
317 self.inner.as_ref().is_some_and(|w| w.draws_own_focus())
318 }
319
320 fn stack(&self) -> bool {
321 self.inner.as_ref().is_some_and(|w| w.stack())
322 }
323
324 fn stack_fit(&self) -> crate::StackFit {
325 self.inner
326 .as_ref()
327 .map_or(crate::StackFit::Passthrough, |w| w.stack_fit())
328 }
329
330 fn stack_measured(&self) -> bool {
331 self.inner.as_ref().is_some_and(|w| w.stack_measured())
332 }
333
334 fn stack_clips(&self) -> bool {
335 self.inner.as_ref().is_none_or(|w| w.stack_clips())
336 }
337
338 fn stack_direction(&self) -> Option<frus_core::TextDirection> {
339 self.inner.as_ref().and_then(|w| w.stack_direction())
340 }
341
342 fn stack_visible(&self) -> Option<usize> {
343 self.inner.as_ref().and_then(|w| w.stack_visible())
344 }
345
346 fn continuous(&self) -> bool {
347 self.inner.as_ref().is_some_and(|w| w.continuous())
348 }
349
350 fn virtual_list(&self, viewport: frus_core::Size) -> Option<crate::list::VirtualList<'_, Msg>> {
351 self.inner.as_ref().and_then(|w| w.virtual_list(viewport))
352 }
353
354 fn page_view(&self) -> Option<crate::pageview::PagedView<'_, Msg>> {
355 self.inner.as_ref().and_then(|w| w.page_view())
356 }
357
358 fn intrinsic(&self) -> Option<(crate::constraints::IntrinsicAxis, Option<f32>)> {
359 self.inner.as_ref().and_then(|w| w.intrinsic())
360 }
361
362 fn overflow_box(&self) -> Option<crate::constraints::Overflow> {
363 self.inner.as_ref().and_then(|w| w.overflow_box())
364 }
365
366 fn hero_tag(&self) -> Option<u64> {
367 self.inner.as_ref().and_then(|w| w.hero_tag())
368 }
369
370 fn drag_payload(&self) -> Option<u64> {
371 self.inner.as_ref().and_then(|w| w.drag_payload())
372 }
373
374 fn drag_needs_long_press(&self) -> bool {
375 self.inner
376 .as_ref()
377 .is_some_and(|w| w.drag_needs_long_press())
378 }
379
380 fn drag_ghost_opacity(&self) -> f32 {
381 self.inner.as_ref().map_or(1.0, |w| w.drag_ghost_opacity())
382 }
383
384 fn on_dropped(&self, accepted: bool) -> Option<Msg> {
385 self.inner.as_ref().and_then(|w| w.on_dropped(accepted))
386 }
387
388 fn drop_zone(&self) -> bool {
389 self.inner.as_ref().is_some_and(|w| w.drop_zone())
390 }
391
392 fn accepts_drag(&self, payload: u64) -> bool {
393 self.inner.as_ref().is_none_or(|w| w.accepts_drag(payload))
394 }
395
396 fn on_drop(&self, payload: u64) -> Option<Msg> {
397 self.inner.as_ref().and_then(|w| w.on_drop(payload))
398 }
399
400 fn on_page_changed(&self, page: usize) -> Option<Msg> {
401 self.inner.as_ref().and_then(|w| w.on_page_changed(page))
402 }
403
404 fn on_scroll(&self, position: crate::ScrollPosition) -> Option<Msg> {
405 self.inner.as_ref().and_then(|w| w.on_scroll(position))
406 }
407
408 fn scroll_grain(&self) -> f32 {
409 self.inner.as_ref().map_or(0.0, |w| w.scroll_grain())
410 }
411
412 fn layout_builder(&self) -> Option<&dyn Fn(frus_core::Size) -> Box<dyn Widget<Msg>>> {
413 self.inner.as_ref().and_then(|w| w.layout_builder())
414 }
415
416 fn scroll_overlay(&self) -> Option<&crate::scrolloverlay::OverlayBuilder<Msg>> {
417 self.inner.as_ref().and_then(|w| w.scroll_overlay())
418 }
419
420 fn draggable(&self) -> bool {
421 self.inner.as_ref().is_some_and(|w| w.draggable())
422 }
423 fn drag_track(&self) -> crate::widget::DragTrack {
424 self.inner
425 .as_ref()
426 .map_or(crate::widget::DragTrack::WHOLE, |w| w.drag_track())
427 }
428
429 fn on_drag(&self, fraction: f32) -> Option<Msg> {
430 self.inner.as_ref().and_then(|w| w.on_drag(fraction))
431 }
432
433 fn on_drag_start(&self, fraction: f32) -> Option<Msg> {
434 self.inner.as_ref().and_then(|w| w.on_drag_start(fraction))
435 }
436
437 fn on_drag_end(&self, fraction: f32) -> Option<Msg> {
438 self.inner.as_ref().and_then(|w| w.on_drag_end(fraction))
439 }
440
441 fn on_drag_delta(&self, dx: f32) -> Option<Msg> {
442 self.inner.as_ref().and_then(|w| w.on_drag_delta(dx))
443 }
444
445 fn reorder_index(&self) -> Option<usize> {
446 self.inner.as_ref().and_then(|w| w.reorder_index())
447 }
448
449 fn on_reorder(&self, to: usize) -> Option<Msg> {
450 self.inner.as_ref().and_then(|w| w.on_reorder(to))
451 }
452
453 fn announce(&self) -> Option<String> {
454 self.inner.as_ref().and_then(|w| w.announce())
455 }
456
457 fn overlay_traps_focus(&self) -> bool {
458 self.inner.as_ref().is_some_and(|w| w.overlay_traps_focus())
459 }
460
461 fn scroll_content(&self) -> Option<&dyn Widget<Msg>> {
462 self.inner.as_ref().and_then(|w| w.scroll_content())
463 }
464
465 fn scroll_axis(&self) -> Axis {
466 self.inner
467 .as_ref()
468 .map(|w| w.scroll_axis())
469 .unwrap_or(Axis::Vertical)
470 }
471
472 fn scroll_physics(&self) -> Option<crate::physics::ScrollPhysics> {
473 self.inner.as_ref().and_then(|w| w.scroll_physics())
474 }
475
476 fn keep_visible(
477 &self,
478 size: frus_core::Size,
479 theme: &crate::theme::Theme,
480 ) -> Option<crate::ui::KeepVisible> {
481 self.inner
482 .as_ref()
483 .and_then(|w| w.keep_visible(size, theme))
484 }
485
486 fn ime(&self) -> crate::ime::Ime {
487 self.inner.as_ref().map(|w| w.ime()).unwrap_or_default()
488 }
489
490 fn overlay(&self) -> Option<(&dyn Widget<Msg>, Placement)> {
491 self.inner.as_ref().and_then(|w| w.overlay())
492 }
493
494 fn overlay_dismiss(&self) -> Option<Msg> {
495 self.inner.as_ref().and_then(|w| w.overlay_dismiss())
496 }
497
498 fn overlay_scrim(&self, theme: &Theme) -> Option<frus_core::Color> {
499 self.inner.as_ref().and_then(|w| w.overlay_scrim(theme))
500 }
501
502 fn anim_target(&self) -> Option<f32> {
503 self.inner.as_ref().and_then(|w| w.anim_target())
504 }
505
506 fn anim_duration(&self) -> f32 {
507 self.inner
508 .as_ref()
509 .map(|w| w.anim_duration())
510 .unwrap_or(crate::runtime::ANIM_DURATION)
511 }
512
513 fn anim_curve(&self) -> frus_core::Curve {
514 self.inner
515 .as_ref()
516 .map(|w| w.anim_curve())
517 .unwrap_or(frus_core::Curve::Linear)
518 }
519
520 fn opacity_group(&self) -> Option<f32> {
521 self.inner.as_ref().and_then(|w| w.opacity_group())
522 }
523
524 fn anim_color(&self) -> Option<frus_core::Color> {
525 self.inner.as_ref().and_then(|w| w.anim_color())
526 }
527
528 fn anim_size(&self) -> Option<frus_core::Size> {
529 self.inner.as_ref().and_then(|w| w.anim_size())
530 }
531
532 fn anim_radius(&self) -> Option<frus_core::BorderRadius> {
533 self.inner.as_ref().and_then(|w| w.anim_radius())
534 }
535
536 fn anim_padding(&self) -> Option<frus_core::Insets> {
537 self.inner.as_ref().and_then(|w| w.anim_padding())
538 }
539
540 fn anim_transform(&self) -> Option<crate::runtime::TransformValues> {
541 self.inner.as_ref().and_then(|w| w.anim_transform())
542 }
543
544 fn anim_offset(&self) -> Option<(f32, f32)> {
550 self.inner.as_ref().and_then(|w| w.anim_offset())
551 }
552
553 fn anim_pins(&self) -> Option<crate::positioned::Positioning> {
554 self.inner.as_ref().and_then(|w| w.anim_pins())
555 }
556
557 fn anim_fractions(&self) -> Option<(Option<f32>, Option<f32>)> {
558 self.inner.as_ref().and_then(|w| w.anim_fractions())
559 }
560 fn anim_text_style(&self) -> Option<frus_core::TextStyle> {
561 self.inner.as_ref().and_then(|w| w.anim_text_style())
562 }
563
564 fn positioned(&self) -> Option<crate::positioned::Positioning> {
565 self.inner.as_ref().and_then(|w| w.positioned())
566 }
567
568 fn alignment_geometry(&self) -> Option<frus_core::AlignmentGeometry> {
569 self.inner.as_ref().and_then(|w| w.alignment_geometry())
570 }
571
572 fn transform_translate(&self) -> Option<(f32, f32)> {
573 self.inner.as_ref().and_then(|w| w.transform_translate())
574 }
575
576 fn transform_scale(&self) -> Option<(f32, f32, frus_core::Alignment)> {
577 self.inner.as_ref().and_then(|w| w.transform_scale())
578 }
579
580 fn transform_rotate(&self) -> Option<(f32, frus_core::Alignment)> {
581 self.inner.as_ref().and_then(|w| w.transform_rotate())
582 }
583
584 fn clip_shape(&self) -> Option<frus_core::ClipShape> {
585 self.inner.as_ref().and_then(|w| w.clip_shape())
586 }
587
588 fn clip_path(&self) -> Option<&frus_core::Path> {
589 self.inner.as_ref().and_then(|w| w.clip_path())
590 }
591
592 fn barrier(&self) -> Option<crate::barrier::ModalBarrier> {
593 self.inner.as_ref().and_then(|w| w.barrier())
594 }
595
596 fn dismissible(&self) -> Option<crate::dismiss::DismissSpec> {
597 self.inner.as_ref().and_then(|w| w.dismissible())
598 }
599
600 fn on_dismissed(&self, direction: crate::dismiss::DismissDirection) -> Option<Msg> {
601 self.inner.as_ref().and_then(|w| w.on_dismissed(direction))
602 }
603
604 fn sheet(&self) -> Option<&crate::sheet::SheetSpec> {
605 self.inner.as_ref().and_then(|w| w.sheet())
606 }
607
608 fn on_sheet_dismissed(&self) -> Option<Msg> {
609 self.inner.as_ref().and_then(|w| w.on_sheet_dismissed())
610 }
611
612 fn refresh(&self) -> Option<crate::refresh::RefreshSpec> {
613 self.inner.as_ref().and_then(|w| w.refresh())
614 }
615
616 fn on_refresh(&self) -> Option<Msg> {
617 self.inner.as_ref().and_then(|w| w.on_refresh())
618 }
619
620 fn interactive(&self) -> Option<(f32, f32)> {
621 self.inner.as_ref().and_then(|w| w.interactive())
622 }
623
624 fn fitted(&self) -> Option<frus_core::BoxFit> {
625 self.inner.as_ref().and_then(|w| w.fitted())
626 }
627
628 fn rotated_quarter_turns(&self) -> Option<i32> {
629 self.inner.as_ref().and_then(|w| w.rotated_quarter_turns())
630 }
631
632 fn navigator(&self) -> Option<(f32, bool)> {
633 self.inner.as_ref().and_then(|w| w.navigator())
634 }
635
636 fn navigator_gesture(&self) -> bool {
637 self.inner.as_ref().is_some_and(|w| w.navigator_gesture())
638 }
639
640 fn navigator_retained(&self) -> usize {
641 self.inner.as_ref().map_or(0, |w| w.navigator_retained())
642 }
643
644 fn measure(&self, theme: &Theme) -> Option<frus_layout::MeasureFn<'_>> {
645 self.inner.as_ref().and_then(|w| w.measure(theme))
646 }
647
648 fn measure_key(&self, theme: &Theme) -> Option<u64> {
649 self.inner.as_ref().and_then(|w| w.measure_key(theme))
650 }
651
652 fn on_long_press(&self) -> Option<Msg> {
653 self.inner.as_ref().and_then(|w| w.on_long_press())
654 }
655
656 fn on_double_tap(&self) -> Option<Msg> {
657 self.inner.as_ref().and_then(|w| w.on_double_tap())
658 }
659
660 fn on_secondary_tap(&self) -> Option<Msg> {
661 self.inner.as_ref().and_then(|w| w.on_secondary_tap())
662 }
663
664 fn pan_axis(&self) -> Option<crate::PanAxis> {
665 self.inner.as_ref().and_then(|w| w.pan_axis())
666 }
667
668 fn hover_region(&self) -> Option<crate::HoverRegion> {
669 self.inner.as_ref().and_then(|w| w.hover_region())
670 }
671
672 fn pointer_listener(&self) -> bool {
673 self.inner.as_ref().is_some_and(|w| w.pointer_listener())
674 }
675
676 fn on_pointer_event(&self, event: crate::ListenerEvent) -> Vec<Msg> {
677 self.inner
678 .as_ref()
679 .map(|w| w.on_pointer_event(event))
680 .unwrap_or_default()
681 }
682
683 fn on_hover_event(&self, event: crate::HoverEvent) -> Option<Msg> {
684 self.inner.as_ref().and_then(|w| w.on_hover_event(event))
685 }
686
687 fn on_pan(&self, event: crate::PanEvent) -> Option<Msg> {
688 self.inner.as_ref().and_then(|w| w.on_pan(event))
689 }
690
691 fn pan_value(&self, event: crate::PanEvent, value: f32, rtl: bool) -> Option<f32> {
692 self.inner
693 .as_ref()
694 .and_then(|w| w.pan_value(event, value, rtl))
695 }
696
697 fn on_value_release(&self, value: f32) -> Option<Msg> {
698 self.inner.as_ref().and_then(|w| w.on_value_release(value))
699 }
700
701 fn on_key(&self, key: &crate::Key) -> crate::KeyResponse<Msg> {
702 self.inner
703 .as_ref()
704 .map(|w| w.on_key(key))
705 .unwrap_or(crate::KeyResponse::Ignored)
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712 use crate::Container;
713 use frus_layout::Dimension;
714
715 fn chosen_width(r: &Responsive<()>) -> Dimension {
718 Widget::<()>::style(r).width
719 }
720
721 fn tagged(w: f32) -> Responsive<()> {
722 responsive(w)
723 .compact(Container::new().width(100.0))
724 .medium(Container::new().width(200.0))
725 .expanded(Container::new().width(300.0))
726 }
727
728 #[test]
729 fn picks_variant_for_width() {
730 assert_eq!(chosen_width(&tagged(400.0)), Dimension::Length(100.0));
731 assert_eq!(chosen_width(&tagged(700.0)), Dimension::Length(200.0));
732 assert_eq!(chosen_width(&tagged(1200.0)), Dimension::Length(300.0));
733 }
734
735 #[test]
736 fn falls_back_to_nearest_when_missing() {
737 let only_expanded = |w: f32| responsive::<()>(w).expanded(Container::new().width(300.0));
739 assert_eq!(
740 chosen_width(&only_expanded(300.0)),
741 Dimension::Length(300.0)
742 );
743
744 let two = responsive::<()>(700.0)
747 .compact(Container::new().width(100.0))
748 .expanded(Container::new().width(300.0));
749 assert_eq!(chosen_width(&two), Dimension::Length(100.0));
750 }
751
752 #[test]
761 fn a_selector_keeps_the_layers_place_in_the_stack() {
762 let pinned = responsive::<()>(400.0).compact(
763 crate::Positioned::new(Container::new().width(20.0).height(20.0))
764 .top(8.0)
765 .right(12.0),
766 );
767 let spec = Widget::<()>::positioned(&pinned).expect("the pins reached through");
768 assert_eq!((spec.top, spec.right), (Some(8.0), Some(12.0)));
769 }
770
771 #[test]
775 fn a_selector_keeps_the_childs_animated_offset() {
776 let sliding = responsive::<()>(400.0).compact(crate::AnimatedAlign::new(
777 frus_core::Alignment::CENTER_RIGHT,
778 0.2,
779 frus_core::Curve::Linear,
780 Container::new().width(20.0).height(20.0),
781 ));
782 assert_eq!(
783 Widget::<()>::anim_offset(&sliding),
784 Some((1.0, 0.0)),
787 "the anchor's two fractions reached through"
788 );
789 }
790
791 #[test]
796 fn a_selector_builds_a_deferred_variant() {
797 let chosen = responsive::<()>(400.0).compact(crate::ThemeBuilder::new(|_: &Theme| {
798 Container::new()
799 .width(30.0)
800 .height(10.0)
801 .color(frus_core::Color::rgb(1.0, 0.0, 0.0))
802 }));
803 let ui = crate::ui::build_ui(
804 &chosen,
805 frus_core::Size::new(100.0, 100.0),
806 &crate::Runtime::default(),
807 &Theme::default(),
808 );
809 assert!(
810 ui.scene()
811 .primitives()
812 .iter()
813 .any(|p| matches!(p, frus_core::Primitive::Rect { color, .. } if color.r > 0.5)),
814 "the variant was built, and painted"
815 );
816 }
817}