repose_material/material3/
bottom_sheet.rs1#![allow(non_snake_case)]
2
3use std::cell::RefCell;
4use std::rc::Rc;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use repose_core::animation::AnimationSpec;
8use repose_core::*;
9use repose_ui::{
10 Box, Column, Row, ViewExt, ZStack, anim::animate_f32_from, overlay::OverlayHandle,
11};
12
13use super::*;
14
15static BOTTOMSHEET_COUNTER: AtomicU64 = AtomicU64::new(0);
16
17#[derive(Clone, Debug)]
19pub struct BottomSheetConfig {
20 pub modifier: Modifier,
21 pub container_color: Color,
22 pub content_color: Color,
23 pub scrim_color: Color,
24 pub tonal_elevation: f32,
25 pub shadow_elevation: f32,
26 pub drag_handle_color: Color,
27 pub shape_radius: f32,
28 pub max_width: f32,
29 pub drag_handle_width: f32,
30 pub drag_handle_height: f32,
31 pub peek_height: f32,
32 pub gestures_enabled: bool,
33}
34
35impl Default for BottomSheetConfig {
36 fn default() -> Self {
37 Self {
38 modifier: Modifier::new(),
39 container_color: BottomSheetDefaults::container_color(),
40 content_color: BottomSheetDefaults::content_color(),
41 scrim_color: BottomSheetDefaults::scrim_color(),
42 tonal_elevation: BottomSheetDefaults::TONAL_ELEVATION,
43 shadow_elevation: 0.0,
44 drag_handle_color: BottomSheetDefaults::drag_handle_color(),
45 shape_radius: BottomSheetDefaults::SHAPE_RADIUS,
46 max_width: BottomSheetDefaults::MAX_WIDTH,
47 drag_handle_width: BottomSheetDefaults::DRAG_HANDLE_WIDTH,
48 drag_handle_height: BottomSheetDefaults::DRAG_HANDLE_HEIGHT,
49 peek_height: BottomSheetDefaults::PEEK_HEIGHT,
50 gestures_enabled: true,
51 }
52 }
53}
54
55pub fn BottomSheet(
56 visible: bool,
57 on_dismiss: impl Fn() + 'static,
58 modifier: Modifier,
59 content: View,
60 _config: BottomSheetConfig, ) -> View {
62 let th = theme();
63 let id = remember(|| BOTTOMSHEET_COUNTER.fetch_add(1, Ordering::Relaxed));
64
65 let opacity = animate_f32_from(
66 format!("bs_opacity_{id}"),
67 if visible { 0.0 } else { 1.0 },
68 if visible { 1.0 } else { 0.0 },
69 th.motion.layout,
70 );
71
72 let keep = visible || opacity > 0.01;
73 if keep {
74 Column(Modifier::new()).child((
75 Box(modifier.alpha(opacity)).child(content),
76 Box(Modifier::new()
77 .width(1.0)
78 .height(0.0)
79 .fill_max_width()
80 .alpha(opacity)
81 .hit_passthrough()
82 .on_pointer_down(move |_| on_dismiss())),
83 ))
84 } else {
85 Box(Modifier::new())
86 }
87}
88
89pub struct SheetState {
91 visible: Signal<bool>,
92 drag_offset: Signal<f32>,
93 peek_height: Signal<f32>,
94}
95
96impl SheetState {
97 pub fn new(peek_height: f32) -> Self {
98 Self {
99 visible: signal(false),
100 drag_offset: signal(0.0),
101 peek_height: signal(peek_height),
102 }
103 }
104
105 pub fn is_visible(&self) -> bool {
106 self.visible.get()
107 }
108
109 pub fn show(&self) {
110 self.visible.set(true);
111 }
112
113 pub fn dismiss(&self) {
114 self.visible.set(false);
115 self.drag_offset.set(0.0);
116 }
117
118 pub fn set_peek_height(&self, h: f32) {
119 self.peek_height.set(h);
120 }
121}
122
123pub fn ModalBottomSheet(
128 state: Rc<SheetState>,
129 overlay: OverlayHandle,
130 modifier: Modifier,
131 content: View,
132 config: BottomSheetConfig,
133) -> View {
134 let th = theme();
135 let peek_h = state.peek_height.get().max(config.peek_height);
136 let anim_distance = peek_h.max(48.0).max(400.0);
137 let overlay_id = remember_with_key("mbs_oid", || signal(0u64));
138
139 let drag_anchor_y: Rc<RefCell<f32>> = remember_state_with_key("mbs_drag_y", || 0.0);
141 let offset_at_drag_start: Rc<RefCell<f32>> = remember_state_with_key("mbs_drag_base", || 0.0);
142 let is_dragging: Rc<RefCell<bool>> = remember_state_with_key("mbs_drag", || false);
143
144 let anim = remember_state_with_key("mbs_anim", || {
146 AnimatedValue::new(anim_distance, theme().motion.spring)
147 });
148 let last_target = remember_state_with_key("mbs_anim_target", || f32::NAN);
149 let anim_target = if state.is_visible() {
150 0.0
151 } else {
152 anim_distance
153 };
154
155 {
156 let mut a = anim.borrow_mut();
157 let mut lt = last_target.borrow_mut();
158 if lt.is_nan() || (*lt - anim_target).abs() > 1e-6 {
159 if state.is_visible() {
160 a.set_spec(th.motion.spring);
161 } else {
162 a.set_spec(AnimationSpec::fast());
163 }
164 a.set_target(anim_target);
165 *lt = anim_target;
166 }
167 drop(lt);
168 let still_animating = a.update();
169 if still_animating {
170 request_frame();
171 }
172 }
173
174 let offset = *anim.borrow().get();
175 let sheet_visible = state.is_visible() || offset < anim_distance - 10.0;
176
177 if sheet_visible {
178 if overlay_id.get() == 0 {
179 let builder: Rc<dyn Fn() -> View> = Rc::new({
180 let state = state.clone();
181 let anim = anim.clone();
182 let modifier = modifier.clone();
183 let content = content.clone();
184 let drag_anchor_y = drag_anchor_y.clone();
185 let offset_at_drag_start = offset_at_drag_start.clone();
186 let is_dragging = is_dragging.clone();
187 let anim_distance = anim_distance;
188 move || {
189 let off = *anim.borrow().get();
190
191 let sheet_body = Box(modifier
192 .clone()
193 .fill_max_width()
194 .max_width(dp_to_px(config.max_width))
195 .translate(0.0, off)
196 .background(config.container_color)
197 .clip_rounded(config.shape_radius)
198 .on_pointer_down({
199 let anim = anim.clone();
200 let drag_anchor_y = drag_anchor_y.clone();
201 let offset_at_drag_start = offset_at_drag_start.clone();
202 let is_dragging = is_dragging.clone();
203 move |ev| {
204 *drag_anchor_y.borrow_mut() = ev.position.y;
205 *offset_at_drag_start.borrow_mut() = *anim.borrow().get();
206 *is_dragging.borrow_mut() = true;
207 }
208 })
209 .on_pointer_move({
210 let anim = anim.clone();
211 let drag_anchor_y = drag_anchor_y.clone();
212 let offset_at_drag_start = offset_at_drag_start.clone();
213 let is_dragging = is_dragging.clone();
214 move |ev| {
215 if !*is_dragging.borrow() {
216 return;
217 }
218 let delta = ev.position.y - *drag_anchor_y.borrow();
219 let start_off = *offset_at_drag_start.borrow();
220 let total = (start_off + delta).max(0.0);
221 anim.borrow_mut().snap_to(total);
222 request_frame();
223 }
224 })
225 .on_pointer_up({
226 let anim = anim.clone();
227 let is_dragging = is_dragging.clone();
228 let state = state.clone();
229 let anim_distance = anim_distance;
230 move |_| {
231 *is_dragging.borrow_mut() = false;
232 let current_off = *anim.borrow().get();
233 let threshold = anim_distance * 0.3;
234 if current_off > threshold {
235 anim.borrow_mut().set_target(anim_distance);
236 state.dismiss();
237 } else {
238 anim.borrow_mut().set_target(0.0);
239 }
240 }
241 }))
242 .child(
243 Column(Modifier::new().fill_max_width()).child((
244 Row(Modifier::new()
245 .fill_max_width()
246 .justify_content(JustifyContent::CENTER))
247 .child(Box(Modifier::new()
248 .margin_vertical(22.0)
249 .width(config.drag_handle_width)
250 .height(config.drag_handle_height)
251 .background(config.drag_handle_color)
252 .clip_rounded(2.0))),
253 content.clone(),
254 )),
255 );
256
257 let sheet = Box(Modifier::new()
258 .fill_max_size()
259 .justify_content(JustifyContent::CENTER)
260 .align_items(AlignItems::FLEX_END))
261 .child(sheet_body);
262
263 let scrim_alpha = if state.is_visible() {
264 config.scrim_color.3
265 } else {
266 let t = (off / anim_distance).clamp(0.0, 1.0);
267 (config.scrim_color.3 as f32 * (1.0 - t)) as u8
268 };
269 let scrim = Box(Modifier::new()
270 .fill_max_size()
271 .background(config.scrim_color.with_alpha(scrim_alpha))
272 .on_pointer_down({
273 let s = state.clone();
274 move |_| s.dismiss()
275 }));
276
277 ZStack(Modifier::new().fill_max_size().absolute()).child((scrim, sheet))
278 }
279 });
280
281 let id = overlay.show_entry(builder, 900.0, false);
282 overlay_id.set(id);
283 }
284 } else {
285 let prev = overlay_id.get();
286 if prev != 0 {
287 let _ = overlay.dismiss(prev);
288 overlay_id.set(0);
289 }
290 }
291
292 Box(Modifier::new())
293}