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