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