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::*;
10
11/// Configuration for a single segment in [`SegmentedButton`].
12#[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/// Configuration for [`SegmentedButton`].
34#[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
65/// M3 Segmented Button - a row of toggle segments. `selected` contains the
66/// indices of selected segments (single-select: pass a single-element set).
67/// Each segment is shaped independently: first has rounded left corners,
68/// last has rounded right corners, middle segments are rectangular.
69pub 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    // corner order: [BL, BR, TR, TL]
81    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    // Outer border wraps the entire group. Internal dividers are inside each segment Row.
94    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}