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, ViewExt,
10 anim::{animate_color, animate_f32},
11};
12
13use super::*;
14
15pub 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#[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
52pub 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 .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
101 color: Some(th.on_surface),
102 bounded: true,
103 ..Default::default()
104 }))
105 .align_items(AlignItems::CENTER)
106 .justify_content(JustifyContent::CENTER)
107 .state_colors(StateColors {
108 default: Color::TRANSPARENT,
109 hovered: Color::TRANSPARENT,
110 focused: Color::TRANSPARENT,
111 pressed: Color::TRANSPARENT,
112 dragged: th.on_surface.with_alpha_f32(0.12),
113 disabled: Color::TRANSPARENT,
114 })
115 .semantics(Semantics::new(Role::Tab).with_label(&tab.label));
116
117 if is_enabled {
118 tab_m = tab_m.clickable().on_click(move || cb());
119 }
120
121 Column(tab_m).child((
122 tab.icon.unwrap_or(Box(Modifier::new())),
123 Text(tab.label)
124 .color(color)
125 .size(th.typography.title_small)
126 .single_line(),
127 Box(Modifier::new()
128 .fill_max_width()
129 .height(indicator_h)
130 .background(config.indicator_color)
131 .clip_rounded(TabDefaults::INDICATOR_CORNER)),
132 ))
133 })
134 .collect::<Vec<_>>(),
135 ),
136 Box(Modifier::new()
138 .fill_max_width()
139 .height(1.0)
140 .background(th.outline_variant)),
141 ))
142}