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    let th = theme();
126    let bg = th.surface_container_low;
127    let modifier = if elevated {
128        modifier
129    } else {
130        modifier.border(1.0, th.outline_variant, 12.0)
131    };
132    Surface(
133        modifier.background(bg).clip_rounded(12.0).padding(16.0),
134        content,
135    )
136}
137
138pub fn Snackbar(
139    message: impl Into<String>,
140    action: Option<SnackbarAction>,
141    base_modifier: Modifier,
142) -> View {
143    let msg = message.into();
144    let th = theme();
145    let bg = th.surface_variant;
146    let fg = th.on_surface;
147    let action_color = th.primary;
148
149    // Base (positioning) first
150    let modifier = Modifier::new()
151        .background(bg)
152        .clip_rounded(th.shapes.small)
153        .border(1.0, th.outline_variant, th.shapes.small)
154        .then(base_modifier)
155        .padding_values(PaddingValues {
156            left: 16.0,
157            right: 16.0,
158            top: 12.0,
159            bottom: 12.0,
160        })
161        .min_height(48.0)
162        .min_width(280.0);
163
164    Surface(
165        modifier,
166        Row(Modifier::new().align_items(repose_core::AlignItems::Center)).child((
167            Text(msg)
168                .color(fg)
169                .size(th.typography.body_medium)
170                .max_lines(2)
171                .overflow_ellipsize(),
172            Spacer(),
173            action
174                .map(|a| {
175                    let label = a.label.clone();
176                    Box(Modifier::new()
177                        .padding_values(PaddingValues {
178                            left: 8.0,
179                            right: 8.0,
180                            top: 6.0,
181                            bottom: 6.0,
182                        })
183                        .clip_rounded(th.shapes.extra_small)
184                        .clickable()
185                        .on_pointer_down(move |_| (a.on_click)()))
186                    .child(
187                        Text(label)
188                            .color(action_color)
189                            .size(th.typography.label_large)
190                            .single_line(),
191                    )
192                })
193                .unwrap_or(Box(Modifier::new())),
194        )),
195    )
196}
197
198pub fn OutlinedCard(modifier: Modifier, content: View) -> View {
199    Surface(
200        modifier
201            .border(1.0, Color::from_hex("#444444"), 12.0)
202            .clip_rounded(12.0)
203            .padding(16.0),
204        content,
205    )
206}
207
208pub fn FilterChip(
209    selected: bool,
210    on_click: impl Fn() + 'static,
211    label: View,
212    leading_icon: Option<View>,
213) -> View {
214    let bg = if selected {
215        theme().primary
216    } else {
217        theme().surface
218    };
219    let fg = if selected {
220        theme().on_primary
221    } else {
222        theme().on_surface
223    };
224
225    Surface(
226        Modifier::new()
227            .background(bg)
228            .border(1.0, Color::from_hex("#444444"), 8.0)
229            .clip_rounded(8.0)
230            .padding(12.0)
231            .clickable()
232            .on_pointer_down(move |_| on_click()),
233        Row(Modifier::new()).child((leading_icon.unwrap_or(Box(Modifier::new())), label)),
234    )
235}
236
237pub fn Scaffold(
238    top_bar: Option<View>,
239    bottom_bar: Option<View>,
240    floating_action_button: Option<View>,
241    content: impl Fn(PaddingValues) -> View,
242) -> View {
243    Stack(Modifier::new().fill_max_size()).child((
244        // Main content with padding
245        Box(Modifier::new()
246            .fill_max_size()
247            .padding_values(PaddingValues {
248                top: if top_bar.is_some() { 64.0 } else { 0.0 },
249                bottom: if bottom_bar.is_some() { 80.0 } else { 0.0 },
250                ..Default::default()
251            }))
252        .child(content(PaddingValues::default())),
253        // Top bar
254        if let Some(bar) = top_bar {
255            Box(Modifier::new()
256                .absolute()
257                .offset(Some(0.0), Some(0.0), Some(0.0), None))
258            .child(bar)
259        } else {
260            Box(Modifier::new())
261        },
262        // Bottom bar
263        if let Some(bar) = bottom_bar {
264            Box(Modifier::new()
265                .absolute()
266                .offset(Some(0.0), None, Some(0.0), Some(0.0)))
267            .child(bar)
268        } else {
269            Box(Modifier::new())
270        },
271        // FAB
272        if let Some(fab) = floating_action_button {
273            Box(Modifier::new()
274                .absolute()
275                .offset(None, None, Some(16.0), Some(16.0)))
276            .child(fab)
277        } else {
278            Box(Modifier::new())
279        },
280    ))
281}