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 dismissed_offset: f32,
57}
58
59impl Default for SwipeToDismissState {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl SwipeToDismissState {
66 pub fn new() -> Self {
67 Self::with_config(SwipeToDismissConfig::default())
68 }
69
70 pub fn with_config(config: SwipeToDismissConfig) -> Self {
71 let one_third = 1.0 / 3.0;
72 let positional_threshold = (config.dismiss_threshold * one_third) / config.dismissed_offset;
73 let mut anchors = vec![(0.0, DismissValue::Default)];
74 if config.enable_dismiss_from_end_to_start {
75 anchors.push((-config.dismissed_offset, DismissValue::DismissedToStart));
76 }
77 if config.enable_dismiss_from_start_to_end {
78 anchors.push((config.dismissed_offset, DismissValue::DismissedToEnd));
79 }
80 anchors.sort_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap());
82 let swipeable = repose_core::SwipeableState::new(
83 anchors,
84 repose_core::SwipeableConfig {
85 animation_spec: config.animation_spec,
86 positional_threshold,
87 ..Default::default()
88 },
89 );
90 swipeable.snap_to(0.0);
92 Self {
93 swipeable,
94 dismissed_offset: config.dismissed_offset,
95 }
96 }
97
98 pub fn offset(&self) -> f32 {
100 self.swipeable.offset()
101 }
102
103 pub fn set_offset_instant(&self, off: f32) {
105 self.swipeable.snap_to(off);
106 }
107
108 pub fn is_dismissed(&self) -> bool {
110 self.swipeable.current_value() != DismissValue::Default
111 }
112
113 pub fn dismiss(&self) {
115 self.swipeable.animate_to(&DismissValue::DismissedToStart);
116 }
117
118 pub fn dismiss_to(&self, offset: f32) {
120 let value = if offset < 0.0 {
121 DismissValue::DismissedToStart
122 } else {
123 DismissValue::DismissedToEnd
124 };
125 self.swipeable.animate_to(&value);
126 }
127
128 pub fn reset(&self) {
130 self.swipeable.animate_to(&DismissValue::Default);
131 }
132
133 fn try_handle_dismiss_with_threshold(
135 &self,
136 on_dismiss: &Option<Rc<dyn Fn()>>,
137 _threshold: f32,
138 ) {
139 if !self.swipeable.is_animating() {
140 let val = self.swipeable.current_value();
141 if val != DismissValue::Default
142 && let Some(cb) = on_dismiss
143 {
144 cb();
145 }
146 }
147 }
148}
149
150pub fn SwipeToDismiss(
157 state: Rc<SwipeToDismissState>,
158 on_dismiss: Option<Rc<dyn Fn()>>,
159 background: View,
160 content: View,
161 modifier: Modifier,
162 config: SwipeToDismissConfig,
163) -> View {
164 let offset = state.offset();
165 state.try_handle_dismiss_with_threshold(&on_dismiss, config.dismiss_threshold);
166
167 let s1 = state.swipeable.clone();
168 let s2 = state.swipeable.clone();
169 let s3 = state.swipeable.clone();
170 let on_down = { move |e: PointerEvent| s1.on_pointer_down(e.position.x) };
171 let on_move = { move |e: PointerEvent| s2.on_pointer_move(e.position.x) };
172 let on_up = { move |_e: PointerEvent| s3.on_pointer_up() };
173
174 let display_offset = offset
175 .max(-config.dismissed_offset)
176 .min(config.dismissed_offset);
177
178 let content_modifier = {
179 let mut m = Modifier::new()
180 .fill_max_width()
181 .translate(display_offset, 0.0);
182 if config.gestures_enabled {
183 m = m
184 .on_pointer_down(on_down)
185 .on_pointer_move(on_move)
186 .on_pointer_up(on_up);
187 }
188 m
189 };
190
191 Column(modifier.fill_max_width()).child((
192 Box(Modifier::new().fill_max_size().absolute()).child(background),
193 Box(content_modifier).child(content),
194 ))
195}