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, ViewExt,
10    anim::{animate_color, animate_f32},
11};
12
13use super::*;
14
15/// A single tab definition for use with `TabRow`.
16pub struct Tab {
17    pub label: String,
18    pub icon: Option<View>,
19    pub on_click: Rc<dyn Fn()>,
20    pub enabled: bool,
21    pub interaction_source: Option<MutableInteractionSource>,
22}
23
24/// Configuration for [`TabRow`].
25#[derive(Clone, Debug)]
26pub struct TabRowConfig {
27    pub modifier: Modifier,
28    pub container_color: Color,
29    pub selected_content_color: Color,
30    pub unselected_content_color: Color,
31    pub indicator_color: Color,
32    pub height: f32,
33    pub indicator_height: f32,
34}
35
36impl Default for TabRowConfig {
37    fn default() -> Self {
38        Self {
39            modifier: Modifier::new(),
40            container_color: TabDefaults::container_color(),
41            selected_content_color: TabDefaults::selected_content_color(),
42            unselected_content_color: TabDefaults::unselected_content_color(),
43            indicator_color: TabDefaults::indicator_color(),
44            height: TabDefaults::HEIGHT,
45            indicator_height: TabDefaults::INDICATOR_HEIGHT,
46        }
47    }
48}
49
50static TABROW_COUNTER: AtomicU64 = AtomicU64::new(0);
51
52/// M3 Tab Row -> a horizontal row of tabs with per-tab animated-height indicators.
53/// Text colors animate with DefaultEffects (spring_crit 40.0).
54/// Indicator height animates with DefaultEffects (spring_crit 40.0).
55pub fn TabRow(selected_index: usize, tabs: Vec<Tab>, config: TabRowConfig) -> View {
56    let th = theme();
57    let id = remember(|| TABROW_COUNTER.fetch_add(1, Ordering::Relaxed));
58    let default_effects = AnimationSpec::spring_crit(40.0);
59    Column(Modifier::new().fill_max_width().then(config.modifier)).child((
60        Row(Modifier::new()
61            .fill_max_width()
62            .height(config.height)
63            .background(config.container_color)
64            .semantics(Semantics::new(Role::Container).with_selectable_group()))
65        .child(
66            tabs.into_iter()
67                .enumerate()
68                .map(|(i, tab)| {
69                    let selected = i == selected_index;
70                    let is_enabled = tab.enabled;
71                    let color = animate_color(
72                        format!("tab_clr_{}_{}", id, i),
73                        if selected {
74                            config.selected_content_color
75                        } else {
76                            config.unselected_content_color
77                        },
78                        default_effects,
79                    );
80                    let indicator_h = animate_f32(
81                        format!("tab_ind_h_{}_{}", id, i),
82                        if selected {
83                            config.indicator_height
84                        } else {
85                            0.0
86                        },
87                        default_effects,
88                    );
89                    let cb = tab.on_click.clone();
90                    let tab_source: Rc<MutableInteractionSource> = tab
91                        .interaction_source
92                        .clone()
93                        .map(Rc::new)
94                        .unwrap_or_else(|| remember(MutableInteractionSource::new));
95
96                    let mut tab_m = Modifier::new()
97                        .flex_grow(1.0)
98                        .fill_max_height()
99                        .interaction_source(&tab_source)
100                        .align_items(AlignItems::CENTER)
101                        .justify_content(JustifyContent::CENTER)
102                        .state_colors(StateColors {
103                            default: Color::TRANSPARENT,
104                            hovered: th.on_surface.with_alpha_f32(0.08),
105                            pressed: th.on_surface.with_alpha_f32(0.12),
106                            dragged: 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}