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,
61) -> 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 return Box(Modifier::new());
75 }
76 Column(Modifier::new().fill_max_width()).child((
77 Box(modifier
78 .alpha(opacity)
79 .background(config.container_color)
80 .clip_rounded(config.shape_radius)
81 .then(config.modifier))
82 .child(with_content_color(config.content_color, move || content)),
83 Box(Modifier::new()
84 .width(1.0)
85 .height(0.0)
86 .fill_max_width()
87 .alpha(opacity)
88 .hit_passthrough()
89 .on_pointer_down(move |_| on_dismiss())),
90 ))
91}
92
93pub struct SheetState {
95 visible: Signal<bool>,
96 drag_offset: Signal<f32>,
97 peek_height: Signal<f32>,
98}
99
100impl SheetState {
101 pub fn new(peek_height: f32) -> Self {
102 Self {
103 visible: signal(false),
104 drag_offset: signal(0.0),
105 peek_height: signal(peek_height),
106 }
107 }
108
109 pub fn is_visible(&self) -> bool {
110 self.visible.get()
111 }
112
113 pub fn show(&self) {
114 self.visible.set(true);
115 }
116
117 pub fn dismiss(&self) {
118 self.visible.set(false);
119 self.drag_offset.set(0.0);
120 }
121
122 pub fn set_peek_height(&self, h: f32) {
123 self.peek_height.set(h);
124 }
125}
126
127pub fn ModalBottomSheet(
132 state: Rc<SheetState>,
133 overlay: OverlayHandle,
134 modifier: Modifier,
135 content: View,
136 config: BottomSheetConfig,
137) -> View {
138 let th = theme();
139 let peek_h = state.peek_height.get().max(config.peek_height);
140 let anim_distance = peek_h.max(48.0).max(400.0);
141 let overlay_id = remember_with_key("mbs_oid", || signal(0u64));
142
143 let current_content = remember_state_with_key("mbs_c", || Box(Modifier::new()));
145 *current_content.borrow_mut() = content;
146
147 let drag_anchor_y: Rc<RefCell<f32>> = remember_state_with_key("mbs_drag_y", || 0.0);
149 let offset_at_drag_start: Rc<RefCell<f32>> = remember_state_with_key("mbs_drag_base", || 0.0);
150 let is_dragging: Rc<RefCell<bool>> = remember_state_with_key("mbs_drag", || false);
151
152 let anim = remember_state_with_key("mbs_anim", || {
154 AnimatedValue::new(anim_distance, theme().motion.spring)
155 });
156 let last_target = remember_state_with_key("mbs_anim_target", || f32::NAN);
157 let anim_target = if state.is_visible() {
158 0.0
159 } else {
160 anim_distance
161 };
162
163 {
164 let mut a = anim.borrow_mut();
165 let mut lt = last_target.borrow_mut();
166 if lt.is_nan() || (*lt - anim_target).abs() > 1e-6 {
167 if state.is_visible() {
168 a.set_spec(th.motion.spring);
169 } else {
170 a.set_spec(AnimationSpec::fast());
171 }
172 a.set_target(anim_target);
173 *lt = anim_target;
174 }
175 drop(lt);
176 let still_animating = a.update();
177 if still_animating {
178 request_frame();
179 }
180 }
181
182 let offset = *anim.borrow().get();
183 let sheet_visible = state.is_visible() || offset < anim_distance - 10.0;
184
185 if sheet_visible {
186 if overlay_id.get() == 0 {
187 let builder: Rc<dyn Fn() -> View> = Rc::new({
188 let state = state.clone();
189 let anim = anim.clone();
190 let modifier = modifier.clone();
191 let current_content = current_content.clone();
192 let drag_anchor_y = drag_anchor_y.clone();
193 let offset_at_drag_start = offset_at_drag_start.clone();
194 let is_dragging = is_dragging.clone();
195 let anim_distance = anim_distance;
196 move || {
197 let off = *anim.borrow().get();
198 let content = current_content.borrow().clone();
199
200 let mut sheet_mod = modifier
201 .clone()
202 .fill_max_width()
203 .max_width(dp_to_px(config.max_width))
204 .translate(0.0, off)
205 .background(config.container_color)
206 .clip_rounded(config.shape_radius);
207
208 if config.gestures_enabled {
209 sheet_mod = sheet_mod
210 .on_pointer_down({
211 let anim = anim.clone();
212 let drag_anchor_y = drag_anchor_y.clone();
213 let offset_at_drag_start = offset_at_drag_start.clone();
214 let is_dragging = is_dragging.clone();
215 move |ev| {
216 *drag_anchor_y.borrow_mut() = ev.position.y;
217 *offset_at_drag_start.borrow_mut() = *anim.borrow().get();
218 *is_dragging.borrow_mut() = true;
219 }
220 })
221 .on_pointer_move({
222 let anim = anim.clone();
223 let drag_anchor_y = drag_anchor_y.clone();
224 let offset_at_drag_start = offset_at_drag_start.clone();
225 let is_dragging = is_dragging.clone();
226 move |ev| {
227 if !*is_dragging.borrow() {
228 return;
229 }
230 let delta = ev.position.y - *drag_anchor_y.borrow();
231 let start_off = *offset_at_drag_start.borrow();
232 let total = (start_off + delta).max(0.0);
233 anim.borrow_mut().snap_to(total);
234 request_frame();
235 }
236 })
237 .on_pointer_up({
238 let anim = anim.clone();
239 let is_dragging = is_dragging.clone();
240 let state = state.clone();
241 let anim_distance = anim_distance;
242 move |_| {
243 *is_dragging.borrow_mut() = false;
244 let current_off = *anim.borrow().get();
245 let threshold = anim_distance * 0.3;
246 if current_off > threshold {
247 anim.borrow_mut().set_target(anim_distance);
248 state.dismiss();
249 } else {
250 anim.borrow_mut().set_target(0.0);
251 }
252 }
253 });
254 }
255
256 let sheet_body = Box(sheet_mod).child(
257 Column(Modifier::new().fill_max_width()).child((
258 Row(Modifier::new()
259 .fill_max_width()
260 .justify_content(JustifyContent::CENTER))
261 .child(Box(Modifier::new()
262 .margin_vertical(22.0)
263 .width(config.drag_handle_width)
264 .height(config.drag_handle_height)
265 .background(config.drag_handle_color)
266 .clip_rounded(2.0))),
267 content,
268 )),
269 );
270
271 let sheet = Box(Modifier::new()
272 .fill_max_size()
273 .justify_content(JustifyContent::CENTER)
274 .align_items(AlignItems::FLEX_END))
275 .child(sheet_body);
276
277 let scrim_alpha = if state.is_visible() {
278 config.scrim_color.3
279 } else {
280 let t = (off / anim_distance).clamp(0.0, 1.0);
281 (config.scrim_color.3 as f32 * (1.0 - t)) as u8
282 };
283 let scrim = Box(Modifier::new()
284 .fill_max_size()
285 .background(config.scrim_color.with_alpha(scrim_alpha))
286 .input_blocker()
287 .on_scroll(|_| Vec2::default())
288 .on_pointer_down({
289 let s = state.clone();
290 move |_| s.dismiss()
291 }));
292
293 ZStack(Modifier::new().fill_max_size().absolute()).child((scrim, sheet))
294 }
295 });
296
297 let id = overlay.show_entry(builder, 900.0, false);
298 overlay_id.set(id);
299 }
300 } else {
301 let prev = overlay_id.get();
302 if prev != 0 {
303 let _ = overlay.dismiss(prev);
304 overlay_id.set(0);
305 }
306 }
307
308 Box(Modifier::new())
309}