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