repose_material/material3/
nav_drawer.rs1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4use std::sync::atomic::Ordering;
5
6use repose_core::*;
7use repose_ui::{
8 Box, Row, TextStyle, ViewExt, ZStack,
9 anim::{animate_color, animate_f32},
10};
11
12use super::util::FILTERCHIP_COUNTER;
13use super::*;
14
15#[derive(Clone, Debug)]
17pub struct NavigationDrawerConfig {
18 pub modifier: Modifier,
19 pub container_color: Color,
20 pub content_color: Color,
21 pub scrim_color: Color,
22 pub tonal_elevation: f32,
23 pub width: f32,
24 pub shape_radius: f32,
25}
26
27impl Default for NavigationDrawerConfig {
28 fn default() -> Self {
29 Self {
30 modifier: Modifier::new(),
31 container_color: NavigationDrawerDefaults::container_color(),
32 content_color: NavigationDrawerDefaults::content_color(),
33 scrim_color: NavigationDrawerDefaults::scrim_color(),
34 tonal_elevation: NavigationDrawerDefaults::TONAL_ELEVATION,
35 width: NavigationDrawerDefaults::WIDTH,
36 shape_radius: NavigationDrawerDefaults::SHAPE_RADIUS,
37 }
38 }
39}
40
41pub struct DrawerState {
43 visible: Signal<bool>,
44}
45
46impl DrawerState {
47 pub fn new() -> Rc<Self> {
48 Rc::new(Self {
49 visible: signal(false),
50 })
51 }
52
53 pub fn is_open(&self) -> bool {
54 self.visible.get()
55 }
56
57 pub fn open(&self) {
58 self.visible.set(true);
59 }
60
61 pub fn dismiss(&self) {
62 self.visible.set(false);
63 }
64}
65
66pub fn ModalNavigationDrawer(
68 drawer_state: Rc<DrawerState>,
69 drawer_content: View,
70 content: View,
71 config: NavigationDrawerConfig,
72) -> View {
73 let _th = theme();
74
75 let drawer_offset = animate_f32(
76 "modal_drawer_offset",
77 if drawer_state.is_open() { 0.0 } else { -360.0 },
78 theme().motion.spring,
79 );
80
81 let mut drawer_m = Modifier::new()
82 .absolute()
83 .offset(Some(drawer_offset), Some(0.0), None, Some(0.0))
84 .fill_max_height()
85 .width(config.width)
86 .background(config.container_color)
87 .clip_rounded(config.shape_radius);
88
89 if config.tonal_elevation > 0.0 {
90 drawer_m = drawer_m.state_elevation(StateElevation {
91 default: config.tonal_elevation,
92 hovered: config.tonal_elevation,
93 focused: config.tonal_elevation,
94 pressed: config.tonal_elevation,
95 dragged: config.tonal_elevation,
96 disabled: 0.0,
97 });
98 }
99
100 ZStack(Modifier::new().fill_max_size()).child((
101 Box(Modifier::new()
102 .fill_max_size()
103 .background(config.content_color))
104 .child(content),
105 if drawer_state.is_open() {
106 Box(Modifier::new()
107 .fill_max_size()
108 .background(config.scrim_color)
109 .clickable()
110 .on_pointer_down({
111 let ds = drawer_state.clone();
112 move |_| ds.dismiss()
113 }))
114 .child(Box(Modifier::new()))
115 } else {
116 Box(Modifier::new())
117 },
118 Box(drawer_m).child(drawer_content),
119 ))
120}
121
122pub fn DismissibleNavigationDrawer(
125 drawer_state: Rc<DrawerState>,
126 drawer_content: View,
127 content: View,
128 config: NavigationDrawerConfig,
129) -> View {
130 let _th = theme();
131 let drawer_offset = animate_f32(
132 "dismissible_drawer_offset",
133 if drawer_state.is_open() { 0.0 } else { -360.0 },
134 theme().motion.spring,
135 );
136
137 let mut drawer_m = Modifier::new()
138 .absolute()
139 .offset(Some(drawer_offset), Some(0.0), None, Some(0.0))
140 .fill_max_height()
141 .width(config.width)
142 .background(config.container_color)
143 .clip_rounded(config.shape_radius);
144
145 if config.tonal_elevation > 0.0 {
146 drawer_m = drawer_m.state_elevation(StateElevation {
147 default: config.tonal_elevation,
148 hovered: config.tonal_elevation,
149 focused: config.tonal_elevation,
150 pressed: config.tonal_elevation,
151 dragged: config.tonal_elevation,
152 disabled: 0.0,
153 });
154 }
155
156 ZStack(Modifier::new().fill_max_size()).child((
157 Box(Modifier::new()
158 .fill_max_size()
159 .background(config.content_color))
160 .child(content),
161 Box(drawer_m).child(drawer_content),
162 ))
163}
164
165pub fn PermanentNavigationDrawer(
167 drawer_content: View,
168 content: View,
169 config: NavigationDrawerConfig,
170) -> View {
171 Row(Modifier::new().fill_max_size()).child((
172 Box(Modifier::new()
173 .width(config.width)
174 .fill_max_height()
175 .background(config.container_color))
176 .child(
177 Box(Modifier::new())
178 .color(config.content_color)
179 .child(drawer_content),
180 ),
181 Box(Modifier::new().flex_grow(1.0)).child(content),
182 ))
183}
184
185#[derive(Clone)]
187pub struct NavigationDrawerItemConfig {
188 pub modifier: Modifier,
189 pub icon: Option<View>,
190 pub badge: Option<View>,
191 pub enabled: bool,
192 pub shape_radius: f32,
193 pub interaction_source: Option<MutableInteractionSource>,
194}
195
196impl Default for NavigationDrawerItemConfig {
197 fn default() -> Self {
198 Self {
199 modifier: Modifier::new(),
200 icon: None,
201 badge: None,
202 enabled: true,
203 shape_radius: repose_core::locals::theme().shapes.large,
204 interaction_source: None,
205 }
206 }
207}
208
209pub fn NavigationDrawerItem(
210 label: View,
211 selected: bool,
212 on_click: impl Fn() + 'static,
213 config: NavigationDrawerItemConfig,
214) -> View {
215 let th = theme();
216 let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
217 let spec = th.motion.color;
218 let bg = animate_color(
219 format!("ndi_bg_{}", id),
220 if selected {
221 th.secondary_container
222 } else {
223 Color::TRANSPARENT
224 },
225 spec,
226 );
227 let fg = animate_color(
228 format!("ndi_fg_{}", id),
229 if selected {
230 th.on_secondary_container
231 } else {
232 th.on_surface_variant
233 },
234 spec,
235 );
236
237 let nd_source: Rc<MutableInteractionSource> = config
238 .interaction_source
239 .clone()
240 .map(Rc::new)
241 .unwrap_or_else(|| remember(MutableInteractionSource::new));
242
243 let mut m = Modifier::new()
244 .fill_max_width()
245 .padding_values(PaddingValues {
246 left: 12.0,
247 right: 12.0,
248 top: 0.0,
249 bottom: 0.0,
250 })
251 .min_height(56.0)
252 .background(bg)
253 .state_colors(StateColors {
254 default: Color::TRANSPARENT,
255 hovered: Color::TRANSPARENT,
256 focused: Color::TRANSPARENT,
257 pressed: Color::TRANSPARENT,
258 dragged: th.on_surface.with_alpha_f32(0.12),
259 disabled: Color::TRANSPARENT,
260 })
261 .clip_rounded(config.shape_radius)
262 .interaction_source(&nd_source)
263 .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
264 color: Some(th.on_surface),
265 bounded: true,
266 ..Default::default()
267 }))
268 .then(config.modifier);
269
270 if config.enabled {
271 m = m.clickable().on_click(on_click);
272 }
273
274 Box(m).child(with_content_color(fg, || {
275 Row(Modifier::new()
276 .align_items(AlignItems::CENTER)
277 .padding_values(PaddingValues {
278 left: 16.0,
279 right: 24.0,
280 top: 0.0,
281 bottom: 0.0,
282 }))
283 .child((
284 config
285 .icon
286 .unwrap_or(Box(Modifier::new().width(24.0).height(24.0))),
287 Box(Modifier::new().width(12.0).height(1.0)),
288 Box(Modifier::new().flex_grow(1.0)).child(label),
289 config.badge.unwrap_or(Box(Modifier::new())),
290 ))
291 }))
292}