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