1use std::cell::{OnceCell, RefCell};
34use std::collections::HashMap;
35
36use frus_core::{ClampingScrollSimulation, Curve, Point, Rect, Scene, Simulation, Tolerance};
37use frus_layout::{Align, Dimension, FlexDirection, Justify, Style};
38
39use crate::interaction::{Status, WidgetId};
40use crate::theme::Theme;
41use crate::widget::Widget;
42
43pub const SHEET_SNAP_MIN_SPEED: f32 = 1600.0;
46
47#[derive(Clone, Debug, PartialEq)]
50pub struct SheetSpec {
51 pub min: f32,
53 pub max: f32,
55 pub initial: f32,
57 pub snap: bool,
60 pub stops: Vec<f32>,
63 pub dismissible: bool,
65}
66
67impl SheetSpec {
68 pub fn floor(&self) -> f32 {
70 if self.dismissible {
71 0.0
72 } else {
73 self.min.min(self.max)
74 }
75 }
76
77 pub fn clamp(&self, size: f32) -> f32 {
79 size.clamp(self.floor(), self.max)
80 }
81}
82
83#[derive(Clone, Debug, PartialEq)]
86pub struct SheetArea {
87 pub id: WidgetId,
89 pub panel: Rect,
91 pub available: f32,
93 pub spec: SheetSpec,
95 pub areas: Vec<WidgetId>,
98}
99
100impl SheetArea {
101 pub fn contains(&self, point: Point) -> bool {
103 let p = self.panel;
104 p.height > 0.0
105 && point.x >= p.x
106 && point.x < p.x + p.width
107 && point.y >= p.y
108 && point.y < p.y + p.height
109 }
110}
111
112#[derive(Clone, Debug, PartialEq)]
130pub struct SheetTo {
131 target: SheetTarget,
132 animation: Option<(f32, Curve)>,
134}
135
136#[derive(Clone, Copy, Debug, PartialEq)]
138enum SheetTarget {
139 Size(f32),
140 Initial,
141}
142
143impl SheetTo {
144 pub fn size(size: f32) -> Self {
146 Self {
147 target: SheetTarget::Size(size),
148 animation: None,
149 }
150 }
151
152 pub fn initial() -> Self {
154 Self {
155 target: SheetTarget::Initial,
156 animation: None,
157 }
158 }
159
160 #[must_use]
163 pub fn animate(mut self, duration: f32, curve: Curve) -> Self {
164 self.animation = (duration > 0.0).then_some((duration, curve));
165 self
166 }
167}
168
169pub fn split_sheet_drag(
178 grow: f32,
179 list_offset: f32,
180 size: f32,
181 floor: f32,
182 max: f32,
183) -> (f32, f32) {
184 if grow > 0.0 {
185 if list_offset > 0.0 {
187 return (0.0, grow);
188 }
189 let sheet = grow.min((max - size).max(0.0));
190 (sheet, grow - sheet)
191 } else if grow < 0.0 {
192 let down = -grow;
193 let list_back = down.min(list_offset.max(0.0));
194 let rest = down - list_back;
195 let sheet = rest.min((size - floor).max(0.0));
196 (-sheet, -(list_back + rest - sheet))
197 } else {
198 (0.0, 0.0)
199 }
200}
201
202pub fn sheet_takes_release(
209 velocity: f32,
210 list_offset: f32,
211 size: f32,
212 spec: &SheetSpec,
213 available: f32,
214) -> bool {
215 let tolerance = Tolerance::PIXELS;
216 let still = velocity.abs() <= tolerance.velocity;
217 let px = size * available;
218 let on_stop = !spec.snap
219 || spec
220 .stops
221 .iter()
222 .any(|stop| (stop * available - px).abs() <= tolerance.distance);
223 let at_max = px >= spec.max * available - tolerance.distance;
224 !((still && on_stop) || (velocity < 0.0 && list_offset > 0.0) || (velocity > 0.0 && at_max))
225}
226
227#[derive(Clone, Copy, Debug, PartialEq)]
232pub struct SnapSimulation {
233 start: f32,
234 target: f32,
235 velocity: f32,
236}
237
238impl SnapSimulation {
239 pub fn new(position: f32, velocity: f32, target: f32) -> Self {
242 let velocity = if target < position {
243 velocity.min(-SHEET_SNAP_MIN_SPEED)
244 } else {
245 velocity.max(SHEET_SNAP_MIN_SPEED)
246 };
247 Self {
248 start: position,
249 target,
250 velocity,
251 }
252 }
253
254 pub fn target(&self) -> f32 {
256 self.target
257 }
258
259 pub fn velocity(&self) -> f32 {
261 self.velocity
262 }
263}
264
265impl Simulation for SnapSimulation {
266 fn x(&self, time: f32) -> f32 {
267 let position = self.start + self.velocity * time;
268 if (self.velocity >= 0.0 && position > self.target)
269 || (self.velocity < 0.0 && position < self.target)
270 {
271 self.target
272 } else {
273 position
274 }
275 }
276
277 fn dx(&self, time: f32) -> f32 {
278 if self.is_done(time) {
279 0.0
280 } else {
281 self.velocity
282 }
283 }
284
285 fn is_done(&self, time: f32) -> bool {
286 self.x(time) == self.target
287 }
288}
289
290#[derive(Clone, Copy, Debug)]
292enum Motion {
293 Snap(SnapSimulation),
295 Coast(ClampingScrollSimulation),
297}
298
299#[derive(Clone, Copy, Debug)]
301struct Settle {
302 motion: Motion,
303 elapsed: f32,
304 available: f32,
305 floor: f32,
306 max: f32,
307 list: Option<WidgetId>,
310}
311
312#[derive(Clone, Debug)]
315struct Drive {
316 from: f32,
317 to: f32,
318 duration: f32,
319 curve: Curve,
320 elapsed: f32,
321}
322
323#[derive(Clone, Debug)]
325pub struct SheetState {
326 size: f32,
327 held: bool,
328 settle: Option<Settle>,
329 drive: Option<Drive>,
331 dismissed: bool,
332 available: f32,
333 handover: Option<(WidgetId, f32)>,
336}
337
338impl SheetState {
339 pub fn new(spec: &SheetSpec) -> Self {
341 Self {
342 size: spec.clamp(spec.initial),
343 held: false,
344 settle: None,
345 drive: None,
346 dismissed: false,
347 available: 0.0,
348 handover: None,
349 }
350 }
351
352 pub fn size(&self) -> f32 {
354 self.size
355 }
356
357 pub fn is_settling(&self) -> bool {
359 self.settle.is_some() || self.drive.is_some()
360 }
361
362 pub fn available(&self) -> f32 {
365 self.available
366 }
367
368 fn hold(&mut self) {
371 self.held = true;
372 self.settle = None;
373 self.drive = None;
374 }
375
376 fn drag(&mut self, spec: &SheetSpec, delta: f32, available: f32) {
378 self.hold();
379 if available > 0.0 {
380 self.available = available;
381 }
382 self.size = spec.clamp(self.size + delta);
383 if self.size > 0.0 {
384 self.dismissed = false;
385 }
386 }
387
388 fn release(&mut self, spec: &SheetSpec, available: f32, velocity: f32, list: Option<WidgetId>) {
392 self.held = false;
393 self.settle = None;
394 self.drive = None;
395 if available <= 0.0 {
396 return;
397 }
398 self.available = available;
399 let tolerance = Tolerance::PIXELS;
400 let position = self.size * available;
401 let motion = if spec.snap {
402 let stops: Vec<f32> = spec.stops.iter().map(|stop| stop * available).collect();
403 let target =
404 crate::physics::snap_target(position, velocity, &stops, tolerance.velocity);
405 Motion::Snap(SnapSimulation::new(position, velocity, target))
406 } else if velocity.abs() > tolerance.velocity {
407 Motion::Coast(ClampingScrollSimulation::new(position, velocity, tolerance))
408 } else {
409 return;
410 };
411 self.settle = Some(Settle {
412 motion,
413 elapsed: 0.0,
414 available,
415 floor: spec.floor() * available,
416 max: spec.max * available,
417 list: list
422 .filter(|_| velocity > 0.0 && position < spec.max * available - tolerance.distance),
423 });
424 }
425
426 fn go(&mut self, spec: &SheetSpec, to: &SheetTo) -> bool {
428 if self.held {
429 return false;
430 }
431 self.settle = None;
432 self.drive = None;
433 let target = spec.clamp(match to.target {
434 SheetTarget::Size(size) => size,
435 SheetTarget::Initial => spec.initial,
436 });
437 match &to.animation {
438 Some((duration, curve)) if target != self.size => {
439 self.drive = Some(Drive {
440 from: self.size,
441 to: target,
442 duration: *duration,
443 curve: curve.clone(),
444 elapsed: 0.0,
445 });
446 }
447 _ => self.size = target,
448 }
449 if target > 0.0 {
451 self.dismissed = false;
452 }
453 true
454 }
455
456 fn advance(&mut self, spec: &SheetSpec, dt: f32) -> (bool, bool) {
458 if self.held {
459 return (false, false);
460 }
461 let mut moving = false;
462 if let Some(drive) = &mut self.drive {
465 drive.elapsed += dt;
466 let t = (drive.elapsed / drive.duration).min(1.0);
467 self.size = spec.clamp(drive.from + (drive.to - drive.from) * drive.curve.transform(t));
468 if t >= 1.0 {
469 self.size = drive.to;
470 self.drive = None;
471 } else {
472 moving = true;
473 }
474 }
475 if let Some(settle) = &mut self.settle {
476 settle.elapsed += dt;
477 let at_max = |x: f32| x >= settle.max - Tolerance::PIXELS.distance;
480 let (x, done, rising) = match settle.motion {
481 Motion::Snap(snap) => {
482 let x = snap.x(settle.elapsed);
483 let done = snap.is_done(settle.elapsed);
484 (x, done, (done && at_max(x)).then_some(snap.velocity()))
485 }
486 Motion::Coast(coast) => {
487 let x = coast.x(settle.elapsed);
488 let out = x <= settle.floor || x >= settle.max;
490 let done = out || coast.is_done(settle.elapsed);
491 (x, done, at_max(x).then(|| coast.dx(settle.elapsed)))
492 }
493 };
494 self.size = x.clamp(settle.floor, settle.max) / settle.available;
495 if let (Some(velocity), Some(list)) = (rising.filter(|v| *v > 0.0), settle.list) {
500 self.handover = Some((list, velocity + Tolerance::PIXELS.velocity));
501 self.settle = None;
502 return (false, false);
503 }
504 if done {
505 self.settle = None;
506 } else {
507 moving = true;
508 }
509 }
510 if self.settle.is_none()
511 && self.drive.is_none()
512 && spec.dismissible
513 && !self.dismissed
514 && self.size <= 1e-4
515 {
516 self.size = 0.0;
517 self.dismissed = true;
518 return (moving, true);
519 }
520 (moving, false)
521 }
522}
523
524pub(crate) fn size_of(
527 states: &HashMap<WidgetId, SheetState>,
528 id: WidgetId,
529 spec: &SheetSpec,
530) -> f32 {
531 states
532 .get(&id)
533 .map(|state| state.size)
534 .unwrap_or_else(|| spec.clamp(spec.initial))
535}
536
537pub(crate) fn drag_into(
540 states: &mut HashMap<WidgetId, SheetState>,
541 id: WidgetId,
542 spec: &SheetSpec,
543 delta: f32,
544 available: f32,
545) {
546 states
547 .entry(id)
548 .or_insert_with(|| SheetState::new(spec))
549 .drag(spec, delta, available);
550}
551
552pub(crate) fn hold_of(states: &mut HashMap<WidgetId, SheetState>, id: WidgetId) {
554 if let Some(state) = states.get_mut(&id) {
555 state.hold();
556 }
557}
558
559pub(crate) fn release_of(
561 states: &mut HashMap<WidgetId, SheetState>,
562 id: WidgetId,
563 spec: &SheetSpec,
564 available: f32,
565 velocity: f32,
566 list: Option<WidgetId>,
567) {
568 if let Some(state) = states.get_mut(&id) {
569 state.release(spec, available, velocity, list);
570 }
571}
572
573pub(crate) fn go_to(
576 states: &mut HashMap<WidgetId, SheetState>,
577 id: WidgetId,
578 spec: &SheetSpec,
579 to: &SheetTo,
580) -> bool {
581 states
582 .entry(id)
583 .or_insert_with(|| SheetState::new(spec))
584 .go(spec, to)
585}
586
587pub(crate) struct SheetsStep {
589 pub moving: bool,
591 pub dismissed: Vec<WidgetId>,
593 pub handovers: Vec<(WidgetId, f32)>,
596}
597
598pub(crate) fn advance_all(
601 states: &mut HashMap<WidgetId, SheetState>,
602 areas: &[SheetArea],
603 dt: f32,
604) -> SheetsStep {
605 let mut step = SheetsStep {
606 moving: false,
607 dismissed: Vec::new(),
608 handovers: Vec::new(),
609 };
610 states.retain(|id, state| {
611 let Some(area) = areas.iter().find(|area| area.id == *id) else {
612 return false;
613 };
614 let (still, gone) = state.advance(&area.spec, dt);
615 step.moving |= still;
616 if gone {
617 step.dismissed.push(*id);
618 }
619 step.handovers.extend(state.handover.take());
620 true
621 });
622 step
623}
624
625pub struct DraggableScrollableSheet<Msg = crate::callback::Callback> {
638 spec: SheetSpec,
639 sizes: Vec<f32>,
640 on_dismiss: Option<Msg>,
641 child: RefCell<Option<Box<dyn Widget<Msg>>>>,
642 built: OnceCell<Vec<Box<dyn Widget<Msg>>>>,
643}
644
645impl<Msg: Clone + 'static> DraggableScrollableSheet<Msg> {
646 pub fn new(child: impl Widget<Msg> + 'static) -> Self {
648 let mut sheet = Self {
649 spec: SheetSpec {
650 min: 0.25,
651 max: 1.0,
652 initial: 0.5,
653 snap: false,
654 stops: Vec::new(),
655 dismissible: false,
656 },
657 sizes: Vec::new(),
658 on_dismiss: None,
659 child: RefCell::new(Some(Box::new(child))),
660 built: OnceCell::new(),
661 };
662 sheet.restop();
663 sheet
664 }
665
666 #[must_use]
668 pub fn initial(mut self, size: f32) -> Self {
669 self.spec.initial = size.clamp(0.0, 1.0);
670 self
671 }
672
673 #[must_use]
675 pub fn min(mut self, size: f32) -> Self {
676 self.spec.min = size.clamp(0.0, 1.0);
677 self.restop();
678 self
679 }
680
681 #[must_use]
683 pub fn max(mut self, size: f32) -> Self {
684 self.spec.max = size.clamp(0.0, 1.0);
685 self.restop();
686 self
687 }
688
689 #[must_use]
692 pub fn snap(mut self, snap: bool) -> Self {
693 self.spec.snap = snap;
694 self
695 }
696
697 #[must_use]
699 pub fn snap_sizes(mut self, sizes: impl IntoIterator<Item = f32>) -> Self {
700 self.sizes = sizes.into_iter().collect();
701 self.spec.snap = true;
702 self.restop();
703 self
704 }
705
706 #[must_use]
709 pub fn on_dismiss(mut self, message: impl Into<Msg>) -> Self {
710 self.on_dismiss = Some(message.into());
711 self.spec.dismissible = true;
712 self.restop();
713 self
714 }
715
716 fn restop(&mut self) {
717 let (min, max) = (self.spec.min.min(self.spec.max), self.spec.max);
718 let mut stops = Vec::with_capacity(self.sizes.len() + 3);
719 if self.spec.dismissible {
720 stops.push(0.0);
721 }
722 stops.push(min);
723 stops.extend(self.sizes.iter().copied().filter(|s| *s > min && *s < max));
724 stops.push(max);
725 stops.sort_by(f32::total_cmp);
726 stops.dedup();
727 self.spec.stops = stops;
728 }
729}
730
731impl<Msg: Clone + 'static> Widget<Msg> for DraggableScrollableSheet<Msg> {
732 fn style(&self) -> Style {
734 Style {
735 width: Dimension::Percent(1.0),
736 height: Dimension::Percent(1.0),
737 flex_direction: FlexDirection::Column,
738 justify: Justify::End,
739 ..Default::default()
740 }
741 }
742
743 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
744 self.built.get_or_init(|| {
745 let child = self.child.borrow_mut().take();
746 vec![Box::new(SheetPanel {
747 spec: self.spec.clone(),
748 on_dismiss: self.on_dismiss.clone(),
749 children: child.into_iter().collect(),
750 })]
751 })
752 }
753
754 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
755
756 fn on_click(&self) -> Option<Msg> {
757 None
758 }
759
760 fn debug_name(&self) -> &'static str {
761 "DraggableScrollableSheet"
762 }
763}
764
765struct SheetPanel<Msg> {
768 spec: SheetSpec,
769 on_dismiss: Option<Msg>,
770 children: Vec<Box<dyn Widget<Msg>>>,
771}
772
773impl<Msg: Clone> Widget<Msg> for SheetPanel<Msg> {
774 fn style(&self) -> Style {
777 Style {
778 width: Dimension::Percent(1.0),
779 height: Dimension::Percent(self.spec.clamp(self.spec.initial)),
780 overlap: true,
781 align: Align::Stretch,
782 ..Default::default()
783 }
784 }
785
786 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
787 &self.children
788 }
789
790 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
791
792 fn on_click(&self) -> Option<Msg> {
793 None
794 }
795
796 fn sheet(&self) -> Option<&SheetSpec> {
797 Some(&self.spec)
798 }
799
800 fn on_sheet_dismissed(&self) -> Option<Msg> {
801 self.on_dismiss.clone()
802 }
803
804 fn debug_name(&self) -> &'static str {
805 "SheetPanel"
806 }
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812 use crate::{build_ui, Container, Runtime};
813 use frus_core::{Color, Primitive, Size};
814
815 fn spec(snap: bool, dismissible: bool) -> SheetSpec {
816 let mut sheet = DraggableScrollableSheet::<()>::new(Container::new()).min(0.25);
817 if snap {
818 sheet = sheet.snap_sizes([0.5]);
819 }
820 if dismissible {
821 sheet = sheet.on_dismiss(());
822 }
823 sheet.spec
824 }
825
826 #[test]
829 fn up_grows_the_sheet_to_its_max_then_scrolls_the_list() {
830 assert_eq!(
831 split_sheet_drag(30.0, 0.0, 400.0, 200.0, 800.0),
832 (30.0, 0.0)
833 );
834 assert_eq!(
835 split_sheet_drag(30.0, 0.0, 790.0, 200.0, 800.0),
836 (10.0, 20.0),
837 "the handover inside one movement"
838 );
839 assert_eq!(
840 split_sheet_drag(30.0, 0.0, 800.0, 200.0, 800.0),
841 (0.0, 30.0)
842 );
843 assert_eq!(
844 split_sheet_drag(30.0, 5.0, 400.0, 200.0, 800.0),
845 (0.0, 30.0),
846 "a scrolled list keeps it"
847 );
848 }
849
850 #[test]
854 fn down_scrolls_the_list_home_then_lowers_the_sheet() {
855 assert_eq!(
856 split_sheet_drag(-30.0, 100.0, 400.0, 200.0, 800.0),
857 (0.0, -30.0)
858 );
859 assert_eq!(
860 split_sheet_drag(-30.0, 10.0, 400.0, 200.0, 800.0),
861 (-20.0, -10.0),
862 "the handover inside one movement"
863 );
864 assert_eq!(
865 split_sheet_drag(-30.0, 0.0, 400.0, 200.0, 800.0),
866 (-30.0, 0.0)
867 );
868 assert_eq!(
869 split_sheet_drag(-30.0, 0.0, 210.0, 200.0, 800.0),
870 (-10.0, -20.0),
871 "past the floor, the rest to the list"
872 );
873 assert_eq!(split_sheet_drag(0.0, 10.0, 400.0, 200.0, 800.0), (0.0, 0.0));
874 }
875
876 #[test]
878 fn a_release_goes_to_the_sheet_unless_the_list_was_thrown() {
879 let snapping = spec(true, false);
880 assert!(!sheet_takes_release(0.0, 0.0, 0.5, &snapping, 800.0));
882 assert!(sheet_takes_release(0.0, 0.0, 0.4, &snapping, 800.0));
884 assert!(!sheet_takes_release(-900.0, 40.0, 1.0, &snapping, 800.0));
886 assert!(sheet_takes_release(-900.0, 0.0, 1.0, &snapping, 800.0));
888 assert!(!sheet_takes_release(900.0, 0.0, 1.0, &snapping, 800.0));
890 assert!(sheet_takes_release(900.0, 0.0, 0.5, &snapping, 800.0));
891 assert!(!sheet_takes_release(
893 0.0,
894 0.0,
895 0.4,
896 &spec(false, false),
897 800.0
898 ));
899 }
900
901 #[test]
904 fn a_snap_travels_at_least_the_minimum_speed_and_stops_on_the_stop() {
905 let up = SnapSimulation::new(300.0, 100.0, 400.0);
906 assert_eq!(up.dx(0.0), SHEET_SNAP_MIN_SPEED);
907 assert!((up.x(0.05) - 380.0).abs() < 1e-3);
908 assert_eq!(
910 up.x(0.09),
911 400.0,
912 "the frame it passes the stop, on the stop"
913 );
914 assert_eq!(up.x(1.0), 400.0, "never past it");
915 assert!(up.is_done(1.0) && up.dx(1.0) == 0.0);
916 let fast = SnapSimulation::new(300.0, 4000.0, 400.0);
917 assert_eq!(fast.dx(0.0), 4000.0, "a faster release keeps its speed");
918 let against = SnapSimulation::new(300.0, 50.0, 200.0);
919 assert_eq!(against.dx(0.0), -SHEET_SNAP_MIN_SPEED, "towards the stop");
920 assert_eq!(against.x(1.0), 200.0);
921 }
922
923 #[test]
926 fn a_request_moves_a_sheet_at_once_within_its_bounds() {
927 let snapping = spec(true, false);
928 let mut state = SheetState::new(&snapping);
929 assert!(state.go(&snapping, &SheetTo::size(0.9)));
930 assert_eq!(state.size(), 0.9);
931 assert!(!state.is_settling(), "at once is at once");
932 state.go(&snapping, &SheetTo::size(3.0));
933 assert_eq!(state.size(), 1.0, "no higher than max");
934 state.go(&snapping, &SheetTo::size(0.0));
935 assert_eq!(
936 state.size(),
937 0.25,
938 "no lower than min, on a sheet that stays open"
939 );
940 state.go(&snapping, &SheetTo::initial());
941 assert_eq!(state.size(), 0.5, "and back where it started");
942 }
943
944 #[test]
947 fn an_animated_request_follows_its_curve_and_stays_where_it_arrives() {
948 let snapping = spec(true, false);
949 let mut state = SheetState::new(&snapping);
950 state.go(&snapping, &SheetTo::size(0.8).animate(0.3, Curve::Linear));
951 assert_eq!(state.size(), 0.5, "nothing moves before a frame");
952 let (moving, _) = state.advance(&snapping, 0.15);
953 assert!(moving);
954 assert!(
955 (state.size() - 0.65).abs() < 1e-4,
956 "half-way along a straight line: {}",
957 state.size()
958 );
959 state.advance(&snapping, 0.2);
960 assert_eq!(state.size(), 0.8);
961 assert!(!state.is_settling());
962 assert!(!settle(&mut state, &snapping));
965 assert_eq!(state.size(), 0.8);
966 let mut eased = SheetState::new(&snapping);
968 eased.go(
969 &snapping,
970 &SheetTo::size(0.8).animate(0.3, Curve::Decelerate),
971 );
972 eased.advance(&snapping, 0.15);
973 assert!(
974 (eased.size() - (0.5 + 0.3 * 0.75)).abs() < 1e-4,
975 "{}",
976 eased.size()
977 );
978 let mut instant = SheetState::new(&snapping);
980 instant.go(&snapping, &SheetTo::size(0.8).animate(0.0, Curve::Linear));
981 assert_eq!(instant.size(), 0.8);
982 }
983
984 #[test]
988 fn a_finger_refuses_a_request_and_stops_one_under_way() {
989 let snapping = spec(true, false);
990 let mut state = SheetState::new(&snapping);
991 state.drag(&snapping, 0.1, 800.0);
992 assert!(
993 !state.go(&snapping, &SheetTo::size(1.0)),
994 "refused under a finger"
995 );
996 assert!((state.size() - 0.6).abs() < 1e-6);
997 state.release(&snapping, 800.0, 0.0, None);
998 assert!(state.is_settling(), "let go off a stop, it settles");
999 assert!(state.go(&snapping, &SheetTo::size(0.9).animate(0.5, Curve::Linear)));
1000 state.advance(&snapping, 0.25);
1001 let midway = state.size();
1002 assert!(
1003 (midway - 0.75).abs() < 1e-4,
1004 "the request's way, not the settle's: {midway}"
1005 );
1006 state.hold();
1007 state.advance(&snapping, 0.25);
1008 assert_eq!(state.size(), midway, "a finger stopped it where it was");
1009 assert!(!state.is_settling());
1010 }
1011
1012 #[test]
1016 fn a_request_to_nothing_puts_a_dismissible_sheet_away() {
1017 let closing = spec(true, true);
1018 let mut state = SheetState::new(&closing);
1019 let early = Curve::Interval {
1022 begin: 0.0,
1023 end: 0.5,
1024 inner: Box::new(Curve::Linear),
1025 };
1026 state.go(&closing, &SheetTo::size(0.0).animate(0.2, early));
1027 let (_, gone) = state.advance(&closing, 0.15);
1028 assert_eq!(state.size(), 0.0);
1029 assert!(!gone, "not while the request is under way");
1030 assert!(settle(&mut state, &closing), "put away once it is done");
1031 assert!(state.go(&closing, &SheetTo::initial()));
1032 assert_eq!(state.size(), 0.5);
1033 state.go(&closing, &SheetTo::size(0.0));
1034 assert!(settle(&mut state, &closing), "and may be put away again");
1035 }
1036
1037 fn settle(state: &mut SheetState, spec: &SheetSpec) -> bool {
1038 let mut dismissed = false;
1039 for _ in 0..240 {
1040 let (_, gone) = state.advance(spec, 1.0 / 60.0);
1041 dismissed |= gone;
1042 }
1043 dismissed
1044 }
1045
1046 #[test]
1049 fn a_released_sheet_arrives_on_a_stop() {
1050 let snapping = spec(true, false);
1051 let mut state = SheetState::new(&snapping);
1052 state.drag(&snapping, -0.2, 800.0); state.release(&snapping, 800.0, 0.0, None);
1054 assert!(state.is_settling());
1055 assert!(!settle(&mut state, &snapping));
1056 assert_eq!(state.size(), 0.25);
1057
1058 state.drag(&snapping, 0.05, 800.0); state.release(&snapping, 800.0, 300.0, None);
1060 settle(&mut state, &snapping);
1061 assert_eq!(state.size(), 0.5);
1062 assert!(!state.is_settling());
1063 }
1064
1065 #[test]
1068 fn a_flick_down_from_the_bottom_stop_dismisses_once() {
1069 let closing = spec(true, true);
1070 let mut state = SheetState::new(&closing);
1071 state.drag(&closing, -0.24, 800.0); state.release(&closing, 800.0, -1200.0, None);
1073 assert!(
1074 !settle(&mut state, &closing),
1075 "the next stop its way, not nothing"
1076 );
1077 assert_eq!(state.size(), 0.25);
1078 state.drag(&closing, -0.01, 800.0); state.release(&closing, 800.0, -1200.0, None);
1080 assert!(settle(&mut state, &closing), "dismissed");
1081 assert_eq!(state.size(), 0.0);
1082 assert!(!settle(&mut state, &closing), "and not twice");
1083
1084 let staying = spec(true, false);
1085 let mut state = SheetState::new(&staying);
1086 state.drag(&staying, -0.24, 800.0);
1087 state.release(&staying, 800.0, -1200.0, None);
1088 assert!(!settle(&mut state, &staying));
1089 assert_eq!(state.size(), 0.25);
1090 }
1091
1092 #[test]
1095 fn a_held_sheet_is_not_dismissed_under_the_finger() {
1096 let closing = spec(true, true);
1097 let mut state = SheetState::new(&closing);
1098 state.drag(&closing, -1.0, 800.0);
1099 assert_eq!(state.size(), 0.0);
1100 assert!(!settle(&mut state, &closing));
1101 }
1102
1103 #[test]
1106 fn a_coasting_sheet_goes_where_it_is_thrown() {
1107 let coasting = spec(false, false);
1108 let mut state = SheetState::new(&coasting);
1109 state.drag(&coasting, 0.0, 800.0);
1110 state.release(&coasting, 800.0, 0.0, None);
1111 assert!(!state.is_settling());
1112 assert_eq!(state.size(), 0.5);
1113 state.release(&coasting, 800.0, 600.0, None);
1114 settle(&mut state, &coasting);
1115 assert!(
1116 state.size() > 0.55 && state.size() < 1.0,
1117 "{}",
1118 state.size()
1119 );
1120 state.release(&coasting, 800.0, 8000.0, None);
1121 settle(&mut state, &coasting);
1122 assert_eq!(state.size(), 1.0, "and stops at its max");
1123 }
1124
1125 #[test]
1128 fn a_sheet_that_leaves_the_frame_forgets_its_height() {
1129 let snapping = spec(true, false);
1130 let id = WidgetId::ROOT.child(0);
1131 let mut states = HashMap::new();
1132 drag_into(&mut states, id, &snapping, 0.3, 800.0);
1133 let area = SheetArea {
1134 id,
1135 panel: Rect::new(0.0, 0.0, 10.0, 10.0),
1136 available: 800.0,
1137 spec: snapping.clone(),
1138 areas: Vec::new(),
1139 };
1140 advance_all(&mut states, std::slice::from_ref(&area), 0.016);
1141 assert_eq!(size_of(&states, id, &snapping), 0.8);
1142 advance_all(&mut states, &[], 0.016);
1143 assert_eq!(size_of(&states, id, &snapping), 0.5);
1144 }
1145
1146 #[test]
1149 fn the_panel_sits_along_the_bottom_at_the_retained_height() {
1150 let red = Color::rgb(1.0, 0.0, 0.0);
1151 let sheet =
1152 || DraggableScrollableSheet::<()>::new(Container::new().color(red)).snap_sizes([0.5]);
1153 let painted = |runtime: &Runtime| {
1154 let root = sheet();
1155 let ui = build_ui(&root, Size::new(400.0, 800.0), runtime, &Theme::default());
1156 ui.scene().primitives().iter().find_map(|p| match p {
1157 Primitive::Rect { rect, color, .. } if *color == red => Some(*rect),
1158 _ => None,
1159 })
1160 };
1161 let at_rest = painted(&Runtime::default()).expect("the content is painted");
1162 assert_eq!(
1163 (at_rest.y, at_rest.height, at_rest.width),
1164 (400.0, 400.0, 400.0)
1165 );
1166
1167 let mut runtime = Runtime::default();
1168 let panel = WidgetId::ROOT.child(0);
1169 drag_into(&mut runtime.sheets, panel, &sheet().spec, 0.25, 800.0);
1170 let raised = painted(&runtime).expect("the content is painted");
1171 assert_eq!((raised.y, raised.height), (200.0, 600.0));
1172 }
1173
1174 fn handed_over(state: &mut SheetState, spec: &SheetSpec) -> Option<(WidgetId, f32)> {
1176 let mut handed = None;
1177 for _ in 0..240 {
1178 state.advance(spec, 1.0 / 60.0);
1179 handed = handed.or(state.handover.take());
1180 }
1181 handed
1182 }
1183
1184 #[test]
1188 fn a_throw_that_reaches_full_height_goes_on_into_the_list() {
1189 let list = WidgetId::ROOT.child(7);
1190 let tolerance = Tolerance::PIXELS.velocity;
1191
1192 let snapping = spec(true, false);
1196 let mut state = SheetState::new(&snapping);
1197 state.drag(&snapping, 0.05, 800.0);
1198 state.release(&snapping, 800.0, 900.0, Some(list));
1199 let handed = handed_over(&mut state, &snapping);
1200 assert_eq!(state.size(), 1.0);
1201 assert_eq!(handed, Some((list, SHEET_SNAP_MIN_SPEED + tolerance)));
1202 assert!(
1203 !state.is_settling(),
1204 "the sheet is done; the list carries on"
1205 );
1206
1207 let coasting = spec(false, false);
1210 let mut state = SheetState::new(&coasting);
1211 state.release(&coasting, 800.0, 8000.0, Some(list));
1212 let (to, velocity) = handed_over(&mut state, &coasting).expect("handed over");
1213 assert_eq!((to, state.size()), (list, 1.0));
1214 assert!(
1215 velocity > tolerance && velocity < 8000.0 + tolerance,
1216 "{velocity}"
1217 );
1218 }
1219
1220 #[test]
1224 fn nothing_is_handed_over_short_of_a_throw_to_the_top() {
1225 let list = WidgetId::ROOT.child(7);
1226 let snapping = spec(true, true);
1227 let mut state = SheetState::new(&snapping);
1228
1229 state.drag(&snapping, -0.24, 800.0); state.release(&snapping, 800.0, 300.0, Some(list));
1231 assert_eq!(handed_over(&mut state, &snapping), None);
1232 assert_eq!(state.size(), 0.5);
1233
1234 state.drag(&snapping, 0.45, 800.0); state.release(&snapping, 800.0, -300.0, Some(list));
1236 assert_eq!(handed_over(&mut state, &snapping), None);
1237 assert_eq!(state.size(), 0.5);
1238
1239 state.drag(&snapping, 0.05, 800.0); state.release(&snapping, 800.0, 900.0, None);
1241 assert_eq!(handed_over(&mut state, &snapping), None);
1242 assert_eq!(state.size(), 1.0);
1243
1244 state.drag(&snapping, 0.0, 800.0); state.release(&snapping, 800.0, 0.0, Some(list));
1246 assert_eq!(handed_over(&mut state, &snapping), None);
1247
1248 state.drag(&snapping, 0.0, 800.0);
1251 state.release(&snapping, 800.0, 900.0, Some(list));
1252 assert_eq!(handed_over(&mut state, &snapping), None);
1253
1254 state.drag(&snapping, -0.05, 800.0); state.release(&snapping, 800.0, 0.0, Some(list));
1256 assert_eq!(handed_over(&mut state, &snapping), None);
1257 assert_eq!(state.size(), 1.0);
1258 }
1259
1260 #[test]
1264 fn a_panel_lowered_to_nothing_keeps_its_list() {
1265 let sheet = || {
1266 DraggableScrollableSheet::<()>::new(
1267 crate::Flex::column().flex(1.0).child(
1268 crate::SingleChildScrollView::new()
1269 .flex(1.0)
1270 .child(Container::new().height(2000.0)),
1271 ),
1272 )
1273 .min(0.25)
1274 .on_dismiss(())
1275 };
1276 let holding = |runtime: &Runtime| {
1277 let root = sheet();
1278 let ui = build_ui(&root, Size::new(400.0, 800.0), runtime, &Theme::default());
1279 let area = ui
1280 .sheets()
1281 .first()
1282 .cloned()
1283 .expect("the sheet is in the frame");
1284 let held = area
1285 .areas
1286 .first()
1287 .and_then(|list| ui.sheet_holding(*list))
1288 .map(|sheet| sheet.id);
1289 (area.areas.len(), held == Some(area.id))
1290 };
1291 let mut runtime = Runtime::default();
1292 let panel = WidgetId::ROOT.child(0);
1293 assert_eq!(
1294 holding(&runtime),
1295 (1, true),
1296 "open, its list shares the finger"
1297 );
1298 drag_into(&mut runtime.sheets, panel, &sheet().spec, -0.5, 800.0);
1299 assert_eq!(runtime.sheets[&panel].size(), 0.0);
1300 assert_eq!(holding(&runtime), (1, true), "at nothing, still");
1301 }
1302}