#![allow(non_snake_case)]
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use repose_core::animation::AnimationSpec;
use repose_core::*;
use repose_ui::{
Box, Column, Row,
ViewExt, ZStack,
anim::animate_f32_from,
overlay::OverlayHandle,
};
use super::*;
static BOTTOMSHEET_COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Debug)]
pub struct BottomSheetConfig {
pub modifier: Modifier,
pub container_color: Color,
pub content_color: Color,
pub scrim_color: Color,
pub tonal_elevation: f32,
pub shadow_elevation: f32,
pub drag_handle_color: Color,
pub shape_radius: f32,
pub max_width: f32,
pub drag_handle_width: f32,
pub drag_handle_height: f32,
pub peek_height: f32,
pub gestures_enabled: bool,
}
impl Default for BottomSheetConfig {
fn default() -> Self {
Self {
modifier: Modifier::new(),
container_color: BottomSheetDefaults::container_color(),
content_color: BottomSheetDefaults::content_color(),
scrim_color: BottomSheetDefaults::scrim_color(),
tonal_elevation: BottomSheetDefaults::TONAL_ELEVATION,
shadow_elevation: 0.0,
drag_handle_color: BottomSheetDefaults::drag_handle_color(),
shape_radius: BottomSheetDefaults::SHAPE_RADIUS,
max_width: BottomSheetDefaults::MAX_WIDTH,
drag_handle_width: BottomSheetDefaults::DRAG_HANDLE_WIDTH,
drag_handle_height: BottomSheetDefaults::DRAG_HANDLE_HEIGHT,
peek_height: BottomSheetDefaults::PEEK_HEIGHT,
gestures_enabled: true,
}
}
}
pub fn BottomSheet(
visible: bool,
on_dismiss: impl Fn() + 'static,
modifier: Modifier,
content: View,
_config: BottomSheetConfig, ) -> View {
let th = theme();
let id = remember(|| BOTTOMSHEET_COUNTER.fetch_add(1, Ordering::Relaxed));
let opacity = animate_f32_from(
format!("bs_opacity_{id}"),
if visible { 0.0 } else { 1.0 },
if visible { 1.0 } else { 0.0 },
th.motion.layout,
);
let keep = visible || opacity > 0.01;
if keep {
Column(Modifier::new()).child((
Box(modifier.alpha(opacity)).child(content),
Box(Modifier::new()
.width(1.0)
.height(0.0)
.fill_max_width()
.alpha(opacity)
.hit_passthrough()
.on_pointer_down(move |_| on_dismiss())),
))
} else {
Box(Modifier::new())
}
}
pub struct SheetState {
visible: Signal<bool>,
drag_offset: Signal<f32>,
peek_height: Signal<f32>,
}
impl SheetState {
pub fn new(peek_height: f32) -> Self {
Self {
visible: signal(false),
drag_offset: signal(0.0),
peek_height: signal(peek_height),
}
}
pub fn is_visible(&self) -> bool {
self.visible.get()
}
pub fn show(&self) {
self.visible.set(true);
}
pub fn dismiss(&self) {
self.visible.set(false);
self.drag_offset.set(0.0);
}
pub fn set_peek_height(&self, h: f32) {
self.peek_height.set(h);
}
}
pub fn ModalBottomSheet(
state: Rc<SheetState>,
overlay: OverlayHandle,
modifier: Modifier,
content: View,
config: BottomSheetConfig,
) -> View {
let th = theme();
let peek_h = state.peek_height.get().max(config.peek_height);
let anim_distance = peek_h.max(48.0).max(400.0);
let overlay_id = remember_with_key("mbs_oid", || signal(0u64));
let drag_anchor_y: Rc<RefCell<f32>> = remember_state_with_key("mbs_drag_y", || 0.0);
let offset_at_drag_start: Rc<RefCell<f32>> = remember_state_with_key("mbs_drag_base", || 0.0);
let is_dragging: Rc<RefCell<bool>> = remember_state_with_key("mbs_drag", || false);
let anim = remember_state_with_key("mbs_anim", || {
AnimatedValue::new(anim_distance, theme().motion.spring)
});
let last_target = remember_state_with_key("mbs_anim_target", || f32::NAN);
let anim_target = if state.is_visible() {
0.0
} else {
anim_distance
};
{
let mut a = anim.borrow_mut();
let mut lt = last_target.borrow_mut();
if lt.is_nan() || (*lt - anim_target).abs() > 1e-6 {
if state.is_visible() {
a.set_spec(th.motion.spring);
} else {
a.set_spec(AnimationSpec::fast());
}
a.set_target(anim_target);
*lt = anim_target;
}
drop(lt);
let still_animating = a.update();
if still_animating {
request_frame();
}
}
let offset = *anim.borrow().get();
let sheet_visible = state.is_visible() || offset < anim_distance - 10.0;
if sheet_visible {
if overlay_id.get() == 0 {
let builder: Rc<dyn Fn() -> View> = Rc::new({
let state = state.clone();
let anim = anim.clone();
let modifier = modifier.clone();
let content = content.clone();
let drag_anchor_y = drag_anchor_y.clone();
let offset_at_drag_start = offset_at_drag_start.clone();
let is_dragging = is_dragging.clone();
let anim_distance = anim_distance;
move || {
let off = *anim.borrow().get();
let sheet_body = Box(modifier
.clone()
.fill_max_width()
.max_width(dp_to_px(config.max_width))
.translate(0.0, off)
.background(config.container_color)
.clip_rounded(config.shape_radius)
.on_pointer_down({
let anim = anim.clone();
let drag_anchor_y = drag_anchor_y.clone();
let offset_at_drag_start = offset_at_drag_start.clone();
let is_dragging = is_dragging.clone();
move |ev| {
*drag_anchor_y.borrow_mut() = ev.position.y;
*offset_at_drag_start.borrow_mut() = *anim.borrow().get();
*is_dragging.borrow_mut() = true;
}
})
.on_pointer_move({
let anim = anim.clone();
let drag_anchor_y = drag_anchor_y.clone();
let offset_at_drag_start = offset_at_drag_start.clone();
let is_dragging = is_dragging.clone();
move |ev| {
if !*is_dragging.borrow() {
return;
}
let delta = ev.position.y - *drag_anchor_y.borrow();
let start_off = *offset_at_drag_start.borrow();
let total = (start_off + delta).max(0.0);
anim.borrow_mut().snap_to(total);
request_frame();
}
})
.on_pointer_up({
let anim = anim.clone();
let is_dragging = is_dragging.clone();
let state = state.clone();
let anim_distance = anim_distance;
move |_| {
*is_dragging.borrow_mut() = false;
let current_off = *anim.borrow().get();
let threshold = anim_distance * 0.3;
if current_off > threshold {
anim.borrow_mut().set_target(anim_distance);
state.dismiss();
} else {
anim.borrow_mut().set_target(0.0);
}
}
}))
.child(
Column(Modifier::new().fill_max_width()).child((
Row(Modifier::new()
.fill_max_width()
.justify_content(JustifyContent::CENTER))
.child(Box(Modifier::new()
.margin_vertical(22.0)
.width(config.drag_handle_width)
.height(config.drag_handle_height)
.background(config.drag_handle_color)
.clip_rounded(2.0))),
content.clone(),
)),
);
let sheet = Box(Modifier::new()
.fill_max_size()
.justify_content(JustifyContent::CENTER)
.align_items(AlignItems::FLEX_END))
.child(sheet_body);
let scrim_alpha = if state.is_visible() {
config.scrim_color.3
} else {
let t = (off / anim_distance).clamp(0.0, 1.0);
(config.scrim_color.3 as f32 * (1.0 - t)) as u8
};
let scrim = Box(Modifier::new()
.fill_max_size()
.background(config.scrim_color.with_alpha(scrim_alpha))
.on_pointer_down({
let s = state.clone();
move |_| s.dismiss()
}));
ZStack(Modifier::new().fill_max_size().absolute()).child((scrim, sheet))
}
});
let id = overlay.show_entry(builder, 900.0, false);
overlay_id.set(id);
}
} else {
let prev = overlay_id.get();
if prev != 0 {
let _ = overlay.dismiss(prev);
overlay_id.set(0);
}
}
Box(Modifier::new())
}