use std::sync::Arc;
use blinc_animation::{AnimationPreset, MultiKeyframeAnimation};
use blinc_core::Color;
use blinc_layout::motion::motion_derived;
use blinc_layout::overlay_state::get_overlay_manager;
use blinc_layout::prelude::*;
use blinc_layout::widgets::overlay::{BackdropConfig, EdgeSide, OverlayHandle, OverlayManagerExt};
use blinc_layout::InstanceKey;
use blinc_theme::{ColorToken, RadiusToken, ThemeState};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DrawerSide {
#[default]
Left,
Right,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DrawerSize {
Narrow,
#[default]
Medium,
Wide,
}
impl DrawerSize {
pub fn width(&self) -> f32 {
match self {
DrawerSize::Narrow => 240.0,
DrawerSize::Medium => 280.0,
DrawerSize::Wide => 320.0,
}
}
}
pub struct DrawerBuilder {
side: DrawerSide,
size: DrawerSize,
title: Option<String>,
header: Option<Arc<dyn Fn() -> Div + Send + Sync>>,
children: Vec<Arc<dyn Fn() -> Div + Send + Sync>>,
footer: Option<Arc<dyn Fn() -> Div + Send + Sync>>,
show_close: bool,
on_close: Option<Arc<dyn Fn() + Send + Sync>>,
animation_duration: u32,
classes: Vec<String>,
user_id: Option<String>,
key: InstanceKey,
}
impl DrawerBuilder {
#[track_caller]
pub fn new() -> Self {
Self {
side: DrawerSide::Left,
size: DrawerSize::Medium,
title: None,
header: None,
children: Vec::new(),
footer: None,
show_close: true,
on_close: None,
animation_duration: 250,
key: InstanceKey::new("drawer"),
classes: Vec::new(),
user_id: None,
}
}
pub fn side(mut self, side: DrawerSide) -> Self {
self.side = side;
self
}
pub fn size(mut self, size: DrawerSize) -> Self {
self.size = size;
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn header<F>(mut self, header: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.header = Some(Arc::new(header));
self
}
pub fn child<F>(mut self, child: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.children.push(Arc::new(child));
self
}
pub fn child_builder<B: ElementBuilder + Clone + Send + Sync + 'static>(
mut self,
builder: B,
) -> Self {
self.children
.push(Arc::new(move || div().child(builder.clone())));
self
}
pub fn footer<F>(mut self, footer: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.footer = Some(Arc::new(footer));
self
}
pub fn show_close(mut self, show: bool) -> Self {
self.show_close = show;
self
}
pub fn on_close<F>(mut self, callback: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_close = Some(Arc::new(callback));
self
}
pub fn animation_duration(mut self, duration_ms: u32) -> Self {
self.animation_duration = duration_ms;
self
}
pub fn class(mut self, name: impl Into<String>) -> Self {
self.classes.push(name.into());
self
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.user_id = Some(id.into());
self
}
fn get_enter_animation(&self) -> MultiKeyframeAnimation {
let distance = self.size.width();
match self.side {
DrawerSide::Left => AnimationPreset::slide_in_left(self.animation_duration, distance),
DrawerSide::Right => AnimationPreset::slide_in_right(self.animation_duration, distance),
}
}
fn get_exit_animation(&self) -> MultiKeyframeAnimation {
let exit_duration = (self.animation_duration as f32 * 0.7) as u32;
let distance = self.size.width();
match self.side {
DrawerSide::Left => AnimationPreset::slide_out_left(exit_duration, distance),
DrawerSide::Right => AnimationPreset::slide_out_right(exit_duration, distance),
}
}
pub fn show(self) -> OverlayHandle {
let theme = ThemeState::get();
let bg = theme.color(ColorToken::Surface);
let border = theme.color(ColorToken::Border);
let text_primary = theme.color(ColorToken::TextPrimary);
let text_secondary = theme.color(ColorToken::TextSecondary);
let enter_animation = self.get_enter_animation();
let exit_animation = self.get_exit_animation();
let side = self.side;
let size = self.size;
let title = self.title;
let header = self.header;
let children = self.children;
let footer = self.footer;
let show_close = self.show_close;
let on_close = self.on_close;
let mgr = get_overlay_manager();
let motion_key_str = format!("drawer_{}", self.key.get());
let motion_key_with_child = format!("{}:child:0", motion_key_str);
let edge_side = match side {
DrawerSide::Left => EdgeSide::Left,
DrawerSide::Right => EdgeSide::Right,
};
let drawer_width = size.width();
mgr.modal()
.dismiss_on_escape(true)
.backdrop(BackdropConfig::dark().dismiss_on_click(true))
.edge_position(edge_side)
.size(drawer_width, 10000.0) .motion_key(&motion_key_with_child)
.content(move || {
build_drawer_content(
side,
size,
&title,
&header,
&children,
&footer,
show_close,
&on_close,
bg,
border,
text_primary,
text_secondary,
&enter_animation,
&exit_animation,
&motion_key_str,
)
})
.show()
}
}
impl Default for DrawerBuilder {
fn default() -> Self {
Self::new()
}
}
#[track_caller]
pub fn drawer() -> DrawerBuilder {
DrawerBuilder::new()
}
#[allow(clippy::too_many_arguments)]
fn build_drawer_content(
side: DrawerSide,
size: DrawerSize,
title: &Option<String>,
header: &Option<Arc<dyn Fn() -> Div + Send + Sync>>,
children: &[Arc<dyn Fn() -> Div + Send + Sync>],
footer: &Option<Arc<dyn Fn() -> Div + Send + Sync>>,
show_close: bool,
on_close: &Option<Arc<dyn Fn() + Send + Sync>>,
bg: Color,
border: Color,
text_primary: Color,
text_secondary: Color,
enter_animation: &MultiKeyframeAnimation,
exit_animation: &MultiKeyframeAnimation,
motion_key: &str,
) -> Div {
let theme = ThemeState::get();
let radius = theme.radius(RadiusToken::Lg);
let border_radius = match side {
DrawerSide::Left => (0.0, radius, radius, 0.0), DrawerSide::Right => (radius, 0.0, 0.0, radius), };
let mut drawer = div()
.class("cn-drawer")
.w(size.width())
.h_full()
.bg(bg)
.border(1.0, border)
.shadow_xl()
.flex_col()
.overflow_clip();
let (tl, tr, br, bl) = border_radius;
drawer = drawer.rounded_corners(tl, tr, br, bl);
let has_header = header.is_some() || title.is_some() || show_close;
if has_header {
let mut header_div = div()
.class("cn-drawer-header")
.w_full()
.flex_row()
.items_center()
.justify_between();
if let Some(ref header_fn) = header {
header_div = header_div.child(header_fn());
} else if let Some(ref title_text) = title {
header_div = header_div.child(
text(title_text)
.size(theme.typography().text_lg)
.color(text_primary)
.semibold(),
);
} else {
header_div = header_div.child(div());
}
if show_close {
let close_icon = r#"<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" x2="6" y1="6" y2="18"/><line x1="6" x2="18" y1="6" y2="18"/></svg>"#;
let on_close_clone = on_close.clone();
header_div = header_div.child(
div()
.w(32.0)
.h(32.0)
.items_center()
.rounded(theme.radius(RadiusToken::Sm))
.cursor_pointer()
.on_click(move |_| {
if let Some(ref cb) = on_close_clone {
cb();
}
get_overlay_manager().close_top();
})
.child(svg(close_icon).size(18.0, 18.0).color(text_secondary)),
);
}
drawer = drawer.child(header_div);
drawer = drawer.child(div().w_full().h(1.0).bg(border));
}
if !children.is_empty() {
let mut body = div()
.flex_1()
.w_full()
.flex_col()
.gap_1()
.p_2()
.overflow_scroll();
for child_fn in children {
body = body.child(child_fn());
}
drawer = drawer.child(body);
}
if let Some(ref footer_fn) = footer {
if children.is_empty() {
drawer = drawer.child(div().flex_1());
}
drawer = drawer.child(div().w_full().h(1.0).bg(border)); drawer = drawer.child(div().class("cn-drawer-footer").w_full().child(footer_fn()));
}
div().child(
motion_derived(motion_key)
.enter_animation(enter_animation.clone())
.exit_animation(exit_animation.clone())
.child(drawer),
)
}
#[track_caller]
pub fn drawer_left() -> DrawerBuilder {
drawer().side(DrawerSide::Left)
}
#[track_caller]
pub fn drawer_right() -> DrawerBuilder {
drawer().side(DrawerSide::Right)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_drawer_builder() {
let builder = drawer()
.side(DrawerSide::Right)
.size(DrawerSize::Wide)
.title("Test");
assert_eq!(builder.side, DrawerSide::Right);
assert_eq!(builder.size, DrawerSize::Wide);
assert_eq!(builder.title, Some("Test".to_string()));
}
#[test]
fn test_drawer_sizes() {
assert_eq!(DrawerSize::Narrow.width(), 240.0);
assert_eq!(DrawerSize::Medium.width(), 280.0);
assert_eq!(DrawerSize::Wide.width(), 320.0);
}
#[test]
fn test_drawer_sides() {
assert_eq!(DrawerSide::default(), DrawerSide::Left);
}
}