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