repose_material/material3/
mod.rs1#![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 Box(Modifier::new()
29 .fill_max_size()
30 .background(Color::from_hex("#000000AA"))
31 .clickable()
32 .on_pointer_down(move |_| on_dismiss())),
33 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 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 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, 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 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 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 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 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 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}