use std::{
collections::HashMap,
sync::Arc,
time::{Duration, Instant},
};
use parking_lot::{Mutex, RwLock};
use tessera_ui::{Color, DimensionValue, tessera};
use crate::{
RippleState,
alignment::MainAxisAlignment,
animation,
button::{ButtonArgsBuilder, button},
pipelines::ShadowProps,
row::{RowArgsBuilder, row},
shape_def::Shape,
surface::{SurfaceArgsBuilder, surface},
};
const ANIMATION_DURATION: Duration = Duration::from_millis(300);
const ACTIVE_COLOR: Color = Color::from_rgb_u8(225, 235, 255);
const INACTIVE_COLOR: Color = Color::WHITE;
const ACTIVE_COLOR_SHADOW: Color = Color::from_rgba_u8(100, 115, 140, 100);
fn interpolate_color(from: Color, to: Color, progress: f32) -> Color {
Color {
r: from.r + (to.r - from.r) * progress,
g: from.g + (to.g - from.g) * progress,
b: from.b + (to.b - from.b) * progress,
a: from.a + (to.a - from.a) * progress,
}
}
#[tessera]
pub fn bottom_nav_bar<F>(state: BottomNavBarState, scope_config: F)
where
F: FnOnce(&mut BottomNavBarScope),
{
let mut child_closures = Vec::new();
{
let mut scope = BottomNavBarScope {
child_closures: &mut child_closures,
};
scope_config(&mut scope);
}
let progress = state.animation_progress().unwrap_or(1.0);
surface(
SurfaceArgsBuilder::default()
.width(DimensionValue::FILLED)
.style(Color::from_rgb(9.333, 9.333, 9.333).into())
.shadow(ShadowProps::default())
.block_input(true)
.build()
.unwrap(),
None,
move || {
row(
RowArgsBuilder::default()
.width(DimensionValue::FILLED)
.main_axis_alignment(MainAxisAlignment::SpaceAround)
.build()
.unwrap(),
move |row_scope| {
for (index, (child_content, on_click)) in child_closures.into_iter().enumerate()
{
let state_clone = state.clone();
row_scope.child(move || {
let selected = state_clone.selected();
let previous_selected = state_clone.previous_selected();
let ripple_state = state_clone.ripple_state(index);
let color;
let shadow_color;
if index == selected {
color = interpolate_color(INACTIVE_COLOR, ACTIVE_COLOR, progress);
shadow_color =
interpolate_color(INACTIVE_COLOR, ACTIVE_COLOR_SHADOW, progress)
} else if index == previous_selected {
color = interpolate_color(ACTIVE_COLOR, INACTIVE_COLOR, progress);
shadow_color =
interpolate_color(ACTIVE_COLOR_SHADOW, INACTIVE_COLOR, progress)
} else {
color = INACTIVE_COLOR;
shadow_color = INACTIVE_COLOR;
}
let button_args = ButtonArgsBuilder::default()
.color(color)
.shape(Shape::HorizontalCapsule)
.on_click(Arc::new(move || {
if index != selected {
state_clone.set_selected(index);
on_click.lock().take().unwrap()();
}
}))
.shadow(ShadowProps {
color: shadow_color,
..Default::default()
})
.build()
.unwrap();
button(button_args, ripple_state, || {
child_content();
});
});
}
},
);
},
);
}
struct BottomNavBarStateInner {
selected: usize,
previous_selected: usize,
ripple_states: HashMap<usize, RippleState>,
anim_start_time: Option<Instant>,
}
impl BottomNavBarStateInner {
fn new(selected: usize) -> Self {
Self {
selected,
previous_selected: selected,
ripple_states: HashMap::new(),
anim_start_time: None,
}
}
fn set_selected(&mut self, index: usize) {
if self.selected != index {
self.previous_selected = self.selected;
self.selected = index;
self.anim_start_time = Some(Instant::now());
}
}
fn animation_progress(&mut self) -> Option<f32> {
if let Some(start_time) = self.anim_start_time {
let elapsed = start_time.elapsed();
if elapsed < ANIMATION_DURATION {
Some(animation::easing(
elapsed.as_secs_f32() / ANIMATION_DURATION.as_secs_f32(),
))
} else {
self.anim_start_time = None;
None
}
} else {
None
}
}
fn ripple_state(&mut self, index: usize) -> RippleState {
self.ripple_states.entry(index).or_default().clone()
}
}
#[derive(Clone)]
pub struct BottomNavBarState {
inner: Arc<RwLock<BottomNavBarStateInner>>,
}
impl BottomNavBarState {
pub fn new(selected: usize) -> Self {
Self {
inner: Arc::new(RwLock::new(BottomNavBarStateInner::new(selected))),
}
}
pub fn selected(&self) -> usize {
self.inner.read().selected
}
pub fn previous_selected(&self) -> usize {
self.inner.read().previous_selected
}
fn set_selected(&self, index: usize) {
self.inner.write().set_selected(index);
}
fn animation_progress(&self) -> Option<f32> {
self.inner.write().animation_progress()
}
fn ripple_state(&self, index: usize) -> RippleState {
self.inner.write().ripple_state(index)
}
}
impl Default for BottomNavBarState {
fn default() -> Self {
Self::new(0)
}
}
pub struct BottomNavBarScope<'a> {
child_closures: &'a mut Vec<(
Box<dyn FnOnce() + Send + Sync>,
Arc<Mutex<Option<Box<dyn FnOnce() + Send + Sync>>>>,
)>,
}
impl<'a> BottomNavBarScope<'a> {
pub fn child<C, O>(&mut self, child: C, on_click: O)
where
C: FnOnce() + Send + Sync + 'static,
O: FnOnce() + Send + Sync + 'static,
{
self.child_closures.push((
Box::new(child),
Arc::new(Mutex::new(Some(Box::new(on_click)))),
));
}
}