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 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 pressed: config.tonal_elevation,
150 dragged: config.tonal_elevation,
151 disabled: 0.0,
152 });
153 }
154
155 ZStack(Modifier::new().fill_max_size()).child((
156 Box(Modifier::new()
157 .fill_max_size()
158 .background(config.content_color))
159 .child(content),
160 Box(drawer_m).child(drawer_content),
161 ))
162}
163
164pub fn PermanentNavigationDrawer(
166 drawer_content: View,
167 content: View,
168 config: NavigationDrawerConfig,
169) -> View {
170 Row(Modifier::new().fill_max_size()).child((
171 Box(Modifier::new()
172 .width(config.width)
173 .fill_max_height()
174 .background(config.container_color))
175 .child(
176 Box(Modifier::new())
177 .color(config.content_color)
178 .child(drawer_content),
179 ),
180 Box(Modifier::new().flex_grow(1.0)).child(content),
181 ))
182}
183
184#[derive(Clone)]
186pub struct NavigationDrawerItemConfig {
187 pub modifier: Modifier,
188 pub icon: Option<View>,
189 pub badge: Option<View>,
190 pub enabled: bool,
191 pub shape_radius: f32,
192 pub interaction_source: Option<MutableInteractionSource>,
193}
194
195impl Default for NavigationDrawerItemConfig {
196 fn default() -> Self {
197 Self {
198 modifier: Modifier::new(),
199 icon: None,
200 badge: None,
201 enabled: true,
202 shape_radius: repose_core::locals::theme().shapes.large,
203 interaction_source: None,
204 }
205 }
206}
207
208pub fn NavigationDrawerItem(
209 label: View,
210 selected: bool,
211 on_click: impl Fn() + 'static,
212 config: NavigationDrawerItemConfig,
213) -> View {
214 let th = theme();
215 let id = remember(|| FILTERCHIP_COUNTER.fetch_add(1, Ordering::Relaxed));
216 let spec = th.motion.color;
217 let bg = animate_color(
218 format!("ndi_bg_{}", id),
219 if selected {
220 th.secondary_container
221 } else {
222 Color::TRANSPARENT
223 },
224 spec,
225 );
226 let fg = animate_color(
227 format!("ndi_fg_{}", id),
228 if selected {
229 th.on_secondary_container
230 } else {
231 th.on_surface_variant
232 },
233 spec,
234 );
235
236 let nd_source: Rc<MutableInteractionSource> = config
237 .interaction_source
238 .clone()
239 .map(Rc::new)
240 .unwrap_or_else(|| remember(MutableInteractionSource::new));
241
242 let mut m = Modifier::new()
243 .fill_max_width()
244 .padding_values(PaddingValues {
245 left: 12.0,
246 right: 12.0,
247 top: 0.0,
248 bottom: 0.0,
249 })
250 .min_height(56.0)
251 .background(bg)
252 .state_colors(StateColors {
253 default: Color::TRANSPARENT,
254 hovered: th.on_surface.with_alpha_f32(0.08),
255 pressed: th.on_surface.with_alpha_f32(0.12),
256 dragged: th.on_surface.with_alpha_f32(0.12),
257 disabled: Color::TRANSPARENT,
258 })
259 .clip_rounded(config.shape_radius)
260 .interaction_source(&*nd_source)
261 .then(config.modifier);
262
263 if config.enabled {
264 m = m.clickable().on_click(move || on_click());
265 }
266
267 Box(m).child(with_content_color(fg, || {
268 Row(Modifier::new()
269 .align_items(AlignItems::CENTER)
270 .padding_values(PaddingValues {
271 left: 16.0,
272 right: 24.0,
273 top: 0.0,
274 bottom: 0.0,
275 }))
276 .child((
277 config
278 .icon
279 .unwrap_or(Box(Modifier::new().width(24.0).height(24.0))),
280 Box(Modifier::new().width(12.0).height(1.0)),
281 Box(Modifier::new().flex_grow(1.0)).child(label),
282 config.badge.unwrap_or(Box(Modifier::new())),
283 ))
284 }))
285}