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,
9 ViewExt, ZStack,
10 anim::{animate_color, animate_f32},
11};
12
13use super::*;
14use super::util::FILTERCHIP_COUNTER;
15
16#[derive(Clone, Debug)]
18pub struct NavigationDrawerConfig {
19 pub modifier: Modifier,
20 pub container_color: Color,
21 pub content_color: Color,
22 pub scrim_color: Color,
23 pub tonal_elevation: f32,
24 pub width: f32,
25 pub shape_radius: f32,
26}
27
28impl Default for NavigationDrawerConfig {
29 fn default() -> Self {
30 Self {
31 modifier: Modifier::new(),
32 container_color: NavigationDrawerDefaults::container_color(),
33 content_color: NavigationDrawerDefaults::content_color(),
34 scrim_color: NavigationDrawerDefaults::scrim_color(),
35 tonal_elevation: NavigationDrawerDefaults::TONAL_ELEVATION,
36 width: NavigationDrawerDefaults::WIDTH,
37 shape_radius: NavigationDrawerDefaults::SHAPE_RADIUS,
38 }
39 }
40}
41
42pub struct DrawerState {
44 visible: Signal<bool>,
45}
46
47impl DrawerState {
48 pub fn new() -> Rc<Self> {
49 Rc::new(Self {
50 visible: signal(false),
51 })
52 }
53
54 pub fn is_open(&self) -> bool {
55 self.visible.get()
56 }
57
58 pub fn open(&self) {
59 self.visible.set(true);
60 }
61
62 pub fn dismiss(&self) {
63 self.visible.set(false);
64 }
65}
66
67pub fn ModalNavigationDrawer(
69 drawer_state: Rc<DrawerState>,
70 drawer_content: View,
71 content: View,
72 config: NavigationDrawerConfig,
73) -> View {
74 let _th = theme();
75
76 let drawer_offset = animate_f32(
77 "modal_drawer_offset",
78 if drawer_state.is_open() { 0.0 } else { -360.0 },
79 theme().motion.spring,
80 );
81
82 let mut drawer_m = Modifier::new()
83 .absolute()
84 .offset(Some(drawer_offset), Some(0.0), None, Some(0.0))
85 .fill_max_height()
86 .width(config.width)
87 .background(config.container_color)
88 .clip_rounded(config.shape_radius);
89
90 if config.tonal_elevation > 0.0 {
91 drawer_m = drawer_m.state_elevation(StateElevation {
92 default: config.tonal_elevation,
93 hovered: config.tonal_elevation,
94 pressed: 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 disabled: 0.0,
150 });
151 }
152
153 ZStack(Modifier::new().fill_max_size()).child((
154 Box(Modifier::new()
155 .fill_max_size()
156 .background(config.content_color))
157 .child(content),
158 Box(drawer_m).child(drawer_content),
159 ))
160}
161
162pub fn PermanentNavigationDrawer(
164 drawer_content: View,
165 content: View,
166 config: NavigationDrawerConfig,
167) -> View {
168 Row(Modifier::new().fill_max_size()).child((
169 Box(Modifier::new()
170 .width(config.width)
171 .fill_max_height()
172 .background(config.container_color))
173 .child(
174 Box(Modifier::new())
175 .color(config.content_color)
176 .child(drawer_content),
177 ),
178 Box(Modifier::new().flex_grow(1.0)).child(content),
179 ))
180}
181
182#[derive(Clone)]
184pub struct NavigationDrawerItemConfig {
185 pub modifier: Modifier,
186 pub icon: Option<View>,
187 pub badge: Option<View>,
188 pub enabled: bool,
189 pub shape_radius: f32,
190 pub interaction_source: Option<MutableInteractionSource>,
191}
192
193impl Default for NavigationDrawerItemConfig {
194 fn default() -> Self {
195 Self {
196 modifier: Modifier::new(),
197 icon: None,
198 badge: None,
199 enabled: true,
200 shape_radius: repose_core::locals::theme().shapes.large,
201 interaction_source: None,
202 }
203 }
204}
205
206pub fn NavigationDrawerItem(
207 label: View,
208 selected: bool,
209 on_click: impl Fn() + 'static,
210 config: NavigationDrawerItemConfig,
211) -> View {
212 let th = theme();
213 let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
214 let spec = th.motion.color;
215 let bg = animate_color(
216 format!("ndi_bg_{}", id),
217 if selected {
218 th.secondary_container
219 } else {
220 Color::TRANSPARENT
221 },
222 spec,
223 );
224 let fg = animate_color(
225 format!("ndi_fg_{}", id),
226 if selected {
227 th.on_secondary_container
228 } else {
229 th.on_surface_variant
230 },
231 spec,
232 );
233
234 let nd_source: Rc<MutableInteractionSource> = config
235 .interaction_source
236 .clone()
237 .map(Rc::new)
238 .unwrap_or_else(|| remember(MutableInteractionSource::new));
239
240 let mut m = Modifier::new()
241 .fill_max_width()
242 .padding_values(PaddingValues {
243 left: 12.0,
244 right: 12.0,
245 top: 0.0,
246 bottom: 0.0,
247 })
248 .min_height(56.0)
249 .background(bg)
250 .state_colors(StateColors {
251 default: Color::TRANSPARENT,
252 hovered: th.on_surface.with_alpha_f32(0.08),
253 pressed: th.on_surface.with_alpha_f32(0.12),
254 disabled: Color::TRANSPARENT,
255 })
256 .clip_rounded(config.shape_radius)
257 .interaction_source(&*nd_source)
258 .then(config.modifier);
259
260 if config.enabled {
261 m = m.clickable().on_click(move || on_click());
262 }
263
264 Box(m).child(with_content_color(fg, || {
265 Row(Modifier::new()
266 .align_items(AlignItems::CENTER)
267 .padding_values(PaddingValues {
268 left: 16.0,
269 right: 24.0,
270 top: 0.0,
271 bottom: 0.0,
272 }))
273 .child((
274 config
275 .icon
276 .unwrap_or(Box(Modifier::new().width(24.0).height(24.0))),
277 Box(Modifier::new().width(12.0).height(1.0)),
278 Box(Modifier::new().flex_grow(1.0)).child(label),
279 config.badge.unwrap_or(Box(Modifier::new())),
280 ))
281 }))
282}