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