1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
use gpui::{AnyElement, ElementId, IntoElement, white};
use std::rc::Rc;
use crate::prelude::*;
/// A horizontal row of connected segments where exactly one is active.
/// Mirrors `RadioButton`'s checked/on_click pattern, rendered as a single
/// connected pill row (visual family of Phase 2's `ButtonGroup`).
///
/// Exclusivity/state is caller-managed: the parent holds the active index
/// and passes it in via `.active(index)`.
#[derive(IntoElement, RegisterComponent)]
pub struct SegmentedControl {
id: ElementId,
segments: Vec<SharedString>,
active: usize,
disabled: bool,
on_change: Option<Rc<dyn Fn(usize, &mut Window, &mut App) + 'static>>,
}
impl SegmentedControl {
pub fn new(
id: impl Into<ElementId>,
segments: impl IntoIterator<Item = impl Into<SharedString>>,
) -> Self {
Self {
id: id.into(),
segments: segments.into_iter().map(Into::into).collect(),
active: 0,
disabled: false,
on_change: None,
}
}
/// Sets the active segment index.
pub fn active(mut self, index: usize) -> Self {
self.active = index;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Binds a handler called with the clicked segment's index.
pub fn on_change(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
}
impl RenderOnce for SegmentedControl {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let active = self.active;
let disabled = self.disabled;
let group_id = self.id.clone();
let on_change = self.on_change;
let mut row = h_flex()
.id(self.id)
.rounded_md()
.border_1()
.border_color(semantic::border(cx))
.bg(semantic::surface(cx))
.p(px(2.))
.gap_0p5();
for (i, segment) in self.segments.into_iter().enumerate() {
let is_active = i == active;
let on_change = on_change.clone();
let mut cell = h_flex()
.id((group_id.clone(), i.to_string()))
// Test-only (no-op in release builds, per `debug_selector`'s
// own doc comment): lets integration tests locate a segment's
// real rendered pixel bounds via `VisualTestContext::debug_bounds`
// and drive a genuine `simulate_click`, instead of calling
// `on_change` directly. Mirrors the existing `Tab`/`ContextMenu`
// precedent for this exact pattern.
.debug_selector({
let group_id = group_id.clone();
move || format!("SEGMENT-{}-{}", group_id, i)
})
.flex_1()
.justify_center()
.px_3()
.py_1()
.rounded_sm()
.when(is_active, |this| this.bg(palette::primary(600)))
.when(disabled, |this| this.opacity(0.5))
.child(
Label::new(segment)
.size(LabelSize::Small)
.color(if is_active {
Color::Custom(white())
} else {
Color::Default
}),
);
if !disabled {
cell = cell.cursor_pointer().on_click(move |_, window, cx| {
if let Some(handler) = &on_change {
handler(i, window, cx);
}
});
}
row = row.child(cell);
}
row
}
}
impl Component for SegmentedControl {
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn description() -> Option<&'static str> {
Some("A connected row of segments where exactly one is active.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_4()
.child(
SegmentedControl::new("segmented-default", ["Day", "Week", "Month"])
.active(1)
.into_any_element(),
)
.child(
SegmentedControl::new("segmented-disabled", ["List", "Grid"])
.disabled(true)
.into_any_element(),
)
.into_any_element(),
)
}
}