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 .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 Box(Modifier::new()
132 .fill_max_width()
133 .height(1.0)
134 .background(th.outline_variant)),
135 ))
136}