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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
//! A "segmented" toggle button with an LED dot.
use egui::{
Color32, CornerRadius, Response, Sense, Stroke, Ui, Vec2, Widget, WidgetInfo, WidgetText,
WidgetType,
};
use crate::button::ButtonSize;
use crate::theme::{with_alpha, Accent, Theme};
/// A toggle button with a built-in LED dot.
///
/// `SegmentedButton` shares [`ButtonSize`] with [`Button`](crate::Button), so a
/// mixed action row stays aligned when both widgets pass matching
/// `.size(...)` values.
///
/// ```no_run
/// # use elegance::{Accent, Button, ButtonSize, SegmentedButton};
/// # egui::__run_test_ui(|ui| {
/// let mut on = false;
/// // Default: ButtonSize::Medium — aligns with Medium Button.
/// ui.add(SegmentedButton::new(&mut on, "Continuous").accent(Accent::Green));
///
/// // Chunkier row — Button + SegmentedButton, same height.
/// ui.horizontal(|ui| {
/// ui.add(Button::new("Collect").size(ButtonSize::Large));
/// ui.add(
/// SegmentedButton::new(&mut on, "Continuous")
/// .accent(Accent::Green)
/// .size(ButtonSize::Large),
/// );
/// });
/// # });
/// ```
#[must_use = "Add with `ui.add(...)`."]
pub struct SegmentedButton<'a> {
on: &'a mut bool,
label: WidgetText,
accent: Accent,
size: ButtonSize,
/// When `true`, the `on` state is dimmed — useful for showing that a
/// linked toggle or prerequisite isn't active.
dim_when_on: bool,
rounded: bool,
corner_radius: Option<CornerRadius>,
min_width: Option<f32>,
}
impl<'a> std::fmt::Debug for SegmentedButton<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SegmentedButton")
.field("on", &*self.on)
.field("label", &self.label.text())
.field("accent", &self.accent)
.field("size", &self.size)
.field("dim_when_on", &self.dim_when_on)
.field("rounded", &self.rounded)
.field("corner_radius", &self.corner_radius)
.field("min_width", &self.min_width)
.finish()
}
}
impl<'a> SegmentedButton<'a> {
/// Create a segmented button bound to `on` with the given label.
pub fn new(on: &'a mut bool, label: impl Into<WidgetText>) -> Self {
Self {
on,
label: label.into(),
accent: Accent::Green,
size: ButtonSize::Medium,
dim_when_on: false,
rounded: true,
corner_radius: None,
min_width: None,
}
}
/// Pick the `on`-state colour from one of the theme's accents. Default: [`Accent::Green`].
pub fn accent(mut self, accent: Accent) -> Self {
self.accent = accent;
self
}
/// Pick a size preset matching [`Button`](crate::Button)'s sizes so a
/// mixed `Button` + `SegmentedButton` row stays aligned at any size.
/// Default: [`ButtonSize::Medium`].
#[inline]
pub fn size(mut self, size: ButtonSize) -> Self {
self.size = size;
self
}
/// When the button is on, render its fill dimmed and the label muted.
/// Used to indicate "enabled but not currently applicable".
pub fn dim_when_on(mut self, dim: bool) -> Self {
self.dim_when_on = dim;
self
}
/// Set whether the button has rounded corners. Disable for segmented
/// groups where neighbours share edges.
pub fn rounded(mut self, rounded: bool) -> Self {
self.rounded = rounded;
self
}
/// Explicitly set the corner radius (per-corner). Overrides [`Self::rounded`].
/// Useful for segmented strips where only the end cells should be rounded.
pub fn corner_radius(mut self, radius: impl Into<CornerRadius>) -> Self {
self.corner_radius = Some(radius.into());
self
}
/// Force the button to occupy at least this width. When wider than
/// the LED + text, the content is centered horizontally.
pub fn min_width(mut self, width: f32) -> Self {
self.min_width = Some(width);
self
}
fn on_fill(&self, theme: &Theme) -> Color32 {
theme.palette.accent_fill(self.accent)
}
fn on_fill_hover(&self, theme: &Theme) -> Color32 {
theme.palette.accent_hover(self.accent)
}
}
impl<'a> Widget for SegmentedButton<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let theme = Theme::current(ui.ctx());
let p = &theme.palette;
let padding = self.size.padding(&theme);
let font_size = self.size.font_size(&theme);
let led_size = 8.0;
let led_gap = 7.0;
let galley =
crate::theme::placeholder_galley(ui, self.label.text(), font_size, true, f32::INFINITY);
let content_w = led_size + led_gap + galley.size().x;
let mut desired = Vec2::new(
padding.x * 2.0 + content_w,
(padding.y * 2.0 + galley.size().y).max(font_size + 2.0 * padding.y),
);
if let Some(min_w) = self.min_width {
desired.x = desired.x.max(min_w);
}
let (rect, mut response) = ui.allocate_exact_size(desired, Sense::click());
if response.clicked() {
*self.on = !*self.on;
response.mark_changed();
}
if ui.is_rect_visible(rect) {
let on = *self.on;
let hovered = response.hovered();
let is_down = response.is_pointer_button_down_on();
let (fill, text_color, led_color, led_glow) = if on {
let mut fill = if is_down {
crate::theme::mix(self.on_fill_hover(&theme), Color32::BLACK, 0.1)
} else if hovered {
self.on_fill_hover(&theme)
} else {
self.on_fill(&theme)
};
let mut text = Color32::WHITE;
if self.dim_when_on {
fill = crate::theme::mix(fill, p.card, 0.55);
text = p.text_muted;
}
let led = Color32::WHITE;
let glow = !self.dim_when_on;
(fill, text, led, glow)
} else {
let fill = if hovered {
p.depth_tint(p.input_bg, 0.05)
} else {
p.input_bg
};
let text = if hovered { p.text_muted } else { p.text_faint };
let led = p.text_faint;
(fill, text, led, false)
};
let radius = self.corner_radius.unwrap_or_else(|| {
if self.rounded {
CornerRadius::same(theme.control_radius as u8 + 2)
} else {
CornerRadius::ZERO
}
});
ui.painter()
.rect(rect, radius, fill, Stroke::NONE, egui::StrokeKind::Inside);
// Center the LED + text combo within the allocated rect.
let content_start = rect.center().x - content_w * 0.5;
let led_center = egui::pos2(content_start + led_size * 0.5, rect.center().y);
if led_glow {
ui.painter().circle_filled(
led_center,
led_size * 0.5 + 2.0,
with_alpha(Color32::WHITE, 70),
);
}
ui.painter()
.circle_filled(led_center, led_size * 0.5, led_color);
let text_pos = egui::pos2(
led_center.x + led_size * 0.5 + led_gap,
rect.center().y - galley.size().y * 0.5,
);
ui.painter().galley(text_pos, galley, text_color);
}
response.widget_info(|| {
WidgetInfo::selected(WidgetType::Checkbox, true, *self.on, self.label.text())
});
response
}
}