tessera_ui_basic_components/
bottom_nav_bar.rs1use std::{
7 collections::HashMap,
8 sync::Arc,
9 time::{Duration, Instant},
10};
11
12use parking_lot::{Mutex, RwLock};
13use tessera_ui::{Color, DimensionValue, tessera};
14
15use crate::{
16 RippleState,
17 alignment::MainAxisAlignment,
18 animation,
19 button::{ButtonArgsBuilder, button},
20 pipelines::ShadowProps,
21 row::{RowArgsBuilder, row},
22 shape_def::Shape,
23 surface::{SurfaceArgsBuilder, surface},
24};
25
26const ANIMATION_DURATION: Duration = Duration::from_millis(300);
27const ACTIVE_COLOR: Color = Color::from_rgb_u8(225, 235, 255);
28const INACTIVE_COLOR: Color = Color::WHITE;
29const ACTIVE_COLOR_SHADOW: Color = Color::from_rgba_u8(100, 115, 140, 100);
30
31fn interpolate_color(from: Color, to: Color, progress: f32) -> Color {
32 Color {
33 r: from.r + (to.r - from.r) * progress,
34 g: from.g + (to.g - from.g) * progress,
35 b: from.b + (to.b - from.b) * progress,
36 a: from.a + (to.a - from.a) * progress,
37 }
38}
39
40#[tessera]
67pub fn bottom_nav_bar<F>(state: BottomNavBarState, scope_config: F)
68where
69 F: FnOnce(&mut BottomNavBarScope),
70{
71 let mut child_closures = Vec::new();
72
73 {
74 let mut scope = BottomNavBarScope {
75 child_closures: &mut child_closures,
76 };
77 scope_config(&mut scope);
78 }
79
80 let progress = state.animation_progress().unwrap_or(1.0);
81
82 surface(
83 SurfaceArgsBuilder::default()
84 .width(DimensionValue::FILLED)
85 .style(Color::from_rgb(9.333, 9.333, 9.333).into())
86 .shadow(ShadowProps::default())
87 .block_input(true)
88 .build()
89 .unwrap(),
90 None,
91 move || {
92 row(
93 RowArgsBuilder::default()
94 .width(DimensionValue::FILLED)
95 .main_axis_alignment(MainAxisAlignment::SpaceAround)
96 .build()
97 .unwrap(),
98 move |row_scope| {
99 for (index, (child_content, on_click)) in child_closures.into_iter().enumerate()
100 {
101 let state_clone = state.clone();
102 row_scope.child(move || {
103 let selected = state_clone.selected();
104 let previous_selected = state_clone.previous_selected();
105 let ripple_state = state_clone.ripple_state(index);
106
107 let color;
108 let shadow_color;
109 if index == selected {
110 color = interpolate_color(INACTIVE_COLOR, ACTIVE_COLOR, progress);
111 shadow_color =
112 interpolate_color(INACTIVE_COLOR, ACTIVE_COLOR_SHADOW, progress)
113 } else if index == previous_selected {
114 color = interpolate_color(ACTIVE_COLOR, INACTIVE_COLOR, progress);
115 shadow_color =
116 interpolate_color(ACTIVE_COLOR_SHADOW, INACTIVE_COLOR, progress)
117 } else {
118 color = INACTIVE_COLOR;
119 shadow_color = INACTIVE_COLOR;
120 }
121
122 let button_args = ButtonArgsBuilder::default()
123 .color(color)
124 .shape(Shape::HorizontalCapsule)
125 .on_click(Arc::new(move || {
126 if index != selected {
127 state_clone.set_selected(index);
128 on_click.lock().take().unwrap()();
129 }
130 }))
131 .shadow(ShadowProps {
132 color: shadow_color,
133 ..Default::default()
134 })
135 .build()
136 .unwrap();
137
138 button(button_args, ripple_state, || {
139 child_content();
140 });
141 });
142 }
143 },
144 );
145 },
146 );
147}
148
149struct BottomNavBarStateInner {
155 selected: usize,
156 previous_selected: usize,
157 ripple_states: HashMap<usize, RippleState>,
158 anim_start_time: Option<Instant>,
159}
160
161impl BottomNavBarStateInner {
162 fn new(selected: usize) -> Self {
163 Self {
164 selected,
165 previous_selected: selected,
166 ripple_states: HashMap::new(),
167 anim_start_time: None,
168 }
169 }
170
171 fn set_selected(&mut self, index: usize) {
172 if self.selected != index {
173 self.previous_selected = self.selected;
174 self.selected = index;
175 self.anim_start_time = Some(Instant::now());
176 }
177 }
178
179 fn animation_progress(&mut self) -> Option<f32> {
180 if let Some(start_time) = self.anim_start_time {
181 let elapsed = start_time.elapsed();
182 if elapsed < ANIMATION_DURATION {
183 Some(animation::easing(
184 elapsed.as_secs_f32() / ANIMATION_DURATION.as_secs_f32(),
185 ))
186 } else {
187 self.anim_start_time = None;
188 None
189 }
190 } else {
191 None
192 }
193 }
194
195 fn ripple_state(&mut self, index: usize) -> RippleState {
196 self.ripple_states.entry(index).or_default().clone()
197 }
198}
199
200#[derive(Clone)]
201pub struct BottomNavBarState {
202 inner: Arc<RwLock<BottomNavBarStateInner>>,
203}
204
205impl BottomNavBarState {
206 pub fn new(selected: usize) -> Self {
208 Self {
209 inner: Arc::new(RwLock::new(BottomNavBarStateInner::new(selected))),
210 }
211 }
212
213 pub fn selected(&self) -> usize {
215 self.inner.read().selected
216 }
217
218 pub fn previous_selected(&self) -> usize {
220 self.inner.read().previous_selected
221 }
222
223 fn set_selected(&self, index: usize) {
224 self.inner.write().set_selected(index);
225 }
226
227 fn animation_progress(&self) -> Option<f32> {
228 self.inner.write().animation_progress()
229 }
230
231 fn ripple_state(&self, index: usize) -> RippleState {
232 self.inner.write().ripple_state(index)
233 }
234}
235
236impl Default for BottomNavBarState {
237 fn default() -> Self {
238 Self::new(0)
239 }
240}
241
242pub struct BottomNavBarScope<'a> {
244 child_closures: &'a mut Vec<(
245 Box<dyn FnOnce() + Send + Sync>,
246 Arc<Mutex<Option<Box<dyn FnOnce() + Send + Sync>>>>,
247 )>,
248}
249
250impl<'a> BottomNavBarScope<'a> {
251 pub fn child<C, O>(&mut self, child: C, on_click: O)
259 where
260 C: FnOnce() + Send + Sync + 'static,
261 O: FnOnce() + Send + Sync + 'static,
262 {
263 self.child_closures.push((
264 Box::new(child),
265 Arc::new(Mutex::new(Some(Box::new(on_click)))),
266 ));
267 }
268}