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