Skip to main content

repose_material/material3/
nav_drawer.rs

1#![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/// Configuration for [`NavigationDrawer`].
17#[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
42/// State controlling drawer open/close.
43pub 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
67/// A modal navigation drawer that slides in from the left with a scrim overlay.
68pub 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
122/// M3 Dismissible Navigation Drawer - slides alongside content without scrim.
123/// Uses [`DrawerState`] to control open/close.
124pub 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
164/// M3 Permanent Navigation Drawer - always visible alongside content.
165pub 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/// A destination entry inside a NavigationDrawer.
185#[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}