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::util::apply_m3_clickable;
14use super::*;
15
16static NAVBAR_COUNTER: AtomicU64 = AtomicU64::new(0);
17
18#[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
60pub 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 focused: config.tonal_elevation,
81 pressed: config.tonal_elevation,
82 dragged: config.tonal_elevation,
83 disabled: 0.0,
84 });
85 }
86
87 Box(bar_m).child(
88 Row(Modifier::new()
89 .fill_max_size()
90 .align_items(AlignItems::CENTER)
91 .column_gap(config.item_spacing)
92 .semantics(Semantics::new(Role::Container).with_selectable_group()))
93 .child(
94 items
95 .into_iter()
96 .enumerate()
97 .map(|(i, item)| {
98 let selected = i == selected_index;
99 let is_enabled = item.enabled;
100 let default_effects = AnimationSpec::spring_crit(40.0);
101 let fg_icon = animate_color(
102 format!("nb_fi_{}_{}", id, i),
103 if selected {
104 config.selected_icon_color
105 } else {
106 config.unselected_icon_color
107 },
108 default_effects,
109 );
110 let fg_label = animate_color(
111 format!("nb_fl_{}_{}", id, i),
112 if selected {
113 config.selected_text_color
114 } else {
115 config.unselected_text_color
116 },
117 default_effects,
118 );
119 let bg_alpha = animate_f32(
120 format!("nb_bg_{}_{}", id, i),
121 if selected { 1.0 } else { 0.0 },
122 default_effects,
123 );
124 let indicator_bg = config
125 .indicator_color
126 .with_alpha_f32(bg_alpha * config.indicator_opacity);
127 let cb = item.on_click.clone();
128 let nb_source: Rc<MutableInteractionSource> = item
129 .interaction_source
130 .clone()
131 .map(Rc::new)
132 .unwrap_or_else(|| remember(MutableInteractionSource::new));
133
134 let mut item_m = Modifier::new()
135 .flex_grow(1.0)
136 .semantics(Semantics::new(Role::Tab).with_label(&item.label));
137
138 item_m = apply_m3_clickable(
139 item_m,
140 &nb_source,
141 theme().on_surface_variant,
142 is_enabled,
143 {
144 let cb = cb.clone();
145 move || cb()
146 },
147 );
148
149 Box(item_m).child(
150 Column(
151 Modifier::new()
152 .fill_max_size()
153 .align_items(AlignItems::CENTER)
154 .justify_content(JustifyContent::CENTER),
155 )
156 .child((
157 Column(
159 Modifier::new()
160 .align_items(AlignItems::CENTER)
161 .justify_content(JustifyContent::CENTER),
162 )
163 .child((
164 Box(Modifier::new()
165 .absolute()
166 .offset(
167 Some((24.0 - config.indicator_width) / 2.0),
168 Some((24.0 - config.indicator_height) / 2.0),
169 None,
170 None,
171 )
172 .width(config.indicator_width)
173 .height(config.indicator_height)
174 .background(indicator_bg)
175 .clip_rounded(config.indicator_radius)
176 .state_colors(StateColors {
177 default: Color::TRANSPARENT,
178 hovered: Color::TRANSPARENT,
179 focused: Color::TRANSPARENT,
180 pressed: Color::TRANSPARENT,
181 dragged: th.on_surface.with_alpha_f32(0.12),
182 disabled: Color::TRANSPARENT,
183 })),
184 with_content_color(fg_icon, move || item.icon),
185 )),
186 Box(Modifier::new().height(8.0)),
188 Text(item.label)
189 .color(fg_label)
190 .size(th.typography.label_medium)
191 .single_line(),
192 )),
193 )
194 })
195 .collect::<Vec<_>>(),
196 ),
197 )
198}
199
200pub struct NavItem {
201 pub icon: View,
202 pub label: String,
203 pub on_click: Rc<dyn Fn()>,
204 pub enabled: bool,
205 pub interaction_source: Option<MutableInteractionSource>,
206}