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: th.on_surface.with_alpha_f32(0.08),
171 pressed: th.on_surface.with_alpha_f32(0.12),
172 dragged: th.on_surface.with_alpha_f32(0.12),
173 disabled: Color::TRANSPARENT,
174 })
175 .clip_rounded(config.item_radius)
176 .interaction_source(&nr_source)
177 .semantics(Semantics::new(Role::Tab).with_label(&item.label));
178
179 if is_enabled {
180 item_m = item_m.clickable().on_click({
181 let cb = cb.clone();
182 move || cb()
183 });
184 }
185
186 item_views.push(
187 Column(item_m).child((
188 Column(Modifier::new()).child((
189 Box(Modifier::new().size(24.0, 24.0))
190 .child(with_content_color(fg, move || item.icon)),
191 item.badge
192 .map(|b| {
193 Box(Modifier::new()
194 .absolute()
195 .offset(None, None, None, Some(0.0)))
196 .child(b)
197 })
198 .unwrap_or(Box(Modifier::new())),
199 )),
200 Box(Modifier::new().fill_max_width().height(4.0)),
201 Text(item.label)
202 .color(fg_label)
203 .size(th.typography.label_medium)
204 .single_line(),
205 )),
206 );
207 }
208
209 Column(
210 Modifier::new()
211 .width(config.width)
212 .fill_max_height()
213 .background(config.container_color)
214 .align_items(AlignItems::CENTER)
215 .semantics(Semantics::new(Role::Container).with_selectable_group())
216 .then(config.modifier),
217 )
218 .child((
219 Column(Modifier::new()).with_children(top_children),
220 Box(Modifier::new().flex_grow(1.0)).child(
221 Column(
222 Modifier::new()
223 .fill_max_size()
224 .justify_content(JustifyContent::SPACE_BETWEEN)
225 .align_items(AlignItems::CENTER),
226 )
227 .with_children(item_views),
228 ),
229 ))
230}