repose_material/material3/
nav_bar.rs1#![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, ViewExt,
10 anim::{animate_color, animate_f32},
11};
12
13use crate::ripple::{RippleConfig, ripple};
14
15use super::util::apply_m3_clickable_without_indication;
16use super::*;
17
18static NAVBAR_COUNTER: AtomicU64 = AtomicU64::new(0);
19
20#[derive(Clone, Debug)]
22pub struct NavigationBarConfig {
23 pub modifier: Modifier,
24 pub container_color: Color,
25 pub content_color: Color,
26 pub selected_icon_color: Color,
27 pub selected_text_color: Color,
28 pub unselected_icon_color: Color,
29 pub unselected_text_color: Color,
30 pub indicator_color: Color,
31 pub height: f32,
32 pub tonal_elevation: f32,
33 pub indicator_opacity: f32,
34 pub indicator_radius: f32,
35 pub item_spacing: f32,
36 pub indicator_width: f32,
37 pub indicator_height: f32,
38}
39
40impl Default for NavigationBarConfig {
41 fn default() -> Self {
42 Self {
43 modifier: Modifier::new(),
44 container_color: NavigationBarDefaults::container_color(),
45 content_color: NavigationBarDefaults::content_color(),
46 selected_icon_color: NavigationBarDefaults::selected_icon_color(),
47 selected_text_color: NavigationBarDefaults::selected_text_color(),
48 unselected_icon_color: NavigationBarDefaults::unselected_icon_color(),
49 unselected_text_color: NavigationBarDefaults::unselected_text_color(),
50 indicator_color: NavigationBarDefaults::indicator_color(),
51 height: NavigationBarDefaults::HEIGHT,
52 tonal_elevation: NavigationBarDefaults::TONAL_ELEVATION,
53 indicator_opacity: NavigationBarDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
54 indicator_radius: NavigationBarDefaults::INDICATOR_RADIUS,
55 item_spacing: NavigationBarDefaults::ITEM_SPACING,
56 indicator_width: NavigationBarDefaults::ACTIVE_INDICATOR_WIDTH,
57 indicator_height: NavigationBarDefaults::ACTIVE_INDICATOR_HEIGHT,
58 }
59 }
60}
61
62pub fn NavigationBar(
65 selected_index: usize,
66 items: Vec<NavItem>,
67 config: NavigationBarConfig,
68) -> View {
69 let th = theme();
70 let id = remember(|| NAVBAR_COUNTER.fetch_add(1, Ordering::Relaxed));
71
72 let mut bar_m = Modifier::new()
73 .fill_max_size()
74 .min_height(config.height)
75 .background(config.container_color)
76 .then(config.modifier);
77
78 if config.tonal_elevation > 0.0 {
79 bar_m = bar_m.state_elevation(StateElevation {
80 default: config.tonal_elevation,
81 hovered: config.tonal_elevation,
82 focused: config.tonal_elevation,
83 pressed: config.tonal_elevation,
84 dragged: config.tonal_elevation,
85 disabled: 0.0,
86 });
87 }
88
89 Box(bar_m).child(
90 Row(Modifier::new()
91 .fill_max_size()
92 .align_items(AlignItems::CENTER)
93 .column_gap(config.item_spacing)
94 .semantics(Semantics::new(Role::Container).with_selectable_group()))
95 .child(
96 items
97 .into_iter()
98 .enumerate()
99 .map(|(i, item)| {
100 let selected = i == selected_index;
101 let is_enabled = item.enabled;
102 let default_effects = AnimationSpec::spring_crit(40.0);
103 let fg_icon = animate_color(
104 format!("nb_fi_{}_{}", id, i),
105 if selected {
106 config.selected_icon_color
107 } else {
108 config.unselected_icon_color
109 },
110 default_effects,
111 );
112 let fg_label = animate_color(
113 format!("nb_fl_{}_{}", id, i),
114 if selected {
115 config.selected_text_color
116 } else {
117 config.unselected_text_color
118 },
119 default_effects,
120 );
121 let bg_alpha = animate_f32(
122 format!("nb_bg_{}_{}", id, i),
123 if selected { 1.0 } else { 0.0 },
124 default_effects,
125 );
126 let indicator_bg = config
127 .indicator_color
128 .with_alpha_f32(bg_alpha * config.indicator_opacity);
129 let cb = item.on_click.clone();
130 let nb_source: Rc<MutableInteractionSource> = item
131 .interaction_source
132 .clone()
133 .map(Rc::new)
134 .unwrap_or_else(|| remember(MutableInteractionSource::new));
135
136 let item_width = remember_state_with_key(
137 format!("nb_w_{}_{}", id, i),
138 || 0.0f32,
139 );
140 let mut item_m = Modifier::new()
141 .flex_grow(1.0)
142 .fill_max_height()
143 .propagate_min_constraints(true)
144 .on_size_changed({
145 let w = item_width.clone();
146 move |size| *w.borrow_mut() = size.x
147 })
148 .semantics(Semantics::new(Role::Tab).with_label(&item.label));
149
150 item_m = apply_m3_clickable_without_indication(
151 item_m, &nb_source, is_enabled, {
152 let cb = cb.clone();
153 move || cb()
154 },
155 );
156
157 let pill_dx = (*item_width.borrow() - config.indicator_width) / 2.0;
160 let pill_dy = 14.0;
161 let pill_m = Modifier::new()
162 .absolute()
163 .offset(
164 Some((24.0 - config.indicator_width) / 2.0),
165 Some((24.0 - config.indicator_height) / 2.0),
166 None,
167 None,
168 )
169 .width(config.indicator_width)
170 .height(config.indicator_height)
171 .clip_rounded(config.indicator_radius)
172 .interaction_source(&nb_source)
173 .indication(ripple(RippleConfig {
174 color: Some(theme().on_surface_variant),
175 bounded: true,
176 press_offset: if *item_width.borrow() > 0.0 {
177 Some(Vec2 {
178 x: pill_dx,
179 y: pill_dy,
180 })
181 } else {
182 None
183 },
184 ..Default::default()
185 }));
186 let bg_m = Modifier::new()
187 .absolute()
188 .offset(
189 Some((24.0 - config.indicator_width) / 2.0),
190 Some((24.0 - config.indicator_height) / 2.0),
191 None,
192 None,
193 )
194 .width(config.indicator_width)
195 .height(config.indicator_height)
196 .background(indicator_bg)
197 .clip_rounded(config.indicator_radius)
198 .state_colors(StateColors {
199 default: Color::TRANSPARENT,
200 hovered: Color::TRANSPARENT,
201 focused: Color::TRANSPARENT,
202 pressed: Color::TRANSPARENT,
203 dragged: th.on_surface.with_alpha_f32(0.12),
204 disabled: Color::TRANSPARENT,
205 });
206
207 Box(item_m).child(
208 Column(
209 Modifier::new()
210 .fill_max_size()
211 .align_items(AlignItems::CENTER)
212 .justify_content(JustifyContent::CENTER),
213 )
214 .child((
215 Column(
216 Modifier::new()
217 .align_items(AlignItems::CENTER)
218 .justify_content(JustifyContent::CENTER),
219 )
220 .child((
221 Box(bg_m),
222 with_content_color(fg_icon, move || item.icon),
223 Box(pill_m),
224 )),
225 Box(Modifier::new().height(8.0)),
227 Text(item.label)
228 .color(fg_label)
229 .size(th.typography.label_medium)
230 .single_line(),
231 )),
232 )
233 })
234 .collect::<Vec<_>>(),
235 ),
236 )
237}
238
239pub struct NavItem {
240 pub icon: View,
241 pub label: String,
242 pub on_click: Rc<dyn Fn()>,
243 pub enabled: bool,
244 pub interaction_source: Option<MutableInteractionSource>,
245}