Skip to main content

repose_material/material3/
tab_row.rs

1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use repose_core::animation::AnimationSpec;
7use repose_core::*;
8use repose_ui::{
9    Box, Column, Row, Text, TextStyle,
10    ViewExt,
11    anim::{animate_color, animate_f32},
12};
13
14use super::*;
15
16/// A single tab definition for use with `TabRow`.
17pub struct Tab {
18    pub label: String,
19    pub icon: Option<View>,
20    pub on_click: Rc<dyn Fn()>,
21    pub enabled: bool,
22    pub interaction_source: Option<MutableInteractionSource>,
23}
24
25/// Configuration for [`TabRow`].
26#[derive(Clone, Debug)]
27pub struct TabRowConfig {
28    pub modifier: Modifier,
29    pub container_color: Color,
30    pub selected_content_color: Color,
31    pub unselected_content_color: Color,
32    pub indicator_color: Color,
33    pub height: f32,
34    pub indicator_height: f32,
35}
36
37impl Default for TabRowConfig {
38    fn default() -> Self {
39        Self {
40            modifier: Modifier::new(),
41            container_color: TabDefaults::container_color(),
42            selected_content_color: TabDefaults::selected_content_color(),
43            unselected_content_color: TabDefaults::unselected_content_color(),
44            indicator_color: TabDefaults::indicator_color(),
45            height: TabDefaults::HEIGHT,
46            indicator_height: TabDefaults::INDICATOR_HEIGHT,
47        }
48    }
49}
50
51static TABROW_COUNTER: AtomicU64 = AtomicU64::new(0);
52
53/// M3 Tab Row -> a horizontal row of tabs with per-tab animated-height indicators.
54/// Text colors animate with DefaultEffects (spring_crit 40.0).
55/// Indicator height animates with DefaultEffects (spring_crit 40.0).
56pub fn TabRow(selected_index: usize, tabs: Vec<Tab>, config: TabRowConfig) -> View {
57    let th = theme();
58    let id = remember(|| TABROW_COUNTER.fetch_add(1, Ordering::Relaxed));
59    let default_effects = AnimationSpec::spring_crit(40.0);
60    Column(Modifier::new().fill_max_width().then(config.modifier)).child((
61        Row(Modifier::new()
62            .fill_max_width()
63            .height(config.height)
64            .background(config.container_color)
65            .semantics(Semantics::new(Role::Container).with_selectable_group()))
66        .child(
67            tabs.into_iter()
68                .enumerate()
69                .map(|(i, tab)| {
70                    let selected = i == selected_index;
71                    let is_enabled = tab.enabled;
72                    let color = animate_color(
73                        format!("tab_clr_{}_{}", id, i),
74                        if selected {
75                            config.selected_content_color
76                        } else {
77                            config.unselected_content_color
78                        },
79                        default_effects,
80                    );
81                    let indicator_h = animate_f32(
82                        format!("tab_ind_h_{}_{}", id, i),
83                        if selected {
84                            config.indicator_height
85                        } else {
86                            0.0
87                        },
88                        default_effects,
89                    );
90                    let cb = tab.on_click.clone();
91                    let tab_source: Rc<MutableInteractionSource> = tab
92                        .interaction_source
93                        .clone()
94                        .map(Rc::new)
95                        .unwrap_or_else(|| remember(MutableInteractionSource::new));
96
97                    let mut tab_m = Modifier::new()
98                        .flex_grow(1.0)
99                        .fill_max_height()
100                        .interaction_source(&*tab_source)
101                        .align_items(AlignItems::CENTER)
102                        .justify_content(JustifyContent::CENTER)
103                        .state_colors(StateColors {
104                            default: Color::TRANSPARENT,
105                            hovered: th.on_surface.with_alpha_f32(0.08),
106                            pressed: th.on_surface.with_alpha_f32(0.12),
107                            disabled: Color::TRANSPARENT,
108                        })
109                        .semantics(Semantics::new(Role::Tab).with_label(&tab.label));
110
111                    if is_enabled {
112                        tab_m = tab_m.clickable().on_click(move || cb());
113                    }
114
115                    Column(tab_m).child((
116                        tab.icon.unwrap_or(Box(Modifier::new())),
117                        Text(tab.label)
118                            .color(color)
119                            .size(th.typography.title_small)
120                            .single_line(),
121                        Box(Modifier::new()
122                            .fill_max_width()
123                            .height(indicator_h)
124                            .background(config.indicator_color)
125                            .clip_rounded(TabDefaults::INDICATOR_CORNER)),
126                    ))
127                })
128                .collect::<Vec<_>>(),
129        ),
130        // Divider
131        Box(Modifier::new()
132            .fill_max_width()
133            .height(1.0)
134            .background(th.outline_variant)),
135    ))
136}