Skip to main content

repose_material/material3/
segmented_button.rs

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