repose_material/material3/
nav_rail.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, Text, TextStyle,
10 ViewExt,
11 anim::animate_color,
12};
13
14use super::*;
15
16#[derive(Clone, Debug)]
18pub struct NavigationRailConfig {
19 pub modifier: Modifier,
20 pub container_color: Color,
21 pub selected_icon_color: Color,
22 pub selected_text_color: Color,
23 pub unselected_icon_color: Color,
24 pub unselected_text_color: Color,
25 pub indicator_color: Color,
26 pub width: f32,
27 pub item_radius: f32,
28 pub indicator_opacity: f32,
29 pub item_spacing: f32,
30 pub indicator_width: f32,
31 pub indicator_height: f32,
32}
33
34impl Default for NavigationRailConfig {
35 fn default() -> Self {
36 Self {
37 modifier: Modifier::new(),
38 container_color: NavigationRailDefaults::container_color(),
39 selected_icon_color: NavigationRailDefaults::selected_icon_color(),
40 selected_text_color: NavigationRailDefaults::selected_text_color(),
41 unselected_icon_color: NavigationRailDefaults::unselected_icon_color(),
42 unselected_text_color: NavigationRailDefaults::unselected_text_color(),
43 indicator_color: NavigationRailDefaults::indicator_color(),
44 width: NavigationRailDefaults::WIDTH,
45 item_radius: NavigationRailDefaults::ITEM_RADIUS,
46 indicator_opacity: NavigationRailDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
47 item_spacing: NavigationRailDefaults::ITEM_SPACING,
48 indicator_width: NavigationRailDefaults::ACTIVE_INDICATOR_WIDTH,
49 indicator_height: NavigationRailDefaults::ACTIVE_INDICATOR_HEIGHT,
50 }
51 }
52}
53
54
55pub struct NavRailItem {
57 pub icon: View,
58 pub label: String,
59 pub on_click: Rc<dyn Fn()>,
60 pub badge: Option<View>,
61 pub enabled: bool,
62 pub interaction_source: Option<MutableInteractionSource>,
63}
64
65static NAVRAIL_COUNTER: AtomicU64 = AtomicU64::new(0);
66
67pub fn NavigationRail(
72 selected_index: usize,
73 items: Vec<NavRailItem>,
74 header: Option<View>,
75 fab: Option<View>,
76 config: NavigationRailConfig,
77) -> View {
78 let th = theme();
79 let id = remember(|| NAVRAIL_COUNTER.fetch_add(1, Ordering::Relaxed));
80 let default_effects = AnimationSpec::spring_crit(40.0);
81
82 let mut top_children: Vec<View> = Vec::new();
83 let mut item_views: Vec<View> = Vec::new();
84
85 let has_header = header.is_some();
86 let has_fab = fab.is_some();
87
88 if let Some(h) = header {
89 top_children.push(
90 Box(Modifier::new()
91 .padding_values(PaddingValues {
92 left: 12.0,
93 right: 12.0,
94 top: 12.0,
95 bottom: 12.0,
96 })
97 .align_self(AlignSelf::CENTER))
98 .child(h),
99 );
100 }
101
102 if let Some(f) = fab {
103 top_children.push(
104 Box(Modifier::new()
105 .padding_values(PaddingValues {
106 left: 12.0,
107 right: 12.0,
108 top: 8.0,
109 bottom: 8.0,
110 })
111 .align_self(AlignSelf::CENTER))
112 .child(f),
113 );
114 }
115
116 if has_header || has_fab {
117 top_children.push(Box(Modifier::new()
118 .fill_max_width()
119 .height(1.0)
120 .background(th.outline_variant)));
121 }
122
123 for (i, item) in items.into_iter().enumerate() {
124 let selected = i == selected_index;
125 let is_enabled = item.enabled;
126
127 let fg = animate_color(
128 format!("nr_fg_{}_{}", id, i),
129 if selected {
130 config.selected_icon_color
131 } else {
132 config.unselected_icon_color
133 },
134 default_effects,
135 );
136 let fg_label = animate_color(
137 format!("nr_fl_{}_{}", id, i),
138 if selected {
139 config.selected_text_color
140 } else {
141 config.unselected_text_color
142 },
143 default_effects,
144 );
145 let bg = animate_color(
146 format!("nr_bg_{}_{}", id, i),
147 if selected {
148 config.indicator_color
149 } else {
150 Color::TRANSPARENT
151 },
152 default_effects,
153 );
154
155 let cb = item.on_click.clone();
156 let nr_source: Rc<MutableInteractionSource> = item
157 .interaction_source
158 .clone()
159 .map(Rc::new)
160 .unwrap_or_else(|| remember(MutableInteractionSource::new));
161
162 let mut item_m = Modifier::new()
163 .fill_max_width()
164 .padding_values(PaddingValues {
165 left: 4.0,
166 right: 4.0,
167 top: 4.0,
168 bottom: 4.0,
169 })
170 .align_items(AlignItems::CENTER)
171 .justify_content(JustifyContent::CENTER)
172 .background(bg)
173 .state_colors(StateColors {
174 default: Color::TRANSPARENT,
175 hovered: th.on_surface.with_alpha_f32(0.08),
176 pressed: th.on_surface.with_alpha_f32(0.12),
177 disabled: Color::TRANSPARENT,
178 })
179 .clip_rounded(config.item_radius)
180 .interaction_source(&*nr_source)
181 .semantics(Semantics::new(Role::Tab).with_label(&item.label));
182
183 if is_enabled {
184 item_m = item_m.clickable().on_click({
185 let cb = cb.clone();
186 move || cb()
187 });
188 }
189
190 item_views.push(
191 Column(item_m).child((
192 Column(Modifier::new()).child((
193 Box(Modifier::new().size(24.0, 24.0))
194 .child(with_content_color(fg, move || item.icon)),
195 item.badge
196 .map(|b| {
197 Box(Modifier::new()
198 .absolute()
199 .offset(None, None, None, Some(0.0)))
200 .child(b)
201 })
202 .unwrap_or(Box(Modifier::new())),
203 )),
204 Box(Modifier::new().fill_max_width().height(4.0)),
205 Text(item.label)
206 .color(fg_label)
207 .size(th.typography.label_medium)
208 .single_line(),
209 )),
210 );
211 }
212
213 Column(
214 Modifier::new()
215 .width(config.width)
216 .fill_max_height()
217 .background(config.container_color)
218 .align_items(AlignItems::CENTER)
219 .semantics(Semantics::new(Role::Container).with_selectable_group())
220 .then(config.modifier),
221 )
222 .child((
223 Column(Modifier::new()).with_children(top_children),
224 Box(Modifier::new().flex_grow(1.0)).child(
225 Column(
226 Modifier::new()
227 .fill_max_size()
228 .justify_content(JustifyContent::SPACE_BETWEEN)
229 .align_items(AlignItems::CENTER),
230 )
231 .with_children(item_views),
232 ),
233 ))
234}