repose_material/material3/
components.rs1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4
5use repose_core::*;
6use repose_ui::{Box, Column, Row, Text, TextStyle, ViewExt};
7
8pub fn TopAppBar(
11 title: impl Into<String>,
12 navigation_icon: Option<View>,
13 actions: Vec<View>,
14) -> View {
15 let th = theme();
16 Row(Modifier::new()
17 .fill_max_width()
18 .height(64.0)
19 .background(th.surface)
20 .padding_values(PaddingValues {
21 left: 4.0,
22 right: 4.0,
23 top: 0.0,
24 bottom: 0.0,
25 })
26 .align_items(AlignItems::Center))
27 .child((
28 navigation_icon.unwrap_or(Box(Modifier::new().size(16.0, 1.0))),
29 Box(Modifier::new()
30 .padding_values(PaddingValues {
31 left: 16.0,
32 right: 0.0,
33 top: 0.0,
34 bottom: 0.0,
35 })
36 .flex_grow(1.0))
37 .child(
38 Text(title)
39 .color(th.on_surface)
40 .size(th.typography.title_large),
41 ),
42 Row(Modifier::new().align_items(AlignItems::Center)).child(actions),
43 ))
44}
45
46pub fn IconButton(icon: View, on_click: impl Fn() + 'static) -> View {
48 Box(Modifier::new()
49 .size(40.0, 40.0)
50 .clip_rounded(20.0)
51 .align_items(AlignItems::Center)
52 .justify_content(JustifyContent::Center)
53 .clickable()
54 .on_pointer_down(move |_| on_click()))
55 .child(icon)
56}
57
58pub fn FilledIconButton(icon: View, on_click: impl Fn() + 'static) -> View {
60 let th = theme();
61 Box(Modifier::new()
62 .size(40.0, 40.0)
63 .clip_rounded(20.0)
64 .background(th.primary)
65 .align_items(AlignItems::Center)
66 .justify_content(JustifyContent::Center)
67 .clickable()
68 .on_pointer_down(move |_| on_click()))
69 .child(icon)
70}
71
72pub fn FilledButton(label: impl Into<String>, on_click: impl Fn() + 'static) -> View {
74 let th = theme();
75 Box(Modifier::new()
76 .height(40.0)
77 .min_width(48.0)
78 .background(th.primary)
79 .clip_rounded(20.0)
80 .padding_values(PaddingValues {
81 left: 24.0,
82 right: 24.0,
83 top: 0.0,
84 bottom: 0.0,
85 })
86 .align_items(AlignItems::Center)
87 .justify_content(JustifyContent::Center)
88 .clickable()
89 .on_pointer_down(move |_| on_click()))
90 .child(
91 Text(label)
92 .color(th.on_primary)
93 .size(th.typography.label_large)
94 .single_line(),
95 )
96}
97
98pub fn FilledTonalButton(label: impl Into<String>, on_click: impl Fn() + 'static) -> View {
100 let th = theme();
101 Box(Modifier::new()
102 .height(40.0)
103 .min_width(48.0)
104 .background(th.secondary_container)
105 .clip_rounded(20.0)
106 .padding_values(PaddingValues {
107 left: 24.0,
108 right: 24.0,
109 top: 0.0,
110 bottom: 0.0,
111 })
112 .align_items(AlignItems::Center)
113 .justify_content(JustifyContent::Center)
114 .clickable()
115 .on_pointer_down(move |_| on_click()))
116 .child(
117 Text(label)
118 .color(th.on_secondary_container)
119 .size(th.typography.label_large)
120 .single_line(),
121 )
122}
123
124pub fn OutlinedButton(label: impl Into<String>, on_click: impl Fn() + 'static) -> View {
126 let th = theme();
127 Box(Modifier::new()
128 .height(40.0)
129 .min_width(48.0)
130 .border(1.0, th.outline, 20.0)
131 .clip_rounded(20.0)
132 .padding_values(PaddingValues {
133 left: 24.0,
134 right: 24.0,
135 top: 0.0,
136 bottom: 0.0,
137 })
138 .align_items(AlignItems::Center)
139 .justify_content(JustifyContent::Center)
140 .clickable()
141 .on_pointer_down(move |_| on_click()))
142 .child(
143 Text(label)
144 .color(th.primary)
145 .size(th.typography.label_large)
146 .single_line(),
147 )
148}
149
150pub fn TextButton(label: impl Into<String>, on_click: impl Fn() + 'static) -> View {
152 let th = theme();
153 Box(Modifier::new()
154 .height(40.0)
155 .min_width(48.0)
156 .clip_rounded(20.0)
157 .padding_values(PaddingValues {
158 left: 12.0,
159 right: 12.0,
160 top: 0.0,
161 bottom: 0.0,
162 })
163 .align_items(AlignItems::Center)
164 .justify_content(JustifyContent::Center)
165 .clickable()
166 .on_pointer_down(move |_| on_click()))
167 .child(
168 Text(label)
169 .color(th.primary)
170 .size(th.typography.label_large)
171 .single_line(),
172 )
173}
174
175pub fn FAB(icon: View, on_click: impl Fn() + 'static) -> View {
177 let th = theme();
178 Box(Modifier::new()
179 .size(56.0, 56.0)
180 .background(th.primary_container)
181 .clip_rounded(16.0)
182 .align_items(AlignItems::Center)
183 .justify_content(JustifyContent::Center)
184 .clickable()
185 .on_pointer_down(move |_| on_click()))
186 .child(icon)
187}
188
189pub fn SmallFAB(icon: View, on_click: impl Fn() + 'static) -> View {
191 let th = theme();
192 Box(Modifier::new()
193 .size(40.0, 40.0)
194 .background(th.primary_container)
195 .clip_rounded(12.0)
196 .align_items(AlignItems::Center)
197 .justify_content(JustifyContent::Center)
198 .clickable()
199 .on_pointer_down(move |_| on_click()))
200 .child(icon)
201}
202
203pub fn LargeFAB(icon: View, on_click: impl Fn() + 'static) -> View {
205 let th = theme();
206 Box(Modifier::new()
207 .size(96.0, 96.0)
208 .background(th.primary_container)
209 .clip_rounded(28.0)
210 .align_items(AlignItems::Center)
211 .justify_content(JustifyContent::Center)
212 .clickable()
213 .on_pointer_down(move |_| on_click()))
214 .child(icon)
215}
216
217pub fn ExtendedFAB(
219 icon: Option<View>,
220 label: impl Into<String>,
221 on_click: impl Fn() + 'static,
222) -> View {
223 let th = theme();
224 let has_icon = icon.is_some();
225 Row(Modifier::new()
226 .height(56.0)
227 .min_width(80.0)
228 .background(th.primary_container)
229 .clip_rounded(16.0)
230 .padding_values(PaddingValues {
231 left: 16.0,
232 right: 20.0,
233 top: 0.0,
234 bottom: 0.0,
235 })
236 .align_items(AlignItems::Center)
237 .clickable()
238 .on_pointer_down(move |_| on_click()))
239 .child((
240 icon.unwrap_or(Box(Modifier::new())),
241 Box(Modifier::new().size(if has_icon { 12.0 } else { 0.0 }, 1.0)),
242 Text(label)
243 .color(th.on_primary_container)
244 .size(th.typography.label_large)
245 .single_line(),
246 ))
247}
248
249pub fn Divider() -> View {
251 let th = theme();
252 Box(Modifier::new()
253 .fill_max_width()
254 .height(1.0)
255 .background(th.outline_variant))
256}
257
258pub fn VerticalDivider() -> View {
260 let th = theme();
261 Box(Modifier::new()
262 .width(1.0)
263 .fill_max_height()
264 .background(th.outline_variant))
265}
266
267pub fn Badge(label: Option<impl Into<String>>) -> View {
270 let th = theme();
271 match label {
272 None => Box(Modifier::new()
273 .size(6.0, 6.0)
274 .background(th.error)
275 .clip_rounded(3.0)),
276 Some(text) => {
277 let text = text.into();
278 Box(Modifier::new()
279 .min_width(16.0)
280 .height(16.0)
281 .background(th.error)
282 .clip_rounded(8.0)
283 .padding_values(PaddingValues {
284 left: 4.0,
285 right: 4.0,
286 top: 0.0,
287 bottom: 0.0,
288 })
289 .align_items(AlignItems::Center)
290 .justify_content(JustifyContent::Center))
291 .child(
292 Text(text)
293 .color(th.on_error)
294 .size(th.typography.label_small)
295 .single_line(),
296 )
297 }
298 }
299}
300
301pub fn ListItem(
303 headline: impl Into<String>,
304 supporting_text: Option<String>,
305 leading: Option<View>,
306 trailing: Option<View>,
307 on_click: Option<Rc<dyn Fn()>>,
308) -> View {
309 let th = theme();
310 let mut modifier = Modifier::new()
311 .fill_max_width()
312 .min_height(if supporting_text.is_some() {
313 72.0
314 } else {
315 56.0
316 })
317 .padding_values(PaddingValues {
318 left: 16.0,
319 right: 24.0,
320 top: 8.0,
321 bottom: 8.0,
322 })
323 .align_items(AlignItems::Center);
324
325 if let Some(cb) = on_click {
326 modifier = modifier.clickable().on_pointer_down(move |_| cb());
327 }
328
329 Row(modifier).child((
330 leading
331 .map(|v| {
332 Box(Modifier::new().padding_values(PaddingValues {
333 left: 0.0,
334 right: 16.0,
335 top: 0.0,
336 bottom: 0.0,
337 }))
338 .child(v)
339 })
340 .unwrap_or(Box(Modifier::new())),
341 Column(
342 Modifier::new()
343 .flex_grow(1.0)
344 .justify_content(JustifyContent::Center),
345 )
346 .child((
347 Text(headline)
348 .color(th.on_surface)
349 .size(th.typography.body_large)
350 .single_line(),
351 supporting_text
352 .map(|st| {
353 Text(st)
354 .color(th.on_surface_variant)
355 .size(th.typography.body_medium)
356 .max_lines(2)
357 .overflow_ellipsize()
358 })
359 .unwrap_or(Box(Modifier::new())),
360 )),
361 trailing
362 .map(|v| {
363 Box(Modifier::new().padding_values(PaddingValues {
364 left: 16.0,
365 right: 0.0,
366 top: 0.0,
367 bottom: 0.0,
368 }))
369 .child(v)
370 })
371 .unwrap_or(Box(Modifier::new())),
372 ))
373}
374
375pub struct Tab {
377 pub label: String,
378 pub icon: Option<View>,
379 pub on_click: Rc<dyn Fn()>,
380}
381
382pub fn TabRow(selected_index: usize, tabs: Vec<Tab>) -> View {
384 let th = theme();
385 Row(Modifier::new()
386 .fill_max_width()
387 .height(48.0)
388 .background(th.surface))
389 .child(
390 tabs.into_iter()
391 .enumerate()
392 .map(|(i, tab)| {
393 let selected = i == selected_index;
394 let color = if selected {
395 th.primary
396 } else {
397 th.on_surface_variant
398 };
399 let cb = tab.on_click.clone();
400
401 Column(
402 Modifier::new()
403 .flex_grow(1.0)
404 .fill_max_height()
405 .align_items(AlignItems::Center)
406 .justify_content(JustifyContent::Center)
407 .clickable()
408 .on_pointer_down(move |_| cb()),
409 )
410 .child((
411 tab.icon.unwrap_or(Box(Modifier::new())),
412 Text(tab.label)
413 .color(color)
414 .size(th.typography.title_small)
415 .single_line(),
416 if selected {
417 Box(Modifier::new()
418 .fill_max_width()
419 .height(3.0)
420 .background(th.primary)
421 .clip_rounded(1.5))
422 } else {
423 Box(Modifier::new().height(3.0))
424 },
425 ))
426 })
427 .collect::<Vec<_>>(),
428 )
429}
430
431pub struct Segment {
433 pub label: String,
434 pub icon: Option<View>,
435 pub on_click: Rc<dyn Fn()>,
436}
437
438pub fn SegmentedButton(selected: &[usize], segments: Vec<Segment>) -> View {
441 let th = theme();
442 let count = segments.len();
443
444 Row(Modifier::new()
445 .height(40.0)
446 .border(1.0, th.outline, 20.0)
447 .clip_rounded(20.0))
448 .child(
449 segments
450 .into_iter()
451 .enumerate()
452 .map(|(i, seg)| {
453 let is_selected = selected.contains(&i);
454 let bg = if is_selected {
455 th.secondary_container
456 } else {
457 Color::TRANSPARENT
458 };
459 let fg = if is_selected {
460 th.on_secondary_container
461 } else {
462 th.on_surface
463 };
464 let cb = seg.on_click.clone();
465
466 let mut modifier = Modifier::new()
467 .flex_grow(1.0)
468 .fill_max_height()
469 .background(bg)
470 .align_items(AlignItems::Center)
471 .justify_content(JustifyContent::Center)
472 .padding_values(PaddingValues {
473 left: 12.0,
474 right: 12.0,
475 top: 0.0,
476 bottom: 0.0,
477 })
478 .clickable()
479 .on_pointer_down(move |_| cb());
480
481 if i < count - 1 {
482 modifier = modifier.border(1.0, th.outline, 0.0);
483 }
484
485 Row(modifier).child((
486 seg.icon.unwrap_or(Box(Modifier::new())),
487 Text(seg.label)
488 .color(fg)
489 .size(th.typography.label_large)
490 .single_line(),
491 ))
492 })
493 .collect::<Vec<_>>(),
494 )
495}
496
497pub fn CircularProgress(value: Option<f32>) -> View {
502 View::new(
503 0,
504 ViewKind::ProgressBar {
505 value: value.unwrap_or(0.0),
506 min: 0.0,
507 max: 1.0,
508 circular: true,
509 },
510 )
511 .modifier(Modifier::new().size(48.0, 48.0))
512 .semantics(Semantics {
513 role: Role::ProgressBar,
514 label: None,
515 focused: false,
516 enabled: true,
517 })
518}