Skip to main content

guise/input/
segmented.rs

1//! `SegmentedControl` — a stateful single-choice segmented switch (gpui entity).
2
3use gpui::prelude::*;
4use gpui::{div, px, App, Context, Entity, EventEmitter, IntoElement, SharedString, Window};
5
6use super::control_metrics;
7use crate::devtools::Probed;
8use crate::reactive::Signal;
9use crate::theme::{theme, ColorName, Size};
10
11/// Emitted when the selected segment changes. Carries the option index.
12#[derive(Debug, Clone)]
13pub struct SegmentedControlEvent(pub usize);
14
15/// A segmented control. Create with
16/// `cx.new(|cx| SegmentedControl::new(cx).data(["Day", "Week", "Month"]))`.
17pub struct SegmentedControl {
18    options: Vec<SharedString>,
19    selected: usize,
20    size: Size,
21}
22
23impl EventEmitter<SegmentedControlEvent> for SegmentedControl {}
24
25impl SegmentedControl {
26    pub fn new(_cx: &mut Context<Self>) -> Self {
27        SegmentedControl {
28            options: Vec::new(),
29            selected: 0,
30            size: Size::Sm,
31        }
32    }
33
34    pub fn data<I, S>(mut self, options: I) -> Self
35    where
36        I: IntoIterator<Item = S>,
37        S: Into<SharedString>,
38    {
39        self.options = options.into_iter().map(Into::into).collect();
40        self.selected = self.selected.min(self.options.len().saturating_sub(1));
41        self
42    }
43
44    pub fn selected(mut self, index: usize) -> Self {
45        self.selected = index;
46        self
47    }
48
49    pub fn size(mut self, size: Size) -> Self {
50        self.size = size;
51        self
52    }
53
54    pub fn selected_index(&self) -> usize {
55        self.selected
56    }
57
58    /// Two-way bind this control's selection to a `Signal<usize>`. The signal
59    /// is the source of truth: the control adopts its index now, clicks write
60    /// back through [`Signal::set_if_changed`], and signal writes move the
61    /// selection without emitting [`SegmentedControlEvent`]. Equality guards
62    /// on both directions prevent update loops.
63    pub fn bind(entity: &Entity<SegmentedControl>, signal: &Signal<usize>, cx: &mut App) {
64        let initial = signal.get(cx);
65        entity.update(cx, |this, cx| this.sync_selected(initial, cx));
66        let sink = signal.clone();
67        cx.subscribe(
68            entity,
69            move |_control, event: &SegmentedControlEvent, cx| {
70                sink.set_if_changed(cx, event.0);
71            },
72        )
73        .detach();
74        let control = entity.downgrade();
75        cx.observe(signal.entity(), move |observed, cx| {
76            let index = *observed.read(cx);
77            control
78                .update(cx, |this, cx| this.sync_selected(index, cx))
79                .ok();
80        })
81        .detach();
82    }
83
84    /// Programmatic set: repaint without emitting an event.
85    fn sync_selected(&mut self, index: usize, cx: &mut Context<Self>) {
86        let selected = index.min(self.options.len().saturating_sub(1));
87        if self.selected != selected {
88            self.selected = selected;
89            cx.notify();
90        }
91    }
92}
93
94impl Render for SegmentedControl {
95    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
96        let t = theme(cx);
97        let (height, pad_x, font) = control_metrics(self.size);
98        let radius = t.radius(Size::Sm);
99        let track = t
100            .color(ColorName::Gray, if t.scheme.is_dark() { 8 } else { 1 })
101            .hsla();
102        let active_bg = t.surface().hsla();
103        let active_fg = t.text().hsla();
104        let inactive_fg = t.dimmed().hsla();
105
106        let count = self.options.len();
107        let selected = if count == 0 {
108            0
109        } else {
110            self.selected.min(count - 1)
111        };
112
113        let mut row = div()
114            .flex()
115            .items_center()
116            .gap(px(2.0))
117            .p(px(3.0))
118            .rounded(px(radius + 2.0))
119            .bg(track);
120
121        for (i, option) in self.options.iter().enumerate() {
122            let is_active = i == selected;
123            let mut seg = div()
124                .id(("guise-segment", i))
125                .flex()
126                .items_center()
127                .justify_center()
128                .h(px(height - 6.0))
129                .px(px(pad_x))
130                .rounded(px(radius))
131                .text_size(px(font))
132                .text_color(if is_active { active_fg } else { inactive_fg })
133                .child(option.clone())
134                .on_click(cx.listener(move |this, _ev, _window, cx| {
135                    this.selected = i;
136                    cx.emit(SegmentedControlEvent(i));
137                    cx.notify();
138                }));
139            if is_active {
140                seg = seg.bg(active_bg).shadow_sm();
141            }
142            row = row.child(seg);
143        }
144        row.probe("SegmentedControl")
145    }
146}