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 pressed: config.tonal_elevation,
94 dragged: config.tonal_elevation,
95 disabled: 0.0,
96 });
97 }
98
99 ZStack(Modifier::new().fill_max_size()).child((
100 Box(Modifier::new()
101 .fill_max_size()
102 .background(config.content_color))
103 .child(content),
104 if drawer_state.is_open() {
105 Box(Modifier::new()
106 .fill_max_size()
107 .background(config.scrim_color)
108 .clickable()
109 .on_pointer_down({
110 let ds = drawer_state.clone();
111 move |_| ds.dismiss()
112 }))
113 .child(Box(Modifier::new()))
114 } else {
115 Box(Modifier::new())
116 },
117 Box(drawer_m).child(drawer_content),
118 ))
119}
120
121pub fn DismissibleNavigationDrawer(
124 drawer_state: Rc<DrawerState>,
125 drawer_content: View,
126 content: View,
127 config: NavigationDrawerConfig,
128) -> View {
129 let _th = theme();
130 let drawer_offset = animate_f32(
131 "dismissible_drawer_offset",
132 if drawer_state.is_open() { 0.0 } else { -360.0 },
133 theme().motion.spring,
134 );
135
136 let mut drawer_m = Modifier::new()
137 .absolute()
138 .offset(Some(drawer_offset), Some(0.0), None, Some(0.0))
139 .fill_max_height()
140 .width(config.width)
141 .background(config.container_color)
142 .clip_rounded(config.shape_radius);
143
144 if config.tonal_elevation > 0.0 {
145 drawer_m = drawer_m.state_elevation(StateElevation {
146 default: config.tonal_elevation,
147 hovered: config.tonal_elevation,
148 pressed: config.tonal_elevation,
149 dragged: config.tonal_elevation,
150 disabled: 0.0,
151 });
152 }
153
154 ZStack(Modifier::new().fill_max_size()).child((
155 Box(Modifier::new()
156 .fill_max_size()
157 .background(config.content_color))
158 .child(content),
159 Box(drawer_m).child(drawer_content),
160 ))
161}
162
163pub fn PermanentNavigationDrawer(
165 drawer_content: View,
166 content: View,
167 config: NavigationDrawerConfig,
168) -> View {
169 Row(Modifier::new().fill_max_size()).child((
170 Box(Modifier::new()
171 .width(config.width)
172 .fill_max_height()
173 .background(config.container_color))
174 .child(
175 Box(Modifier::new())
176 .color(config.content_color)
177 .child(drawer_content),
178 ),
179 Box(Modifier::new().flex_grow(1.0)).child(content),
180 ))
181}
182
183#[derive(Clone)]
185pub struct NavigationDrawerItemConfig {
186 pub modifier: Modifier,
187 pub icon: Option<View>,
188 pub badge: Option<View>,
189 pub enabled: bool,
190 pub shape_radius: f32,
191 pub interaction_source: Option<MutableInteractionSource>,
192}
193
194impl Default for NavigationDrawerItemConfig {
195 fn default() -> Self {
196 Self {
197 modifier: Modifier::new(),
198 icon: None,
199 badge: None,
200 enabled: true,
201 shape_radius: repose_core::locals::theme().shapes.large,
202 interaction_source: None,
203 }
204 }
205}
206
207pub fn NavigationDrawerItem(
208 label: View,
209 selected: bool,
210 on_click: impl Fn() + 'static,
211 config: NavigationDrawerItemConfig,
212) -> View {
213 let th = theme();
214 let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
215 let spec = th.motion.color;
216 let bg = animate_color(
217 format!("ndi_bg_{}", id),
218 if selected {
219 th.secondary_container
220 } else {
221 Color::TRANSPARENT
222 },
223 spec,
224 );
225 let fg = animate_color(
226 format!("ndi_fg_{}", id),
227 if selected {
228 th.on_secondary_container
229 } else {
230 th.on_surface_variant
231 },
232 spec,
233 );
234
235 let nd_source: Rc<MutableInteractionSource> = config
236 .interaction_source
237 .clone()
238 .map(Rc::new)
239 .unwrap_or_else(|| remember(MutableInteractionSource::new));
240
241 let mut m = Modifier::new()
242 .fill_max_width()
243 .padding_values(PaddingValues {
244 left: 12.0,
245 right: 12.0,
246 top: 0.0,
247 bottom: 0.0,
248 })
249 .min_height(56.0)
250 .background(bg)
251 .state_colors(StateColors {
252 default: Color::TRANSPARENT,
253 hovered: th.on_surface.with_alpha_f32(0.08),
254 pressed: th.on_surface.with_alpha_f32(0.12),
255 dragged: th.on_surface.with_alpha_f32(0.12),
256 disabled: Color::TRANSPARENT,
257 })
258 .clip_rounded(config.shape_radius)
259 .interaction_source(&nd_source)
260 .then(config.modifier);
261
262 if config.enabled {
263 m = m.clickable().on_click(on_click);
264 }
265
266 Box(m).child(with_content_color(fg, || {
267 Row(Modifier::new()
268 .align_items(AlignItems::CENTER)
269 .padding_values(PaddingValues {
270 left: 16.0,
271 right: 24.0,
272 top: 0.0,
273 bottom: 0.0,
274 }))
275 .child((
276 config
277 .icon
278 .unwrap_or(Box(Modifier::new().width(24.0).height(24.0))),
279 Box(Modifier::new().width(12.0).height(1.0)),
280 Box(Modifier::new().flex_grow(1.0)).child(label),
281 config.badge.unwrap_or(Box(Modifier::new())),
282 ))
283 }))
284}