repose_material/material3/
swipe.rs1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4
5use repose_core::animation::AnimationSpec;
6use repose_core::*;
7use repose_ui::{Box, Column, ViewExt};
8
9use super::*;
10
11#[derive(Clone, Debug)]
13pub struct SwipeToDismissConfig {
14 pub modifier: Modifier,
15 pub dismiss_threshold: f32,
16 pub dismissed_offset: f32,
17 pub animation_spec: AnimationSpec,
18 pub gestures_enabled: bool,
19 pub enable_dismiss_from_start_to_end: bool,
20 pub enable_dismiss_from_end_to_start: bool,
21}
22
23impl Default for SwipeToDismissConfig {
24 fn default() -> Self {
25 Self {
26 modifier: Modifier::new(),
27 dismiss_threshold: SwipeToDismissDefaults::DISMISS_THRESHOLD,
28 dismissed_offset: SwipeToDismissDefaults::DISMISSED_OFFSET,
29 animation_spec: AnimationSpec::spring_gentle(),
30 gestures_enabled: true,
31 enable_dismiss_from_start_to_end: true,
32 enable_dismiss_from_end_to_start: true,
33 }
34 }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq)]
39pub enum DismissDirection {
40 StartToEnd,
41 EndToStart,
42 Both,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq)]
47pub enum DismissValue {
48 Default,
49 DismissedToStart,
50 DismissedToEnd,
51}
52
53pub struct SwipeToDismissState {
55 swipeable: repose_core::SwipeableState<DismissValue>,
56}
57
58impl Default for SwipeToDismissState {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl SwipeToDismissState {
65 pub fn new() -> Self {
66 Self::with_config(SwipeToDismissConfig::default())
67 }
68
69 pub fn with_config(config: SwipeToDismissConfig) -> Self {
70 let one_third = 1.0 / 3.0;
71 let positional_threshold = (config.dismiss_threshold * one_third) / config.dismissed_offset;
72 let mut anchors = vec![(0.0, DismissValue::Default)];
73 if config.enable_dismiss_from_end_to_start {
74 anchors.push((-config.dismissed_offset, DismissValue::DismissedToStart));
75 }
76 if config.enable_dismiss_from_start_to_end {
77 anchors.push((config.dismissed_offset, DismissValue::DismissedToEnd));
78 }
79 anchors.sort_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap());
81 let swipeable = repose_core::SwipeableState::new(
82 anchors,
83 repose_core::SwipeableConfig {
84 animation_spec: config.animation_spec,
85 positional_threshold,
86 ..Default::default()
87 },
88 );
89 swipeable.snap_to(0.0);
91 Self { swipeable }
92 }
93
94 pub fn offset(&self) -> f32 {
96 self.swipeable.offset()
97 }
98
99 pub fn set_offset_instant(&self, off: f32) {
101 self.swipeable.snap_to(off);
102 }
103
104 pub fn is_dismissed(&self) -> bool {
106 self.swipeable.current_value() != DismissValue::Default
107 }
108
109 pub fn dismiss(&self) {
111 self.swipeable.animate_to(&DismissValue::DismissedToStart);
112 }
113
114 pub fn dismiss_to(&self, offset: f32) {
116 let value = if offset < 0.0 {
117 DismissValue::DismissedToStart
118 } else {
119 DismissValue::DismissedToEnd
120 };
121 self.swipeable.animate_to(&value);
122 }
123
124 pub fn reset(&self) {
126 self.swipeable.animate_to(&DismissValue::Default);
127 }
128
129 fn try_handle_dismiss_with_threshold(
131 &self,
132 on_dismiss: &Option<Rc<dyn Fn()>>,
133 _threshold: f32,
134 ) {
135 if !self.swipeable.is_animating() {
136 let val = self.swipeable.current_value();
137 if val != DismissValue::Default
138 && let Some(cb) = on_dismiss
139 {
140 cb();
141 }
142 }
143 }
144}
145
146pub fn SwipeToDismiss(
153 state: Rc<SwipeToDismissState>,
154 on_dismiss: Option<Rc<dyn Fn()>>,
155 background: View,
156 content: View,
157 modifier: Modifier,
158 config: SwipeToDismissConfig,
159) -> View {
160 let offset = state.offset();
161 state.try_handle_dismiss_with_threshold(&on_dismiss, config.dismiss_threshold);
162
163 let s1 = state.swipeable.clone();
164 let s2 = state.swipeable.clone();
165 let s3 = state.swipeable.clone();
166 let on_down = { move |e: PointerEvent| s1.on_pointer_down(e.position.x) };
167 let on_move = { move |e: PointerEvent| s2.on_pointer_move(e.position.x) };
168 let on_up = { move |_e: PointerEvent| s3.on_pointer_up() };
169
170 let display_offset = offset
171 .max(-config.dismissed_offset)
172 .min(config.dismissed_offset);
173
174 let content_modifier = {
175 let mut m = Modifier::new()
176 .fill_max_width()
177 .translate(display_offset, 0.0);
178 if config.gestures_enabled {
179 m = m
180 .on_pointer_down(on_down)
181 .on_pointer_move(on_move)
182 .on_pointer_up(on_up);
183 }
184 m
185 };
186
187 Column(modifier.fill_max_width()).child((
188 Box(Modifier::new().fill_max_size().absolute()).child(background),
189 Box(content_modifier).child(content),
190 ))
191}