Skip to main content

repose_material/material3/
components.rs

1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4
5use repose_core::*;
6use repose_ui::{Box, Column, Row, Text, TextStyle, ViewExt};
7
8/// M3 Top App Bar (small). Displays a title with optional navigation icon and
9/// trailing action buttons.
10pub 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
46/// M3 Icon Button — a tappable circular container for an icon.
47pub 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
58/// M3 Filled Icon Button — icon button with a filled container background.
59pub 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
72/// M3 Filled Button — prominent action button with primary color fill.
73pub 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
98/// M3 Filled Tonal Button — a softer variant using secondary container colors.
99pub 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
124/// M3 Outlined Button — button with an outline border and no fill.
125pub 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
150/// M3 Text Button — a low-emphasis button with only a text label.
151pub 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
175/// M3 Floating Action Button (regular, 56dp).
176pub 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
189/// M3 Small FAB (40dp).
190pub 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
203/// M3 Large FAB (96dp).
204pub 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
217/// M3 Extended FAB — FAB with icon + label.
218pub 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
249/// M3 Horizontal Divider — a thin 1dp line.
250pub 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
258/// M3 Vertical Divider — a thin 1dp vertical line.
259pub 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
267/// M3 Badge — a small notification indicator. If `label` is `None`, shows a
268/// small 6dp dot; otherwise shows the label text inside a 16dp pill.
269pub 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
301/// M3 List Item — a single row in a list with optional leading/trailing content.
302pub 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
375/// A single tab definition for use with `TabRow`.
376pub struct Tab {
377    pub label: String,
378    pub icon: Option<View>,
379    pub on_click: Rc<dyn Fn()>,
380}
381
382/// M3 Tab Row — a horizontal row of tabs with an active indicator.
383pub 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
431/// A single segment definition for `SegmentedButton`.
432pub struct Segment {
433    pub label: String,
434    pub icon: Option<View>,
435    pub on_click: Rc<dyn Fn()>,
436}
437
438/// M3 Segmented Button — a row of toggle segments. `selected` contains the
439/// indices of selected segments (single-select: pass a single-element set).
440pub 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
497/// M3 Circular Progress Indicator. Uses the built-in `ProgressBar` view kind
498/// with `circular: true`.
499///
500/// - `value`: `Some(0.0..=1.0)` for determinate, `None` for indeterminate.
501pub 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}