Skip to main content

repose_material/material3/
nav_rail.rs

1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use repose_core::animation::AnimationSpec;
7use repose_core::*;
8use repose_ui::{Box, Column, Text, TextStyle, ViewExt, anim::animate_color};
9
10use super::*;
11
12/// Configuration for [`NavigationRail`].
13#[derive(Clone, Debug)]
14pub struct NavigationRailConfig {
15    pub modifier: Modifier,
16    pub container_color: Color,
17    pub selected_icon_color: Color,
18    pub selected_text_color: Color,
19    pub unselected_icon_color: Color,
20    pub unselected_text_color: Color,
21    pub indicator_color: Color,
22    pub width: f32,
23    pub item_radius: f32,
24    pub indicator_opacity: f32,
25    pub item_spacing: f32,
26    pub indicator_width: f32,
27    pub indicator_height: f32,
28}
29
30impl Default for NavigationRailConfig {
31    fn default() -> Self {
32        Self {
33            modifier: Modifier::new(),
34            container_color: NavigationRailDefaults::container_color(),
35            selected_icon_color: NavigationRailDefaults::selected_icon_color(),
36            selected_text_color: NavigationRailDefaults::selected_text_color(),
37            unselected_icon_color: NavigationRailDefaults::unselected_icon_color(),
38            unselected_text_color: NavigationRailDefaults::unselected_text_color(),
39            indicator_color: NavigationRailDefaults::indicator_color(),
40            width: NavigationRailDefaults::WIDTH,
41            item_radius: NavigationRailDefaults::ITEM_RADIUS,
42            indicator_opacity: NavigationRailDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
43            item_spacing: NavigationRailDefaults::ITEM_SPACING,
44            indicator_width: NavigationRailDefaults::ACTIVE_INDICATOR_WIDTH,
45            indicator_height: NavigationRailDefaults::ACTIVE_INDICATOR_HEIGHT,
46        }
47    }
48}
49
50/// A destination entry inside a NavigationRail.
51pub struct NavRailItem {
52    pub icon: View,
53    pub label: String,
54    pub on_click: Rc<dyn Fn()>,
55    pub badge: Option<View>,
56    pub enabled: bool,
57    pub interaction_source: Option<MutableInteractionSource>,
58}
59
60static NAVRAIL_COUNTER: AtomicU64 = AtomicU64::new(0);
61
62/// M3 Navigation Rail - a compact vertical navigation sidebar.
63///
64/// Typically placed on the left side of the screen. Contains navigation items
65/// (icon + label) with animated selection indicator.
66pub fn NavigationRail(
67    selected_index: usize,
68    items: Vec<NavRailItem>,
69    header: Option<View>,
70    fab: Option<View>,
71    config: NavigationRailConfig,
72) -> View {
73    let th = theme();
74    let id = remember(|| NAVRAIL_COUNTER.fetch_add(1, Ordering::Relaxed));
75    let default_effects = AnimationSpec::spring_crit(40.0);
76
77    let mut top_children: Vec<View> = Vec::new();
78    let mut item_views: Vec<View> = Vec::new();
79
80    let has_header = header.is_some();
81    let has_fab = fab.is_some();
82
83    if let Some(h) = header {
84        top_children.push(
85            Box(Modifier::new()
86                .padding_values(PaddingValues {
87                    left: 12.0,
88                    right: 12.0,
89                    top: 12.0,
90                    bottom: 12.0,
91                })
92                .align_self(AlignSelf::CENTER))
93            .child(h),
94        );
95    }
96
97    if let Some(f) = fab {
98        top_children.push(
99            Box(Modifier::new()
100                .padding_values(PaddingValues {
101                    left: 12.0,
102                    right: 12.0,
103                    top: 8.0,
104                    bottom: 8.0,
105                })
106                .align_self(AlignSelf::CENTER))
107            .child(f),
108        );
109    }
110
111    if has_header || has_fab {
112        top_children.push(Box(Modifier::new()
113            .fill_max_width()
114            .height(1.0)
115            .background(th.outline_variant)));
116    }
117
118    for (i, item) in items.into_iter().enumerate() {
119        let selected = i == selected_index;
120        let is_enabled = item.enabled;
121
122        let fg = animate_color(
123            format!("nr_fg_{}_{}", id, i),
124            if selected {
125                config.selected_icon_color
126            } else {
127                config.unselected_icon_color
128            },
129            default_effects,
130        );
131        let fg_label = animate_color(
132            format!("nr_fl_{}_{}", id, i),
133            if selected {
134                config.selected_text_color
135            } else {
136                config.unselected_text_color
137            },
138            default_effects,
139        );
140        let bg = animate_color(
141            format!("nr_bg_{}_{}", id, i),
142            if selected {
143                config.indicator_color
144            } else {
145                Color::TRANSPARENT
146            },
147            default_effects,
148        );
149
150        let cb = item.on_click.clone();
151        let nr_source: Rc<MutableInteractionSource> = item
152            .interaction_source
153            .clone()
154            .map(Rc::new)
155            .unwrap_or_else(|| remember(MutableInteractionSource::new));
156
157        let mut item_m = Modifier::new()
158            .fill_max_width()
159            .padding_values(PaddingValues {
160                left: 4.0,
161                right: 4.0,
162                top: 4.0,
163                bottom: 4.0,
164            })
165            .align_items(AlignItems::CENTER)
166            .justify_content(JustifyContent::CENTER)
167            .background(bg)
168            .state_colors(StateColors {
169                default: Color::TRANSPARENT,
170                hovered: Color::TRANSPARENT,
171                focused: Color::TRANSPARENT,
172                pressed: Color::TRANSPARENT,
173                dragged: th.on_surface.with_alpha_f32(0.12),
174                disabled: Color::TRANSPARENT,
175            })
176            .clip_rounded(config.item_radius)
177            .interaction_source(&nr_source)
178            .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
179                color: Some(th.on_surface),
180                bounded: true,
181                ..Default::default()
182            }))
183            .semantics(Semantics::new(Role::Tab).with_label(&item.label));
184
185        if is_enabled {
186            item_m = item_m.clickable().on_click({
187                let cb = cb.clone();
188                move || cb()
189            });
190        }
191
192        item_views.push(
193            Column(item_m).child((
194                Column(Modifier::new()).child((
195                    Box(Modifier::new().size(24.0, 24.0))
196                        .child(with_content_color(fg, move || item.icon)),
197                    item.badge
198                        .map(|b| {
199                            Box(Modifier::new()
200                                .absolute()
201                                .offset(None, None, None, Some(0.0)))
202                            .child(b)
203                        })
204                        .unwrap_or(Box(Modifier::new())),
205                )),
206                Box(Modifier::new().fill_max_width().height(4.0)),
207                Text(item.label)
208                    .color(fg_label)
209                    .size(th.typography.label_medium)
210                    .single_line(),
211            )),
212        );
213    }
214
215    Column(
216        Modifier::new()
217            .width(config.width)
218            .fill_max_height()
219            .background(config.container_color)
220            .align_items(AlignItems::CENTER)
221            .semantics(Semantics::new(Role::Container).with_selectable_group())
222            .then(config.modifier),
223    )
224    .child((
225        Column(Modifier::new()).with_children(top_children),
226        Box(Modifier::new().flex_grow(1.0)).child(
227            Column(
228                Modifier::new()
229                    .fill_max_size()
230                    .justify_content(JustifyContent::SPACE_BETWEEN)
231                    .align_items(AlignItems::CENTER),
232            )
233            .with_children(item_views),
234        ),
235    ))
236}