Skip to main content

repose_material/material3/
mod.rs

1#![allow(non_snake_case)]
2
3mod components;
4pub use components::*;
5
6use std::rc::Rc;
7
8use repose_core::*;
9use repose_ui::{
10    Box, Column, Row, Spacer, Stack, Surface, Text, TextStyle, ViewExt, anim::animate_f32,
11    overlay::SnackbarAction,
12};
13
14pub fn AlertDialog(
15    visible: bool,
16    on_dismiss: impl Fn() + 'static,
17    title: View,
18    text: View,
19    confirm_button: View,
20    dismiss_button: Option<View>,
21) -> View {
22    if !visible {
23        return Box(Modifier::new());
24    }
25
26    Stack(Modifier::new().fill_max_size()).child((
27        // Scrim
28        Box(Modifier::new()
29            .fill_max_size()
30            .background(Color::from_hex("#000000AA"))
31            .clickable()
32            .on_pointer_down(move |_| on_dismiss())),
33        // Dialog content
34        Surface(
35            Modifier::new()
36                .size(280.0, 200.0)
37                .background(theme().surface)
38                .clip_rounded(28.0)
39                .padding(24.0),
40            Column(Modifier::new()).child((
41                title,
42                Box(Modifier::new().size(1.0, 16.0)),
43                text,
44                Spacer(),
45                Row(Modifier::new()).child((
46                    dismiss_button.unwrap_or(Box(Modifier::new())),
47                    Spacer(),
48                    confirm_button,
49                )),
50            )),
51        ),
52    ))
53}
54
55pub fn BottomSheet(
56    visible: bool,
57    on_dismiss: impl Fn() + 'static,
58    modifier: Modifier,
59    content: View,
60) -> View {
61    let offset = animate_f32(
62        "sheet_offset",
63        if visible { 0.0 } else { 800.0 },
64        AnimationSpec::spring_gentle(),
65    );
66
67    Stack(Modifier::new().fill_max_size()).child((
68        // Scrim
69        if visible {
70            Box(Modifier::new()
71                .fill_max_size()
72                .background(Color::from_hex("#00000055"))
73                .on_pointer_down(move |_| on_dismiss()))
74        } else {
75            Box(Modifier::new())
76        },
77        // Sheet
78        Box(modifier
79            .absolute()
80            .offset(None, Some(offset), Some(0.0), Some(0.0)))
81        .child(content),
82    ))
83}
84
85pub fn NavigationBar(selected_index: usize, items: Vec<NavItem>) -> View {
86    Row(Modifier::new()
87        .fill_max_size()
88        .background(theme().surface)
89        .padding(8.0))
90    .child(
91        items
92            .into_iter()
93            .enumerate()
94            .map(|(i, item)| NavigationBarItem(item, i == selected_index))
95            .collect::<Vec<_>>(),
96    )
97}
98
99pub struct NavItem {
100    pub icon: View,
101    pub label: String,
102    pub on_click: Rc<dyn Fn()>,
103}
104
105fn NavigationBarItem(item: NavItem, selected: bool) -> View {
106    let color = if selected {
107        theme().primary
108    } else {
109        theme().on_surface
110    };
111
112    Column(
113        Modifier::new()
114            .flex_grow(1.0)
115            .clickable()
116            .on_pointer_down(move |_| (item.on_click)()),
117    )
118    .child((
119        item.icon, // Tint with color
120        Text(item.label).color(color),
121    ))
122}
123
124pub fn Card(modifier: Modifier, elevated: bool, content: View) -> View {
125    Surface(
126        modifier
127            .background(theme().surface)
128            .border(1.0, Color::from_hex("#22222222"), 12.0)
129            .clip_rounded(12.0)
130            .padding(16.0),
131        content,
132    )
133}
134
135pub fn Snackbar(
136    message: impl Into<String>,
137    action: Option<SnackbarAction>,
138    base_modifier: Modifier,
139) -> View {
140    let msg = message.into();
141    let th = theme();
142    let bg = th.surface_variant;
143    let fg = th.on_surface;
144    let action_color = th.primary;
145
146    // Base (positioning) first, then layer on snackbar styling
147    let modifier = base_modifier
148        .background(bg)
149        .clip_rounded(th.shapes.small)
150        .border(1.0, th.outline_variant, th.shapes.small)
151        .padding_values(PaddingValues {
152            left: 16.0,
153            right: 16.0,
154            top: 12.0,
155            bottom: 12.0,
156        })
157        .min_height(48.0)
158        .min_width(280.0);
159
160    Surface(
161        modifier,
162        Row(Modifier::new().align_items(repose_core::AlignItems::Center)).child((
163            Text(msg)
164                .color(fg)
165                .size(th.typography.body_medium)
166                .max_lines(2)
167                .overflow_ellipsize(),
168            Spacer(),
169            action
170                .map(|a| {
171                    let label = a.label.clone();
172                    Box(Modifier::new()
173                        .padding_values(PaddingValues {
174                            left: 8.0,
175                            right: 8.0,
176                            top: 6.0,
177                            bottom: 6.0,
178                        })
179                        .clip_rounded(th.shapes.extra_small)
180                        .clickable()
181                        .on_pointer_down(move |_| (a.on_click)()))
182                    .child(
183                        Text(label)
184                            .color(action_color)
185                            .size(th.typography.label_large)
186                            .single_line(),
187                    )
188                })
189                .unwrap_or(Box(Modifier::new())),
190        )),
191    )
192}
193
194pub fn OutlinedCard(modifier: Modifier, content: View) -> View {
195    Surface(
196        modifier
197            .border(1.0, Color::from_hex("#444444"), 12.0)
198            .clip_rounded(12.0)
199            .padding(16.0),
200        content,
201    )
202}
203
204pub fn FilterChip(
205    selected: bool,
206    on_click: impl Fn() + 'static,
207    label: View,
208    leading_icon: Option<View>,
209) -> View {
210    let bg = if selected {
211        theme().primary
212    } else {
213        theme().surface
214    };
215    let fg = if selected {
216        theme().on_primary
217    } else {
218        theme().on_surface
219    };
220
221    Surface(
222        Modifier::new()
223            .background(bg)
224            .border(1.0, Color::from_hex("#444444"), 8.0)
225            .clip_rounded(8.0)
226            .padding(12.0)
227            .clickable()
228            .on_pointer_down(move |_| on_click()),
229        Row(Modifier::new()).child((leading_icon.unwrap_or(Box(Modifier::new())), label)),
230    )
231}
232
233pub fn Scaffold(
234    top_bar: Option<View>,
235    bottom_bar: Option<View>,
236    floating_action_button: Option<View>,
237    content: impl Fn(PaddingValues) -> View,
238) -> View {
239    Stack(Modifier::new().fill_max_size()).child((
240        // Main content with padding
241        Box(Modifier::new()
242            .fill_max_size()
243            .padding_values(PaddingValues {
244                top: if top_bar.is_some() { 64.0 } else { 0.0 },
245                bottom: if bottom_bar.is_some() { 80.0 } else { 0.0 },
246                ..Default::default()
247            }))
248        .child(content(PaddingValues::default())),
249        // Top bar
250        if let Some(bar) = top_bar {
251            Box(Modifier::new()
252                .absolute()
253                .offset(Some(0.0), Some(0.0), Some(0.0), None))
254            .child(bar)
255        } else {
256            Box(Modifier::new())
257        },
258        // Bottom bar
259        if let Some(bar) = bottom_bar {
260            Box(Modifier::new()
261                .absolute()
262                .offset(Some(0.0), None, Some(0.0), Some(0.0)))
263            .child(bar)
264        } else {
265            Box(Modifier::new())
266        },
267        // FAB
268        if let Some(fab) = floating_action_button {
269            Box(Modifier::new()
270                .absolute()
271                .offset(None, None, Some(16.0), Some(16.0)))
272            .child(fab)
273        } else {
274            Box(Modifier::new())
275        },
276    ))
277}