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