1use std::{
23 cell::{Cell, RefCell},
24 rc::Rc,
25 sync::atomic::{AtomicU64, Ordering},
26};
27
28use cranpose_animation::{Animatable, AnimationType, Spring, spring};
29use cranpose_core::{
30 NodeId, Owned, OwnedMutableState, RuntimeHandle, internal::FrameCallbackRegistration,
31 with_current_composer,
32};
33use cranpose_foundation::DRAG_THRESHOLD;
34use cranpose_ui_layout::{Measurable, MeasurePolicy, MeasureResult, MeasureScope, Placement};
35
36use crate::{
37 composable,
38 layout::policies::BoxMeasurePolicy,
39 modifier::{GraphicsLayer, Modifier, PointerEvent, PointerEventKind},
40 subcompose_layout::Constraints,
41 widgets::{
42 box_widget::{Box, BoxSpec},
43 layout::Layout,
44 },
45};
46
47const DISMISS_SETTLE_EPSILON: f32 = 0.5;
49
50const COLLAPSE_SETTLE_EPSILON: f32 = 0.01;
53
54fn swipe_spring() -> AnimationType {
56 spring(Spring::DampingRatioNoBouncy, Spring::StiffnessMediumLow)
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum SwipeDismissSide {
65 Start,
68 End,
71}
72
73#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
75pub enum SwipeDismissDirection {
76 #[default]
77 Both,
78 StartToEnd,
79 EndToStart,
80}
81
82type BackgroundFn = Rc<RefCell<dyn FnMut(SwipeDismissSide)>>;
85
86#[derive(Clone)]
88pub struct SwipeToDismissSpec {
89 pub threshold_fraction: f32,
92 background: Option<BackgroundFn>,
93 pub key: Option<u64>,
105 pub state: Option<SwipeDismissState>,
112 pub direction: SwipeDismissDirection,
113 pub edge_width: Option<f32>,
114 pub collapse_after_dismiss: bool,
115 pub reset_after_dismiss: bool,
129 pub enabled: bool,
130}
131
132impl SwipeToDismissSpec {
133 pub fn new() -> Self {
134 Self {
135 threshold_fraction: 0.5,
136 background: None,
137 key: None,
138 state: None,
139 direction: SwipeDismissDirection::Both,
140 edge_width: None,
141 collapse_after_dismiss: true,
142 reset_after_dismiss: false,
143 enabled: true,
144 }
145 }
146
147 pub fn with_key(mut self, key: u64) -> Self {
153 self.key = Some(key);
154 self
155 }
156
157 pub fn with_state(mut self, state: SwipeDismissState) -> Self {
160 self.state = Some(state);
161 self
162 }
163
164 pub fn with_threshold_fraction(mut self, fraction: f32) -> Self {
166 self.threshold_fraction = fraction;
167 self
168 }
169
170 pub fn with_background(mut self, background: impl FnMut(SwipeDismissSide) + 'static) -> Self {
174 self.background = Some(Rc::new(RefCell::new(background)));
175 self
176 }
177
178 pub fn with_direction(mut self, direction: SwipeDismissDirection) -> Self {
179 self.direction = direction;
180 self
181 }
182
183 pub fn from_edge(mut self, width: f32) -> Self {
184 self.edge_width = Some(width.max(0.0));
185 self
186 }
187
188 pub fn with_reset_after_dismiss(mut self, reset: bool) -> Self {
189 self.reset_after_dismiss = reset;
190 self
191 }
192
193 pub fn with_collapse_after_dismiss(mut self, collapse: bool) -> Self {
194 self.collapse_after_dismiss = collapse;
195 self
196 }
197
198 pub fn with_enabled(mut self, enabled: bool) -> Self {
199 self.enabled = enabled;
200 self
201 }
202}
203
204impl Default for SwipeToDismissSpec {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210#[derive(Clone, Copy, Debug, PartialEq)]
212enum SwipePhase {
213 Idle,
215 Tracking {
217 down_x: f32,
218 down_y: f32,
219 start_offset: f32,
220 },
221 Dragging { down_x: f32, start_offset: f32 },
223 LockedOut,
225}
226
227#[derive(Clone, Copy, Debug, PartialEq, Eq)]
231pub(crate) enum SwipeAxisDecision {
232 Undecided,
233 Horizontal,
234 Vertical,
235}
236
237pub(crate) fn decide_axis(total_dx: f32, total_dy: f32, slop: f32) -> SwipeAxisDecision {
239 let horizontal = total_dx.abs();
240 let vertical = total_dy.abs();
241 if horizontal > slop && horizontal >= vertical {
242 SwipeAxisDecision::Horizontal
243 } else if vertical > slop && vertical > horizontal {
244 SwipeAxisDecision::Vertical
245 } else {
246 SwipeAxisDecision::Undecided
247 }
248}
249
250pub(crate) fn dismissal_target(offset: f32, width: f32, threshold_fraction: f32) -> Option<f32> {
253 if !width.is_finite() || width <= 0.0 {
254 return None;
255 }
256 let threshold = width * threshold_fraction.clamp(f32::EPSILON, 1.0);
257 (offset.abs() >= threshold).then(|| width * offset.signum())
258}
259
260pub(crate) fn clamp_offset(offset: f32, width: f32) -> f32 {
263 if width.is_finite() && width > 0.0 {
264 offset.clamp(-width, width)
265 } else {
266 offset
267 }
268}
269
270static NEXT_SWIPE_ID: AtomicU64 = AtomicU64::new(0);
271
272struct SwipeToDismissController {
275 id: u64,
276 runtime: RuntimeHandle,
277 offset: RefCell<Animatable<f32>>,
278 revealed: OwnedMutableState<bool>,
279 collapse: RefCell<Animatable<f32>>,
280 phase: Cell<SwipePhase>,
281 width_px: Cell<f32>,
282 threshold_fraction: Cell<f32>,
283 on_dismiss: RefCell<Option<Rc<dyn Fn()>>>,
284 dismissed: OwnedMutableState<bool>,
285 node_id: Cell<Option<NodeId>>,
286 settle_watcher: RefCell<Option<FrameCallbackRegistration>>,
287 collapse_watcher: RefCell<Option<FrameCallbackRegistration>>,
288 identity: Cell<Option<u64>>,
289 active_pointer: Cell<Option<u64>>,
290 direction: Cell<SwipeDismissDirection>,
291 edge_width: Cell<Option<f32>>,
292 collapse_after_dismiss: Cell<bool>,
293 reset_after_dismiss: Cell<bool>,
294 enabled: Cell<bool>,
295}
296
297impl SwipeToDismissController {
298 fn new(runtime: RuntimeHandle) -> Rc<Self> {
299 Rc::new(Self {
300 id: NEXT_SWIPE_ID.fetch_add(1, Ordering::Relaxed),
301 offset: RefCell::new(Animatable::new(0.0, runtime.clone())),
302 revealed: OwnedMutableState::with_runtime(false, runtime.clone()),
303 collapse: RefCell::new(Animatable::new(1.0, runtime.clone())),
304 dismissed: OwnedMutableState::with_runtime(false, runtime.clone()),
305 runtime,
306 phase: Cell::new(SwipePhase::Idle),
307 width_px: Cell::new(f32::NAN),
308 threshold_fraction: Cell::new(0.5),
309 on_dismiss: RefCell::new(None),
310 node_id: Cell::new(None),
311 settle_watcher: RefCell::new(None),
312 collapse_watcher: RefCell::new(None),
313 identity: Cell::new(None),
314 active_pointer: Cell::new(None),
315 direction: Cell::new(SwipeDismissDirection::Both),
316 edge_width: Cell::new(None),
317 collapse_after_dismiss: Cell::new(true),
318 reset_after_dismiss: Cell::new(false),
319 enabled: Cell::new(true),
320 })
321 }
322
323 fn reset_to_rest(&self) {
324 self.settle_watcher.borrow_mut().take();
325 self.collapse_watcher.borrow_mut().take();
326 self.offset.borrow_mut().snapTo(0.0);
327 self.collapse.borrow_mut().snapTo(1.0);
328 self.phase.set(SwipePhase::Idle);
329 self.active_pointer.set(None);
330 self.set_dismissed(false);
331 self.set_revealed(false);
332 }
333
334 fn current_offset(&self) -> f32 {
335 self.offset.borrow().state().value()
336 }
337
338 fn revealed_side(&self) -> SwipeDismissSide {
339 if self.current_offset() >= 0.0 {
340 SwipeDismissSide::Start
341 } else {
342 SwipeDismissSide::End
343 }
344 }
345
346 fn collapse_fraction(&self) -> f32 {
347 self.collapse.borrow().state().value()
348 }
349
350 fn revealed(&self) -> bool {
351 self.revealed.value()
352 }
353
354 fn set_revealed(&self, revealed: bool) {
355 if self.revealed.get_non_reactive() != revealed {
356 self.revealed.set_value(revealed);
357 }
358 }
359
360 fn set_dismissed(&self, dismissed: bool) {
361 if self.dismissed.get_non_reactive() != dismissed {
362 self.dismissed.set_value(dismissed);
363 }
364 }
365
366 fn snap_to(&self, offset: f32) {
367 self.offset.borrow_mut().snapTo(offset);
368 self.set_revealed(offset != 0.0);
369 }
370
371 fn animate_to(&self, target: f32) {
372 self.offset.borrow_mut().animateTo(target, swipe_spring());
373 if target != 0.0 {
374 self.set_revealed(true);
375 }
376 }
377}
378
379#[derive(Clone)]
389pub struct SwipeDismissState {
390 controller: Rc<SwipeToDismissController>,
391}
392
393impl PartialEq for SwipeDismissState {
394 fn eq(&self, other: &Self) -> bool {
395 Rc::ptr_eq(&self.controller, &other.controller)
396 }
397}
398
399impl SwipeDismissState {
400 fn new(runtime: RuntimeHandle) -> Self {
401 Self {
402 controller: SwipeToDismissController::new(runtime),
403 }
404 }
405
406 pub fn offset(&self) -> f32 {
409 self.controller.current_offset()
410 }
411
412 pub fn progress(&self) -> f32 {
418 let width = self.controller.width_px.get();
419 if !width.is_finite() || width <= 0.0 {
420 return 0.0;
421 }
422 let threshold = width * self.controller.threshold_fraction.get();
423 if threshold <= 0.0 {
424 return 0.0;
425 }
426 (self.offset().abs() / threshold).clamp(0.0, 1.0)
427 }
428
429 pub fn side(&self) -> Option<SwipeDismissSide> {
432 (self.offset() != 0.0).then(|| self.controller.revealed_side())
433 }
434
435 pub fn is_displaced(&self) -> bool {
438 self.controller.revealed()
439 }
440
441 pub fn is_dismissed(&self) -> bool {
443 self.controller.dismissed.value()
444 }
445
446 pub fn reset(&self) {
449 self.controller.reset_to_rest();
450 }
451}
452
453#[expect(non_snake_case)]
459#[track_caller]
460pub fn rememberSwipeDismissState() -> SwipeDismissState {
461 let caller = cranpose_core::caller_location_key();
462 let state = with_current_composer(|composer| {
463 let runtime = composer.runtime_handle();
464 let owned: Owned<SwipeDismissState> =
465 composer.remember_at(caller, || SwipeDismissState::new(runtime));
466 owned.with(SwipeDismissState::clone)
467 });
468 let identity = crate::lazy_item::lazy_item_key();
469 if state.controller.identity.get() != identity {
470 state.controller.reset_to_rest();
471 state.controller.identity.set(identity);
472 }
473 state
474}
475
476fn swipe_gesture_modifier(base: Modifier, controller: Rc<SwipeToDismissController>) -> Modifier {
480 let key = controller.id;
481 base.pointer_input(key, move |scope| {
482 let controller = Rc::clone(&controller);
483 async move {
484 scope
485 .await_pointer_event_scope(|await_scope| async move {
486 loop {
487 let event = await_scope.await_pointer_event().await;
488 handle_swipe_event(&controller, &event);
489 }
490 })
491 .await;
492 }
493 })
494}
495
496fn handle_swipe_event(controller: &Rc<SwipeToDismissController>, event: &PointerEvent) {
499 if event.kind != PointerEventKind::Down
500 && event.kind != PointerEventKind::Cancel
501 && controller.active_pointer.get() != Some(event.id)
502 {
503 return;
504 }
505
506 match event.kind {
507 PointerEventKind::Down => {
508 if event.is_consumed()
509 || !controller.enabled.get()
510 || controller.active_pointer.get().is_some()
511 {
512 return;
513 }
514 let width = controller.width_px.get();
515 let inside_edge = match (controller.edge_width.get(), controller.direction.get()) {
516 (None, _) => true,
517 (Some(edge), SwipeDismissDirection::StartToEnd) => event.global_position.x <= edge,
518 (Some(edge), SwipeDismissDirection::EndToStart) => {
519 width.is_finite() && event.global_position.x >= width - edge
520 }
521 (Some(edge), SwipeDismissDirection::Both) => {
522 event.global_position.x <= edge
523 || (width.is_finite() && event.global_position.x >= width - edge)
524 }
525 };
526 if !inside_edge {
527 return;
528 }
529 controller.active_pointer.set(Some(event.id));
530 let current = controller.current_offset();
531 controller.snap_to(current);
532 controller.phase.set(SwipePhase::Tracking {
533 down_x: event.global_position.x,
534 down_y: event.global_position.y,
535 start_offset: current,
536 });
537 }
538 PointerEventKind::Move => {
539 if event.is_consumed() {
540 if matches!(controller.phase.get(), SwipePhase::Dragging { .. }) {
541 animate_spring_back(controller);
542 }
543 controller.phase.set(SwipePhase::Idle);
544 return;
545 }
546 match controller.phase.get() {
547 SwipePhase::Tracking {
548 down_x,
549 down_y,
550 start_offset,
551 } => {
552 let total_dx = event.global_position.x - down_x;
553 let total_dy = event.global_position.y - down_y;
554 match decide_axis(total_dx, total_dy, DRAG_THRESHOLD) {
555 SwipeAxisDecision::Horizontal => {
556 controller.phase.set(SwipePhase::Dragging {
557 down_x,
558 start_offset,
559 });
560 let width = controller.width_px.get();
561 controller.snap_to(constrain_direction(
562 clamp_offset(start_offset + total_dx, width),
563 controller.direction.get(),
564 ));
565 event.consume();
566 }
567 SwipeAxisDecision::Vertical => {
568 controller.phase.set(SwipePhase::LockedOut);
569 }
570 SwipeAxisDecision::Undecided => {}
571 }
572 }
573 SwipePhase::Dragging {
574 down_x,
575 start_offset,
576 } => {
577 let total_dx = event.global_position.x - down_x;
578 let width = controller.width_px.get();
579 controller.snap_to(constrain_direction(
580 clamp_offset(start_offset + total_dx, width),
581 controller.direction.get(),
582 ));
583 event.consume();
584 }
585 SwipePhase::Idle | SwipePhase::LockedOut => {}
586 }
587 }
588 PointerEventKind::Up => {
589 let phase = controller.phase.get();
590 controller.phase.set(SwipePhase::Idle);
591 if let SwipePhase::Dragging { .. } = phase {
592 settle_release(controller);
593 event.consume();
594 }
595 controller.active_pointer.set(None);
596 }
597 PointerEventKind::Cancel => {
598 if matches!(controller.phase.get(), SwipePhase::Dragging { .. }) {
599 animate_spring_back(controller);
600 }
601 controller.phase.set(SwipePhase::Idle);
602 controller.active_pointer.set(None);
603 }
604 PointerEventKind::Scroll
605 | PointerEventKind::Zoom
606 | PointerEventKind::RotaryScrollPre
607 | PointerEventKind::RotaryScroll
608 | PointerEventKind::Enter
609 | PointerEventKind::Exit => {}
610 }
611}
612
613fn constrain_direction(offset: f32, direction: SwipeDismissDirection) -> f32 {
614 match direction {
615 SwipeDismissDirection::Both => offset,
616 SwipeDismissDirection::StartToEnd => offset.max(0.0),
617 SwipeDismissDirection::EndToStart => offset.min(0.0),
618 }
619}
620
621fn settle_release(controller: &Rc<SwipeToDismissController>) {
624 let offset = controller.current_offset();
625 let width = controller.width_px.get();
626 match dismissal_target(offset, width, controller.threshold_fraction.get()) {
627 Some(target) => animate_dismiss(controller, target),
628 None => animate_spring_back(controller),
629 }
630}
631
632fn animate_dismiss(controller: &Rc<SwipeToDismissController>, target: f32) {
634 controller.animate_to(target);
635 watch_settle(controller, true);
636}
637
638fn animate_spring_back(controller: &Rc<SwipeToDismissController>) {
641 controller.animate_to(0.0);
642 watch_settle(controller, false);
643}
644
645fn watch_settle(controller: &Rc<SwipeToDismissController>, dismissing: bool) {
650 let weak = Rc::downgrade(controller);
651 let registration =
652 controller
653 .runtime
654 .frame_clock()
655 .with_frame_nanos(move |_frame_time_nanos| {
656 let Some(controller) = weak.upgrade() else {
657 return;
658 };
659 controller.settle_watcher.borrow_mut().take();
660 if dismissing && controller.dismissed.get_non_reactive() {
661 return;
662 }
663 if matches!(controller.phase.get(), SwipePhase::Dragging { .. }) {
664 return;
665 }
666 let target = controller.offset.borrow().target();
667 let value = controller.current_offset();
668 if (value - target).abs() <= DISMISS_SETTLE_EPSILON {
669 controller.set_revealed(false);
670 if dismissing && !controller.dismissed.get_non_reactive() {
671 controller.set_dismissed(true);
672 if controller.collapse_after_dismiss.get() {
673 start_collapse(&controller);
674 }
675 let on_dismiss = controller.on_dismiss.borrow().clone();
676 if let Some(on_dismiss) = on_dismiss {
677 on_dismiss();
678 }
679 if controller.reset_after_dismiss.get() {
680 controller.reset_to_rest();
681 }
682 }
683 } else {
684 watch_settle(&controller, dismissing);
685 }
686 });
687 *controller.settle_watcher.borrow_mut() = Some(registration);
688}
689
690fn start_collapse(controller: &Rc<SwipeToDismissController>) {
693 controller
694 .collapse
695 .borrow_mut()
696 .animateTo(0.0, swipe_spring());
697 watch_collapse(controller);
698}
699
700fn watch_collapse(controller: &Rc<SwipeToDismissController>) {
705 let weak = Rc::downgrade(controller);
706 let registration =
707 controller
708 .runtime
709 .frame_clock()
710 .with_frame_nanos(move |_frame_time_nanos| {
711 let Some(controller) = weak.upgrade() else {
712 return;
713 };
714 controller.collapse_watcher.borrow_mut().take();
715 if let Some(node_id) = controller.node_id.get() {
716 crate::schedule_measure_repass(node_id);
717 }
718 crate::request_render_invalidation();
719 if controller.collapse_fraction() > COLLAPSE_SETTLE_EPSILON {
720 watch_collapse(&controller);
721 }
722 });
723 *controller.collapse_watcher.borrow_mut() = Some(registration);
724}
725
726#[derive(Clone, Copy, PartialEq)]
727enum SwipeLayoutPhase {
728 Row,
729 Collapse,
730}
731
732#[derive(Clone)]
733struct SwipeMeasurePolicy {
734 controller: Rc<SwipeToDismissController>,
735 phase: SwipeLayoutPhase,
736}
737
738impl PartialEq for SwipeMeasurePolicy {
739 fn eq(&self, other: &Self) -> bool {
740 self.phase == other.phase && Rc::ptr_eq(&self.controller, &other.controller)
741 }
742}
743
744impl MeasurePolicy for SwipeMeasurePolicy {
745 fn measure(
746 &self,
747 scope: &dyn MeasureScope,
748 measurables: &[Box<dyn Measurable>],
749 constraints: Constraints,
750 ) -> MeasureResult {
751 if self.phase == SwipeLayoutPhase::Row {
752 self.controller.width_px.set(constraints.max_width);
753 return BoxMeasurePolicy::new(crate::Alignment::TOP_START, false).measure(
754 scope,
755 measurables,
756 constraints,
757 );
758 }
759 let child_constraints = Constraints {
760 min_height: 0.0,
761 ..constraints
762 };
763 let mut placements = Vec::with_capacity(measurables.len());
764 let mut width = 0.0_f32;
765 let mut natural_height = 0.0_f32;
766 for measurable in measurables {
767 let placeable = measurable.measure(child_constraints);
768 width = width.max(placeable.width());
769 natural_height = natural_height.max(placeable.height());
770 placeable.place(0.0, 0.0);
771 placements.push(Placement::new(placeable.node_id(), 0.0, 0.0, 0));
772 }
773 let width = width.clamp(constraints.min_width, constraints.max_width);
774 let height = (natural_height * self.controller.collapse_fraction().clamp(0.0, 1.0))
775 .clamp(0.0, constraints.max_height);
776 MeasureResult::new(crate::modifier::Size { width, height }, placements)
777 }
778
779 fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
780 BoxMeasurePolicy::new(crate::Alignment::TOP_START, false)
781 .min_intrinsic_width(measurables, height)
782 }
783
784 fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
785 BoxMeasurePolicy::new(crate::Alignment::TOP_START, false)
786 .max_intrinsic_width(measurables, height)
787 }
788
789 fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
790 BoxMeasurePolicy::new(crate::Alignment::TOP_START, false)
791 .min_intrinsic_height(measurables, width)
792 }
793
794 fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
795 BoxMeasurePolicy::new(crate::Alignment::TOP_START, false)
796 .max_intrinsic_height(measurables, width)
797 }
798}
799
800#[composable(no_skip)]
813pub fn SwipeToDismiss<D, F>(
814 modifier: Modifier,
815 spec: SwipeToDismissSpec,
816 on_dismiss: D,
817 content: F,
818) -> cranpose_core::NodeId
819where
820 D: Fn() + 'static,
821 F: FnMut() + 'static,
822{
823 let owned_controller: Rc<SwipeToDismissController> = with_current_composer(|composer| {
824 let runtime = composer.runtime_handle();
825 let owned: Owned<Rc<SwipeToDismissController>> =
826 composer.remember(|| SwipeToDismissController::new(runtime));
827 owned.with(Rc::clone)
828 });
829 let controller = match &spec.state {
830 Some(state) => Rc::clone(&state.controller),
831 None => owned_controller,
832 };
833
834 let identity = spec.key.or_else(crate::lazy_item::lazy_item_key);
835 if controller.identity.get() != identity {
836 controller.reset_to_rest();
837 controller.identity.set(identity);
838 }
839
840 controller
841 .threshold_fraction
842 .set(spec.threshold_fraction.clamp(f32::EPSILON, 1.0));
843 controller.direction.set(spec.direction);
844 controller.edge_width.set(spec.edge_width);
845 controller
846 .collapse_after_dismiss
847 .set(spec.collapse_after_dismiss);
848 controller.reset_after_dismiss.set(spec.reset_after_dismiss);
849 controller.enabled.set(spec.enabled);
850 *controller.on_dismiss.borrow_mut() = Some(Rc::new(on_dismiss));
851
852 let background = spec.background.clone();
853 let content = Rc::new(RefCell::new(content));
854
855 let gesture_modifier = swipe_gesture_modifier(modifier, Rc::clone(&controller))
856 .semantics(dismiss_semantics(Rc::clone(&controller), spec.direction));
857
858 let controller_for_layout = Rc::clone(&controller);
859 let node = Layout(
860 Modifier::empty(),
861 SwipeMeasurePolicy {
862 phase: SwipeLayoutPhase::Collapse,
863 controller: Rc::clone(&controller_for_layout),
864 },
865 move || {
866 let background = background.clone();
867 let content = Rc::clone(&content);
868 let controller_for_row = Rc::clone(&controller_for_layout);
869 let gesture_modifier = gesture_modifier.clone();
870 Layout(
871 gesture_modifier,
872 SwipeMeasurePolicy {
873 phase: SwipeLayoutPhase::Row,
874 controller: Rc::clone(&controller_for_row),
875 },
876 move || {
877 if controller_for_row.revealed() {
878 if let Some(background) = &background {
879 let background = Rc::clone(background);
880 let side = controller_for_row.revealed_side();
881 Box(Modifier::empty(), BoxSpec::new(), move || {
882 (background.borrow_mut())(side);
883 });
884 }
885 }
886 let content = Rc::clone(&content);
887 let controller_for_layer = Rc::clone(&controller_for_row);
888 Box(
889 Modifier::empty().graphics_layer(move || GraphicsLayer {
890 translation_x: controller_for_layer.current_offset(),
891 ..GraphicsLayer::default()
892 }),
893 BoxSpec::new(),
894 move || {
895 (content.borrow_mut())();
896 },
897 );
898 },
899 );
900 },
901 );
902 controller.node_id.set(Some(node));
903 node
904}
905
906#[composable]
909pub fn SwipeToDismissBox<D, F>(modifier: Modifier, on_dismiss: D, content: F) -> NodeId
910where
911 D: Fn() + 'static,
912 F: FnMut() + 'static,
913{
914 SwipeToDismiss(
915 modifier,
916 SwipeToDismissSpec::new()
917 .with_threshold_fraction(0.35)
918 .with_direction(SwipeDismissDirection::StartToEnd)
919 .from_edge(32.0)
920 .with_collapse_after_dismiss(false)
921 .with_reset_after_dismiss(true),
922 on_dismiss,
923 content,
924 )
925}
926
927#[cfg(test)]
928#[path = "../tests/swipe_to_dismiss_tests.rs"]
929mod tests;
930
931fn dismiss_semantics(
938 controller: Rc<SwipeToDismissController>,
939 direction: SwipeDismissDirection,
940) -> impl Fn(&mut cranpose_foundation::SemanticsConfiguration) + 'static {
941 move |config| {
942 if !controller.enabled.get() {
943 return;
944 }
945 let fling = fling_away(Rc::clone(&controller), direction);
946 config
947 .custom_actions
948 .push(cranpose_foundation::SemanticsCustomAction::new(
949 "Dismiss",
950 {
951 let fling = fling.clone();
952 move || {
953 fling();
954 }
955 },
956 ));
957 config.dismiss = Some(cranpose_foundation::SemanticsDismiss::new(move || fling()));
958 }
959}
960
961fn fling_away(
964 controller: Rc<SwipeToDismissController>,
965 direction: SwipeDismissDirection,
966) -> Rc<dyn Fn() -> bool> {
967 Rc::new(move || {
968 let width = controller.width_px.get();
969 let target = match direction {
970 SwipeDismissDirection::EndToStart => -width,
971 SwipeDismissDirection::StartToEnd | SwipeDismissDirection::Both => width,
972 };
973 animate_dismiss(&controller, target);
974 true
975 })
976}