Skip to main content

repose_material/material3/
nav_bar.rs

1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use repose_core::animation::AnimationSpec;
7use repose_core::*;
8use repose_ui::{
9    Box, Column, Row, Text, TextStyle,
10    ViewExt,
11    anim::{animate_color, animate_f32},
12};
13
14use super::*;
15
16static NAVBAR_COUNTER: AtomicU64 = AtomicU64::new(0);
17
18/// Configuration for [`NavigationBar`].
19#[derive(Clone, Debug)]
20pub struct NavigationBarConfig {
21    pub modifier: Modifier,
22    pub container_color: Color,
23    pub content_color: Color,
24    pub selected_icon_color: Color,
25    pub selected_text_color: Color,
26    pub unselected_icon_color: Color,
27    pub unselected_text_color: Color,
28    pub indicator_color: Color,
29    pub height: f32,
30    pub tonal_elevation: f32,
31    pub indicator_opacity: f32,
32    pub indicator_radius: f32,
33    pub item_spacing: f32,
34    pub indicator_width: f32,
35    pub indicator_height: f32,
36}
37
38impl Default for NavigationBarConfig {
39    fn default() -> Self {
40        Self {
41            modifier: Modifier::new(),
42            container_color: NavigationBarDefaults::container_color(),
43            content_color: NavigationBarDefaults::content_color(),
44            selected_icon_color: NavigationBarDefaults::selected_icon_color(),
45            selected_text_color: NavigationBarDefaults::selected_text_color(),
46            unselected_icon_color: NavigationBarDefaults::unselected_icon_color(),
47            unselected_text_color: NavigationBarDefaults::unselected_text_color(),
48            indicator_color: NavigationBarDefaults::indicator_color(),
49            height: NavigationBarDefaults::HEIGHT,
50            tonal_elevation: NavigationBarDefaults::TONAL_ELEVATION,
51            indicator_opacity: NavigationBarDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
52            indicator_radius: NavigationBarDefaults::INDICATOR_RADIUS,
53            item_spacing: NavigationBarDefaults::ITEM_SPACING,
54            indicator_width: NavigationBarDefaults::ACTIVE_INDICATOR_WIDTH,
55            indicator_height: NavigationBarDefaults::ACTIVE_INDICATOR_HEIGHT,
56        }
57    }
58}
59
60/// M3 Navigation Bar - a bottom navigation bar with animated selection.
61/// Colors and indicator background transition with 200ms FastOutSlowIn.
62pub fn NavigationBar(
63    selected_index: usize,
64    items: Vec<NavItem>,
65    config: NavigationBarConfig,
66) -> View {
67    let th = theme();
68    let id = remember(|| NAVBAR_COUNTER.fetch_add(1, Ordering::Relaxed));
69
70    let mut bar_m = Modifier::new()
71        .fill_max_size()
72        .min_height(config.height)
73        .background(config.container_color)
74        .then(config.modifier);
75
76    if config.tonal_elevation > 0.0 {
77        bar_m = bar_m.state_elevation(StateElevation {
78            default: config.tonal_elevation,
79            hovered: config.tonal_elevation,
80            pressed: config.tonal_elevation,
81            disabled: 0.0,
82        });
83    }
84
85    Box(bar_m).child(
86        Row(Modifier::new()
87            .fill_max_size()
88            .align_items(AlignItems::CENTER)
89            .column_gap(config.item_spacing)
90            .semantics(Semantics::new(Role::Container).with_selectable_group()))
91        .child(
92            items
93                .into_iter()
94                .enumerate()
95                .map(|(i, item)| {
96                    let selected = i == selected_index;
97                    let is_enabled = item.enabled;
98                    let default_effects = AnimationSpec::spring_crit(40.0);
99                    let fg_icon = animate_color(
100                        format!("nb_fi_{}_{}", id, i),
101                        if selected {
102                            config.selected_icon_color
103                        } else {
104                            config.unselected_icon_color
105                        },
106                        default_effects,
107                    );
108                    let fg_label = animate_color(
109                        format!("nb_fl_{}_{}", id, i),
110                        if selected {
111                            config.selected_text_color
112                        } else {
113                            config.unselected_text_color
114                        },
115                        default_effects,
116                    );
117                    let bg_alpha = animate_f32(
118                        format!("nb_bg_{}_{}", id, i),
119                        if selected { 1.0 } else { 0.0 },
120                        default_effects,
121                    );
122                    let indicator_bg = config
123                        .indicator_color
124                        .with_alpha_f32(bg_alpha * config.indicator_opacity);
125                    let cb = item.on_click.clone();
126                    let nb_source: Rc<MutableInteractionSource> = item
127                        .interaction_source
128                        .clone()
129                        .map(Rc::new)
130                        .unwrap_or_else(|| remember(MutableInteractionSource::new));
131
132                    let mut item_m = Modifier::new()
133                        .flex_grow(1.0)
134                        .interaction_source(&*nb_source)
135                        .semantics(Semantics::new(Role::Tab).with_label(&item.label));
136
137                    if is_enabled {
138                        item_m = item_m.clickable().on_click({
139                            let cb = cb.clone();
140                            move || cb()
141                        });
142                    }
143
144                    Box(item_m).child(
145                        Column(
146                            Modifier::new()
147                                .fill_max_size()
148                                .align_items(AlignItems::CENTER)
149                                .justify_content(JustifyContent::CENTER),
150                        )
151                        .child((
152                            // Indicator pill behind icon
153                            Column(
154                                Modifier::new()
155                                    .align_items(AlignItems::CENTER)
156                                    .justify_content(JustifyContent::CENTER),
157                            )
158                            .child((
159                                Box(Modifier::new()
160                                    .absolute()
161                                    .offset(
162                                        Some((24.0 - config.indicator_width) / 2.0),
163                                        Some((24.0 - config.indicator_height) / 2.0),
164                                        None,
165                                        None,
166                                    )
167                                    .width(config.indicator_width)
168                                    .height(config.indicator_height)
169                                    .background(indicator_bg)
170                                    .clip_rounded(config.indicator_radius)
171                                    .state_colors(StateColors {
172                                        default: Color::TRANSPARENT,
173                                        hovered: th.on_surface.with_alpha_f32(0.08),
174                                        pressed: th.on_surface.with_alpha_f32(0.12),
175                                        disabled: Color::TRANSPARENT,
176                                    })),
177                                with_content_color(fg_icon, move || item.icon),
178                            )),
179                            // 8dp gap: 4dp IndicatorVerticalPadding + 4dp IndicatorToLabelPadding
180                            Box(Modifier::new().height(8.0)),
181                            Text(item.label)
182                                .color(fg_label)
183                                .size(th.typography.label_medium)
184                                .single_line(),
185                        )),
186                    )
187                })
188                .collect::<Vec<_>>(),
189        ),
190    )
191}
192
193pub struct NavItem {
194    pub icon: View,
195    pub label: String,
196    pub on_click: Rc<dyn Fn()>,
197    pub enabled: bool,
198    pub interaction_source: Option<MutableInteractionSource>,
199}