repose_material/material3/
tab_row.rs1#![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
16pub 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#[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
53pub 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 dragged: th.on_surface.with_alpha_f32(0.12),
108 disabled: Color::TRANSPARENT,
109 })
110 .semantics(Semantics::new(Role::Tab).with_label(&tab.label));
111
112 if is_enabled {
113 tab_m = tab_m.clickable().on_click(move || cb());
114 }
115
116 Column(tab_m).child((
117 tab.icon.unwrap_or(Box(Modifier::new())),
118 Text(tab.label)
119 .color(color)
120 .size(th.typography.title_small)
121 .single_line(),
122 Box(Modifier::new()
123 .fill_max_width()
124 .height(indicator_h)
125 .background(config.indicator_color)
126 .clip_rounded(TabDefaults::INDICATOR_CORNER)),
127 ))
128 })
129 .collect::<Vec<_>>(),
130 ),
131 Box(Modifier::new()
133 .fill_max_width()
134 .height(1.0)
135 .background(th.outline_variant)),
136 ))
137}