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