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(on_click: impl Fn() + 'static, content: impl FnOnce() -> View) -> View {
74 let th = theme();
75 let content = with_content_color(th.on_primary, content);
76 Box(Modifier::new()
77 .height(40.0)
78 .min_width(48.0)
79 .background(th.primary)
80 .clip_rounded(20.0)
81 .padding_values(PaddingValues {
82 left: 24.0,
83 right: 24.0,
84 top: 0.0,
85 bottom: 0.0,
86 })
87 .align_items(AlignItems::Center)
88 .justify_content(JustifyContent::Center)
89 .clickable()
90 .on_pointer_down(move |_| on_click()))
91 .child(content)
92}
93
94pub fn FilledTonalButton(on_click: impl Fn() + 'static, content: impl FnOnce() -> View) -> View {
96 let th = theme();
97 let content = with_content_color(th.on_secondary_container, content);
98 Box(Modifier::new()
99 .height(40.0)
100 .min_width(48.0)
101 .background(th.secondary_container)
102 .clip_rounded(20.0)
103 .padding_values(PaddingValues {
104 left: 24.0,
105 right: 24.0,
106 top: 0.0,
107 bottom: 0.0,
108 })
109 .align_items(AlignItems::Center)
110 .justify_content(JustifyContent::Center)
111 .clickable()
112 .on_pointer_down(move |_| on_click()))
113 .child(content)
114}
115
116pub fn OutlinedButton(on_click: impl Fn() + 'static, content: impl FnOnce() -> View) -> View {
118 let th = theme();
119 let content = with_content_color(th.on_surface, content);
120 Box(Modifier::new()
121 .height(40.0)
122 .min_width(48.0)
123 .border(1.0, th.outline_variant, 20.0)
124 .clip_rounded(20.0)
125 .padding_values(PaddingValues {
126 left: 24.0,
127 right: 24.0,
128 top: 0.0,
129 bottom: 0.0,
130 })
131 .align_items(AlignItems::Center)
132 .justify_content(JustifyContent::Center)
133 .clickable()
134 .on_pointer_down(move |_| on_click()))
135 .child(content)
136}
137
138pub fn TextButton(on_click: impl Fn() + 'static, content: impl FnOnce() -> View) -> View {
140 let th = theme();
141 let content = with_content_color(th.on_surface, content);
142 Box(Modifier::new()
143 .height(40.0)
144 .min_width(48.0)
145 .clip_rounded(20.0)
146 .padding_values(PaddingValues {
147 left: 12.0,
148 right: 12.0,
149 top: 0.0,
150 bottom: 0.0,
151 })
152 .align_items(AlignItems::Center)
153 .justify_content(JustifyContent::Center)
154 .clickable()
155 .on_pointer_down(move |_| on_click()))
156 .child(content)
157}
158
159pub fn FAB(icon: View, on_click: impl Fn() + 'static) -> View {
161 let th = theme();
162 Box(Modifier::new()
163 .size(56.0, 56.0)
164 .background(th.primary_container)
165 .clip_rounded(16.0)
166 .align_items(AlignItems::Center)
167 .justify_content(JustifyContent::Center)
168 .clickable()
169 .on_pointer_down(move |_| on_click()))
170 .child(icon)
171}
172
173pub fn SmallFAB(icon: View, on_click: impl Fn() + 'static) -> View {
175 let th = theme();
176 Box(Modifier::new()
177 .size(40.0, 40.0)
178 .background(th.primary_container)
179 .clip_rounded(12.0)
180 .align_items(AlignItems::Center)
181 .justify_content(JustifyContent::Center)
182 .clickable()
183 .on_pointer_down(move |_| on_click()))
184 .child(icon)
185}
186
187pub fn LargeFAB(icon: View, on_click: impl Fn() + 'static) -> View {
189 let th = theme();
190 Box(Modifier::new()
191 .size(96.0, 96.0)
192 .background(th.primary_container)
193 .clip_rounded(28.0)
194 .align_items(AlignItems::Center)
195 .justify_content(JustifyContent::Center)
196 .clickable()
197 .on_pointer_down(move |_| on_click()))
198 .child(icon)
199}
200
201pub fn ExtendedFAB(
203 icon: Option<View>,
204 label: impl Into<String>,
205 on_click: impl Fn() + 'static,
206) -> View {
207 let th = theme();
208 let has_icon = icon.is_some();
209 Row(Modifier::new()
210 .height(56.0)
211 .min_width(80.0)
212 .background(th.primary_container)
213 .clip_rounded(16.0)
214 .padding_values(PaddingValues {
215 left: 16.0,
216 right: 20.0,
217 top: 0.0,
218 bottom: 0.0,
219 })
220 .align_items(AlignItems::Center)
221 .clickable()
222 .on_pointer_down(move |_| on_click()))
223 .child((
224 icon.unwrap_or(Box(Modifier::new())),
225 Box(Modifier::new().size(if has_icon { 12.0 } else { 0.0 }, 1.0)),
226 Text(label)
227 .color(th.on_primary_container)
228 .size(th.typography.label_large)
229 .single_line(),
230 ))
231}
232
233pub fn Divider() -> View {
235 let th = theme();
236 Box(Modifier::new()
237 .fill_max_width()
238 .height(1.0)
239 .background(th.outline_variant))
240}
241
242pub fn VerticalDivider() -> View {
244 let th = theme();
245 Box(Modifier::new()
246 .width(1.0)
247 .fill_max_height()
248 .background(th.outline_variant))
249}
250
251pub fn Badge(label: Option<impl Into<String>>) -> View {
254 let th = theme();
255 match label {
256 None => Box(Modifier::new()
257 .size(6.0, 6.0)
258 .background(th.error)
259 .clip_rounded(3.0)),
260 Some(text) => {
261 let text = text.into();
262 Box(Modifier::new()
263 .min_width(16.0)
264 .height(16.0)
265 .background(th.error)
266 .clip_rounded(8.0)
267 .padding_values(PaddingValues {
268 left: 4.0,
269 right: 4.0,
270 top: 0.0,
271 bottom: 0.0,
272 })
273 .align_items(AlignItems::Center)
274 .justify_content(JustifyContent::Center))
275 .child(
276 Text(text)
277 .color(th.on_error)
278 .size(th.typography.label_small)
279 .single_line(),
280 )
281 }
282 }
283}
284
285pub fn ListItem(
287 headline: impl Into<String>,
288 supporting_text: Option<String>,
289 leading: Option<View>,
290 trailing: Option<View>,
291 on_click: Option<Rc<dyn Fn()>>,
292) -> View {
293 let th = theme();
294 let mut modifier = Modifier::new()
295 .fill_max_width()
296 .min_height(if supporting_text.is_some() {
297 72.0
298 } else {
299 56.0
300 })
301 .padding_values(PaddingValues {
302 left: 16.0,
303 right: 24.0,
304 top: 8.0,
305 bottom: 8.0,
306 })
307 .align_items(AlignItems::Center);
308
309 if let Some(cb) = on_click {
310 modifier = modifier.clickable().on_pointer_down(move |_| cb());
311 }
312
313 Row(modifier).child((
314 leading
315 .map(|v| {
316 Box(Modifier::new().padding_values(PaddingValues {
317 left: 0.0,
318 right: 16.0,
319 top: 0.0,
320 bottom: 0.0,
321 }))
322 .child(v)
323 })
324 .unwrap_or(Box(Modifier::new())),
325 Column(
326 Modifier::new()
327 .flex_grow(1.0)
328 .justify_content(JustifyContent::Center),
329 )
330 .child((
331 Text(headline)
332 .color(th.on_surface)
333 .size(th.typography.body_large)
334 .single_line(),
335 supporting_text
336 .map(|st| {
337 Text(st)
338 .color(th.on_surface_variant)
339 .size(th.typography.body_medium)
340 .max_lines(2)
341 .overflow_ellipsize()
342 })
343 .unwrap_or(Box(Modifier::new())),
344 )),
345 trailing
346 .map(|v| {
347 Box(Modifier::new().padding_values(PaddingValues {
348 left: 16.0,
349 right: 0.0,
350 top: 0.0,
351 bottom: 0.0,
352 }))
353 .child(v)
354 })
355 .unwrap_or(Box(Modifier::new())),
356 ))
357}
358
359pub struct Tab {
361 pub label: String,
362 pub icon: Option<View>,
363 pub on_click: Rc<dyn Fn()>,
364}
365
366pub fn TabRow(selected_index: usize, tabs: Vec<Tab>) -> View {
368 let th = theme();
369 Row(Modifier::new()
370 .fill_max_width()
371 .height(48.0)
372 .background(th.surface))
373 .child(
374 tabs.into_iter()
375 .enumerate()
376 .map(|(i, tab)| {
377 let selected = i == selected_index;
378 let color = if selected {
379 th.primary
380 } else {
381 th.on_surface_variant
382 };
383 let cb = tab.on_click.clone();
384
385 Column(
386 Modifier::new()
387 .flex_grow(1.0)
388 .fill_max_height()
389 .align_items(AlignItems::Center)
390 .justify_content(JustifyContent::Center)
391 .clickable()
392 .on_pointer_down(move |_| cb()),
393 )
394 .child((
395 tab.icon.unwrap_or(Box(Modifier::new())),
396 Text(tab.label)
397 .color(color)
398 .size(th.typography.title_small)
399 .single_line(),
400 if selected {
401 Box(Modifier::new()
402 .fill_max_width()
403 .height(3.0)
404 .background(th.primary)
405 .clip_rounded(1.5))
406 } else {
407 Box(Modifier::new().height(3.0))
408 },
409 ))
410 })
411 .collect::<Vec<_>>(),
412 )
413}
414
415pub struct Segment {
417 pub label: String,
418 pub icon: Option<View>,
419 pub on_click: Rc<dyn Fn()>,
420}
421
422pub fn SegmentedButton(selected: &[usize], segments: Vec<Segment>) -> View {
425 let th = theme();
426 let count = segments.len();
427
428 Row(Modifier::new()
429 .height(40.0)
430 .border(1.0, th.outline, 20.0)
431 .clip_rounded(20.0))
432 .child(
433 segments
434 .into_iter()
435 .enumerate()
436 .map(|(i, seg)| {
437 let is_selected = selected.contains(&i);
438 let bg = if is_selected {
439 th.secondary_container
440 } else {
441 Color::TRANSPARENT
442 };
443 let fg = if is_selected {
444 th.on_secondary_container
445 } else {
446 th.on_surface
447 };
448 let cb = seg.on_click.clone();
449
450 let mut modifier = Modifier::new()
451 .flex_grow(1.0)
452 .fill_max_height()
453 .background(bg)
454 .align_items(AlignItems::Center)
455 .justify_content(JustifyContent::Center)
456 .padding_values(PaddingValues {
457 left: 12.0,
458 right: 12.0,
459 top: 0.0,
460 bottom: 0.0,
461 })
462 .clickable()
463 .on_pointer_down(move |_| cb());
464
465 if i < count - 1 {
466 modifier = modifier.border(1.0, th.outline, 0.0);
467 }
468
469 Row(modifier).child((
470 seg.icon.unwrap_or(Box(Modifier::new())),
471 Text(seg.label)
472 .color(fg)
473 .size(th.typography.label_large)
474 .single_line(),
475 ))
476 })
477 .collect::<Vec<_>>(),
478 )
479}
480
481pub fn CircularProgressIndicator(value: Option<f32>) -> View {
486 View::new(
487 0,
488 ViewKind::ProgressBar {
489 value: value.unwrap_or(0.0),
490 min: 0.0,
491 max: 1.0,
492 circular: true,
493 },
494 )
495 .modifier(Modifier::new().size(48.0, 48.0))
496 .semantics(Semantics {
497 role: Role::ProgressBar,
498 label: None,
499 focused: false,
500 enabled: true,
501 })
502}