Skip to main content

repose_material/material3/
mod.rs

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