repose_material/material3/
segmented_button.rs1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use repose_core::*;
7use repose_ui::{Box, Row, Text, TextStyle, ViewExt, anim::animate_color};
8
9use super::*;
10
11#[derive(Clone)]
13pub struct SegmentConfig {
14 pub label: String,
15 pub icon: Option<View>,
16 pub on_click: Rc<dyn Fn()>,
17 pub enabled: bool,
18 pub interaction_source: Option<MutableInteractionSource>,
19}
20
21impl Default for SegmentConfig {
22 fn default() -> Self {
23 Self {
24 label: String::new(),
25 icon: None,
26 on_click: Rc::new(|| {}),
27 enabled: true,
28 interaction_source: None,
29 }
30 }
31}
32
33#[derive(Clone, Debug)]
35pub struct SegmentedButtonConfig {
36 pub modifier: Modifier,
37 pub border_color: Color,
38 pub selected_container_color: Color,
39 pub selected_content_color: Color,
40 pub unselected_content_color: Color,
41 pub state_colors: StateColors,
42 pub height: f32,
43 pub shape_radius: f32,
44 pub content_padding: PaddingValues,
45}
46
47impl Default for SegmentedButtonConfig {
48 fn default() -> Self {
49 Self {
50 modifier: Modifier::new(),
51 border_color: SegmentedButtonDefaults::border_color(),
52 selected_container_color: SegmentedButtonDefaults::selected_container_color(),
53 selected_content_color: SegmentedButtonDefaults::selected_content_color(),
54 unselected_content_color: SegmentedButtonDefaults::unselected_content_color(),
55 state_colors: SegmentedButtonDefaults::state_colors_default(),
56 height: SegmentedButtonDefaults::HEIGHT,
57 shape_radius: SegmentedButtonDefaults::SHAPE_RADIUS,
58 content_padding: SegmentedButtonDefaults::CONTENT_PADDING,
59 }
60 }
61}
62
63static SEGBUTTON_COUNTER: AtomicU64 = AtomicU64::new(0);
64
65pub fn SegmentedButton(
70 selected: &[usize],
71 segments: Vec<SegmentConfig>,
72 config: SegmentedButtonConfig,
73) -> View {
74 let th = theme();
75 let count = segments.len();
76 let id = remember(|| SEGBUTTON_COUNTER.fetch_add(1, Ordering::Relaxed));
77 let spec = th.motion.color;
78 let shape_r = config.shape_radius;
79
80 let segment_radii = |i: usize| -> [f32; 4] {
82 if count == 1 {
83 [shape_r, shape_r, shape_r, shape_r]
84 } else if i == 0 {
85 [shape_r, 0.0, 0.0, shape_r]
86 } else if i == count - 1 {
87 [0.0, shape_r, shape_r, 0.0]
88 } else {
89 [0.0, 0.0, 0.0, 0.0]
90 }
91 };
92
93 Row(Modifier::new()
95 .height(config.height)
96 .border(1.0, config.border_color, shape_r)
97 .then(config.modifier))
98 .child(
99 segments
100 .into_iter()
101 .enumerate()
102 .map(|(i, seg)| {
103 let is_selected = selected.contains(&i);
104
105 let bg = animate_color(
106 format!("sb_bg_{}_{}", id, i),
107 if is_selected {
108 config.selected_container_color
109 } else {
110 Color::TRANSPARENT
111 },
112 spec,
113 );
114 let fg = animate_color(
115 format!("sb_fg_{}_{}", id, i),
116 if is_selected {
117 config.selected_content_color
118 } else {
119 config.unselected_content_color
120 },
121 spec,
122 );
123
124 let cb = seg.on_click.clone();
125 let radii = segment_radii(i);
126 let is_enabled = seg.enabled;
127 let seg_source: Rc<MutableInteractionSource> = seg
128 .interaction_source
129 .clone()
130 .map(Rc::new)
131 .unwrap_or_else(|| remember(MutableInteractionSource::new));
132
133 let state_colors = config.state_colors;
134 let content_modifier = Modifier::new()
135 .flex_grow(1.0)
136 .fill_max_height()
137 .clip_rounded_radii(radii)
138 .background(bg)
139 .state_colors(state_colors)
140 .interaction_source(&seg_source)
141 .align_items(AlignItems::CENTER)
142 .justify_content(JustifyContent::CENTER)
143 .padding_values(config.content_padding);
144
145 let content_modifier = if is_enabled {
146 content_modifier.clickable().on_click(move || cb())
147 } else {
148 content_modifier
149 };
150
151 Row(Modifier::new().flex_grow(1.0).fill_max_height()).child((
152 Row(content_modifier).child((
153 seg.icon.unwrap_or(Box(Modifier::new())),
154 Text(seg.label)
155 .color(fg)
156 .size(th.typography.label_large)
157 .single_line(),
158 )),
159 if i < count - 1 {
160 Box(Modifier::new()
161 .width(1.0)
162 .fill_max_height()
163 .background(th.outline))
164 } else {
165 Box(Modifier::new())
166 },
167 ))
168 })
169 .collect::<Vec<_>>(),
170 )
171}