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(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
94/// M3 Filled Tonal Button — uses secondary container colors.
95pub 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
116/// M3 Outlined Button — button with an outline border and no fill.
117pub 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
138/// M3 Text Button — a low-emphasis button.
139pub 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
159/// M3 Floating Action Button (regular, 56dp).
160pub 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
173/// M3 Small FAB (40dp).
174pub 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
187/// M3 Large FAB (96dp).
188pub 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
201/// M3 Extended FAB — FAB with icon + label.
202pub 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
233/// M3 Horizontal Divider — a thin 1dp line.
234pub 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
242/// M3 Vertical Divider — a thin 1dp vertical line.
243pub 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
251/// M3 Badge — a small notification indicator. If `label` is `None`, shows a
252/// small 6dp dot; otherwise shows the label text inside a 16dp pill.
253pub 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
285/// M3 List Item — a single row in a list with optional leading/trailing content.
286pub 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
359/// A single tab definition for use with `TabRow`.
360pub struct Tab {
361    pub label: String,
362    pub icon: Option<View>,
363    pub on_click: Rc<dyn Fn()>,
364}
365
366/// M3 Tab Row — a horizontal row of tabs with an active indicator.
367pub 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
415/// A single segment definition for `SegmentedButton`.
416pub struct Segment {
417    pub label: String,
418    pub icon: Option<View>,
419    pub on_click: Rc<dyn Fn()>,
420}
421
422/// M3 Segmented Button — a row of toggle segments. `selected` contains the
423/// indices of selected segments (single-select: pass a single-element set).
424pub 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
481/// M3 Circular Progress Indicator. Uses the built-in `ProgressBar` view kind
482/// with `circular: true`.
483///
484/// - `value`: `Some(0.0..=1.0)` for determinate, `None` for indeterminate.
485pub 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}