1#[cfg(test)]
28#[path = "segmented_tests.rs"]
29mod tests;
30
31use std::cell::Cell;
32use std::rc::Rc;
33use std::sync::Arc;
34
35use crate::color::Color;
36use crate::draw_ctx::DrawCtx;
37use crate::event::{Event, EventResult, Key, MouseButton};
38use crate::geometry::{Point, Rect, Size};
39use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
40use crate::text::Font;
41use crate::theme::Visuals;
42use crate::widget::Widget;
43use crate::widgets::label::{Label, LabelAlign};
44
45const REGULAR_H: f64 = 24.0;
47const COMPACT_H: f64 = 20.0;
49const REGULAR_FONT: f64 = 13.0;
51const COMPACT_FONT: f64 = 11.0;
52const REGULAR_PAD: f64 = 12.0;
54const COMPACT_PAD: f64 = 8.0;
55const REGULAR_R: f64 = 6.0;
57const COMPACT_R: f64 = 5.0;
58const FILL_INSET: f64 = 1.0;
61const DIVIDER_INSET: f64 = 4.0;
63const KAPPA: f64 = 0.552_284_75;
65
66pub struct SegmentedControl {
68 bounds: Rect,
69 children: Vec<Box<dyn Widget>>,
71 base: WidgetBase,
72 labels: Vec<String>,
73 selected: Rc<Cell<usize>>,
75 font: Arc<Font>,
76 font_size: f64,
77 compact: bool,
78 fit_width: bool,
82 on_change: Option<Box<dyn FnMut(usize)>>,
83 enabled_fn: Option<Rc<dyn Fn() -> bool>>,
85 segment_enabled_fn: Option<Rc<dyn Fn(usize) -> bool>>,
87 segments: Vec<Rect>,
89 hovered: Option<usize>,
90 pressed: Option<usize>,
91 focused: bool,
92}
93
94impl SegmentedControl {
95 pub fn new(labels: Vec<impl Into<String>>, selected: Rc<Cell<usize>>, font: Arc<Font>) -> Self {
99 let labels: Vec<String> = labels.into_iter().map(Into::into).collect();
100 let font_size = REGULAR_FONT;
101 let children = Self::build_labels(&labels, &font, font_size);
102 let ctl = Self {
103 bounds: Rect::default(),
104 children,
105 base: WidgetBase::new(),
106 labels,
107 selected,
108 font,
109 font_size,
110 compact: false,
111 fit_width: false,
112 on_change: None,
113 enabled_fn: None,
114 segment_enabled_fn: None,
115 segments: Vec::new(),
116 hovered: None,
117 pressed: None,
118 focused: false,
119 };
120 ctl.clamp_selection();
121 ctl
122 }
123
124 fn build_labels(labels: &[String], font: &Arc<Font>, font_size: f64) -> Vec<Box<dyn Widget>> {
125 labels
126 .iter()
127 .map(|text| {
128 Box::new(
129 Label::new(text.as_str(), Arc::clone(font))
130 .with_font_size(font_size)
131 .with_align(LabelAlign::Center),
132 ) as Box<dyn Widget>
133 })
134 .collect()
135 }
136
137 pub fn with_font_size(mut self, size: f64) -> Self {
139 self.font_size = size;
140 self.children = Self::build_labels(&self.labels, &self.font, size);
141 self
142 }
143
144 pub fn with_compact(mut self) -> Self {
148 self.compact = true;
149 if (self.font_size - REGULAR_FONT).abs() < f64::EPSILON {
150 self.font_size = COMPACT_FONT;
151 self.children = Self::build_labels(&self.labels, &self.font, COMPACT_FONT);
152 }
153 self
154 }
155
156 pub fn with_fit_width(mut self, fit: bool) -> Self {
159 self.fit_width = fit;
160 self
161 }
162
163 pub fn on_change(mut self, cb: impl FnMut(usize) + 'static) -> Self {
166 self.on_change = Some(Box::new(cb));
167 self
168 }
169
170 pub fn with_enabled_fn(mut self, f: impl Fn() -> bool + 'static) -> Self {
173 self.enabled_fn = Some(Rc::new(f));
174 self
175 }
176
177 pub fn with_segment_enabled_fn(mut self, f: impl Fn(usize) -> bool + 'static) -> Self {
181 self.segment_enabled_fn = Some(Rc::new(f));
182 self
183 }
184
185 pub fn with_margin(mut self, m: Insets) -> Self {
186 self.base.margin = m;
187 self
188 }
189 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
190 self.base.h_anchor = h;
191 self
192 }
193 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
194 self.base.v_anchor = v;
195 self
196 }
197 pub fn with_min_size(mut self, s: Size) -> Self {
198 self.base.min_size = s;
199 self
200 }
201 pub fn with_max_size(mut self, s: Size) -> Self {
202 self.base.max_size = s;
203 self
204 }
205
206 pub fn selected(&self) -> usize {
208 let n = self.labels.len();
209 if n == 0 {
210 0
211 } else {
212 self.selected.get().min(n - 1)
213 }
214 }
215
216 pub fn set_selected(&mut self, idx: usize) {
218 if idx < self.labels.len() {
219 self.selected.set(idx);
220 crate::animation::request_draw();
221 }
222 }
223
224 pub fn selected_cell(&self) -> Rc<Cell<usize>> {
226 Rc::clone(&self.selected)
227 }
228
229 pub fn labels(&self) -> &[String] {
230 &self.labels
231 }
232
233 fn clamp_selection(&self) {
235 let n = self.labels.len();
236 if n > 0 && self.selected.get() >= n {
237 self.selected.set(n - 1);
238 }
239 }
240
241 fn is_enabled(&self) -> bool {
242 self.enabled_fn.as_ref().map(|f| f()).unwrap_or(true)
243 }
244
245 fn segment_enabled(&self, i: usize) -> bool {
246 self.is_enabled()
247 && self
248 .segment_enabled_fn
249 .as_ref()
250 .map(|f| f(i))
251 .unwrap_or(true)
252 }
253
254 fn height(&self) -> f64 {
255 let base = if self.compact { COMPACT_H } else { REGULAR_H };
256 base.max(self.font_size * 1.6)
257 .max(self.base.min_size.height)
258 }
259
260 fn corner_radius(&self) -> f64 {
261 if self.compact {
262 COMPACT_R
263 } else {
264 REGULAR_R
265 }
266 }
267
268 fn pad_h(&self) -> f64 {
269 if self.compact {
270 COMPACT_PAD
271 } else {
272 REGULAR_PAD
273 }
274 }
275
276 fn segment_at(&self, pos: Point) -> Option<usize> {
278 self.segments.iter().position(|r| {
279 pos.x >= r.x && pos.x < r.x + r.width && pos.y >= r.y && pos.y < r.y + r.height
280 })
281 }
282
283 fn choose(&mut self, idx: usize) {
286 if idx >= self.labels.len() || !self.segment_enabled(idx) {
287 return;
288 }
289 let was = self.selected();
290 self.selected.set(idx);
291 if was != idx {
292 if let Some(cb) = self.on_change.as_mut() {
293 cb(idx);
294 }
295 }
296 crate::animation::request_draw();
297 }
298
299 fn step_selection(&self, from: usize, dir: i64) -> Option<usize> {
301 let n = self.labels.len() as i64;
302 let mut i = from as i64 + dir;
303 while i >= 0 && i < n {
304 if self.segment_enabled(i as usize) {
305 return Some(i as usize);
306 }
307 i += dir;
308 }
309 None
310 }
311
312 fn segment_widths(&mut self, available: Size) -> Vec<f64> {
315 let n = self.labels.len();
316 if n == 0 {
317 return Vec::new();
318 }
319 let h = self.height();
320 let pad = self.pad_h();
321 let natural: Vec<f64> = self
322 .children
323 .iter_mut()
324 .map(|c| c.layout(Size::new(available.width.max(0.0), h)).width + 2.0 * pad)
325 .collect();
326 let mut widths = if self.fit_width {
327 natural
328 } else {
329 let w = natural.iter().cloned().fold(0.0, f64::max);
330 vec![w; n]
331 };
332 let total: f64 = widths.iter().sum();
333 let target = if self.base.h_anchor.is_stretch() {
334 available.width.max(self.base.min_size.width)
335 } else {
336 total.max(self.base.min_size.width)
337 };
338 let target = if available.width > 0.0 {
339 target.min(available.width)
340 } else {
341 target
342 };
343 if total > 0.0 && (total - target).abs() > 1e-6 {
344 if self.fit_width {
345 let k = target / total;
348 for w in widths.iter_mut() {
349 *w *= k;
350 }
351 } else {
352 Self::distribute_whole_pixels(&mut widths, target);
357 }
358 }
359 widths
360 }
361
362 fn distribute_whole_pixels(widths: &mut [f64], target: f64) {
366 let n = widths.len();
367 if n == 0 {
368 return;
369 }
370 let whole = target.max(0.0).floor();
371 let base = (whole / n as f64).floor();
372 let extra = (whole - base * n as f64) as usize;
373 for (i, w) in widths.iter_mut().enumerate() {
374 *w = if i < extra { base + 1.0 } else { base };
375 }
376 }
377
378 fn path_rect_corners(ctx: &mut dyn DrawCtx, r: Rect, rl: f64, rr: f64) {
382 let rl = rl.min(r.width * 0.5).min(r.height * 0.5).max(0.0);
383 let rr = rr.min(r.width * 0.5).min(r.height * 0.5).max(0.0);
384 let (x0, y0, x1, y1) = (r.x, r.y, r.x + r.width, r.y + r.height);
385 let kl = rl * KAPPA;
386 let kr = rr * KAPPA;
387 ctx.move_to(x0 + rl, y0);
388 ctx.line_to(x1 - rr, y0);
389 if rr > 0.0 {
390 ctx.cubic_to(x1 - rr + kr, y0, x1, y0 + rr - kr, x1, y0 + rr);
391 }
392 ctx.line_to(x1, y1 - rr);
393 if rr > 0.0 {
394 ctx.cubic_to(x1, y1 - rr + kr, x1 - rr + kr, y1, x1 - rr, y1);
395 }
396 ctx.line_to(x0 + rl, y1);
397 if rl > 0.0 {
398 ctx.cubic_to(x0 + rl - kl, y1, x0, y1 - rl + kl, x0, y1 - rl);
399 }
400 ctx.line_to(x0, y0 + rl);
401 if rl > 0.0 {
402 ctx.cubic_to(x0, y0 + rl - kl, x0 + rl - kl, y0, x0 + rl, y0);
403 }
404 ctx.close_path();
405 }
406
407 fn fill_segment(&self, ctx: &mut dyn DrawCtx, i: usize, color: Color) {
411 let Some(seg) = self.segments.get(i) else {
412 return;
413 };
414 let n = self.segments.len();
415 let outer_r = (self.corner_radius() - FILL_INSET).max(0.0);
416 let rl = if i == 0 { outer_r } else { 0.0 };
417 let rr = if i + 1 == n { outer_r } else { 0.0 };
418 let inset = Rect::new(
419 seg.x + if i == 0 { FILL_INSET } else { 0.0 },
420 seg.y + FILL_INSET,
421 (seg.width
422 - if i == 0 { FILL_INSET } else { 0.0 }
423 - if i + 1 == n { FILL_INSET } else { 0.0 })
424 .max(0.0),
425 (seg.height - 2.0 * FILL_INSET).max(0.0),
426 );
427 ctx.set_fill_color(color);
428 ctx.begin_path();
429 Self::path_rect_corners(ctx, inset, rl, rr);
430 ctx.fill();
431 }
432
433 fn disabled_colors(v: &Visuals) -> (Color, Color, Color) {
436 if v.is_dark() {
437 (
438 v.window_fill,
439 Color::rgba(1.0, 1.0, 1.0, 0.22),
440 v.text_dim.with_alpha(0.42),
441 )
442 } else {
443 (v.track_bg, v.widget_stroke.with_alpha(0.45), v.text_dim)
444 }
445 }
446
447 fn label_color(&self, v: &Visuals, i: usize, selected: usize, enabled: bool) -> Color {
449 if !enabled || !self.segment_enabled(i) {
450 Self::disabled_colors(v).2
451 } else if i == selected {
452 Color::white()
454 } else {
455 v.text_color
456 }
457 }
458
459 fn handle_key(&mut self, key: &Key) -> EventResult {
460 let n = self.labels.len();
461 if n == 0 {
462 return EventResult::Ignored;
463 }
464 let cur = self.selected();
465 let next = match key {
466 Key::ArrowLeft | Key::ArrowUp => self.step_selection(cur, -1),
467 Key::ArrowRight | Key::ArrowDown => self.step_selection(cur, 1),
468 Key::Home => (0..n).find(|&i| self.segment_enabled(i)),
469 Key::End => (0..n).rev().find(|&i| self.segment_enabled(i)),
470 _ => return EventResult::Ignored,
471 };
472 match next {
473 Some(i) if i != cur => {
474 self.choose(i);
475 EventResult::Consumed
476 }
477 Some(_) => EventResult::Consumed,
480 None => EventResult::Ignored,
481 }
482 }
483}
484
485impl Widget for SegmentedControl {
486 fn type_name(&self) -> &'static str {
487 "SegmentedControl"
488 }
489 fn bounds(&self) -> Rect {
490 self.bounds
491 }
492 fn set_bounds(&mut self, b: Rect) {
493 self.bounds = b;
494 }
495 fn children(&self) -> &[Box<dyn Widget>] {
496 &self.children
497 }
498 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
499 &mut self.children
500 }
501 fn is_focusable(&self) -> bool {
502 self.is_enabled() && !self.labels.is_empty()
503 }
504 fn margin(&self) -> Insets {
505 self.base.margin
506 }
507 fn widget_base(&self) -> Option<&WidgetBase> {
508 Some(&self.base)
509 }
510 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
511 Some(&mut self.base)
512 }
513 fn h_anchor(&self) -> HAnchor {
514 self.base.h_anchor
515 }
516 fn v_anchor(&self) -> VAnchor {
517 self.base.v_anchor
518 }
519 fn min_size(&self) -> Size {
520 self.base.min_size
521 }
522 fn max_size(&self) -> Size {
523 self.base.max_size
524 }
525
526 fn measure_min_height(&self, _available_w: f64) -> f64 {
527 self.height()
528 }
529
530 fn layout(&mut self, available: Size) -> Size {
531 self.clamp_selection();
532 let h = self.height();
533 let widths = self.segment_widths(available);
534 let mut x = 0.0;
535 self.segments.clear();
536 for (i, w) in widths.iter().enumerate() {
537 let seg = Rect::new(x, 0.0, *w, h);
538 self.segments.push(seg);
539 if let Some(child) = self.children.get_mut(i) {
540 let s = child.layout(Size::new(*w, h));
541 let lw = s.width.min(*w);
542 child.set_bounds(Rect::new(
543 x + ((*w - lw) * 0.5).max(0.0),
544 ((h - s.height) * 0.5).max(0.0),
545 lw,
546 s.height,
547 ));
548 }
549 x += w;
550 }
551 let size = Size::new(x, h);
552 self.bounds = Rect::new(self.bounds.x, self.bounds.y, size.width, size.height);
553 size
554 }
555
556 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
557 let v = ctx.visuals();
558 let w = self.bounds.width;
559 let h = self.bounds.height;
560 let r = self.corner_radius();
561 let enabled = self.is_enabled();
562 let selected = self.selected();
563 let n = self.segments.len();
564 let (disabled_bg, disabled_stroke, _) = Self::disabled_colors(&v);
565
566 if enabled && self.focused {
568 ctx.set_stroke_color(v.accent_focus);
569 ctx.set_line_width(1.5);
570 ctx.begin_path();
571 ctx.rounded_rect(0.75, 0.75, (w - 1.5).max(0.0), (h - 1.5).max(0.0), r);
572 ctx.stroke();
573 }
574
575 ctx.set_fill_color(if enabled { v.widget_bg } else { disabled_bg });
577 ctx.begin_path();
578 ctx.rounded_rect(0.0, 0.0, w, h, r);
579 ctx.fill();
580
581 if n > 0 {
582 if enabled {
584 if let Some(hi) = self.hovered {
585 if hi != selected && self.segment_enabled(hi) {
586 self.fill_segment(ctx, hi, v.widget_bg_hovered);
587 }
588 }
589 }
590 if enabled {
592 let fill = if self.pressed == Some(selected) {
593 v.accent_pressed
594 } else if self.hovered == Some(selected) {
595 v.accent_hovered
596 } else {
597 v.accent
598 };
599 self.fill_segment(ctx, selected, fill);
600 } else {
601 self.fill_segment(ctx, selected, v.widget_bg_hovered);
602 }
603 ctx.set_stroke_color(if enabled {
606 v.widget_stroke
607 } else {
608 disabled_stroke
609 });
610 ctx.set_line_width(1.0);
611 for i in 1..n {
612 if i == selected || i - 1 == selected {
613 continue;
614 }
615 let x = self.segments[i].x.round() + 0.5;
616 ctx.begin_path();
617 ctx.move_to(x, DIVIDER_INSET);
618 ctx.line_to(x, h - DIVIDER_INSET);
619 ctx.stroke();
620 }
621 }
622
623 ctx.set_stroke_color(if enabled {
625 v.widget_stroke
626 } else {
627 disabled_stroke
628 });
629 ctx.set_line_width(1.0);
630 ctx.begin_path();
631 ctx.rounded_rect(0.5, 0.5, (w - 1.0).max(0.0), (h - 1.0).max(0.0), r);
632 ctx.stroke();
633
634 for i in 0..n {
637 let color = self.label_color(&v, i, selected, enabled);
638 if let Some(child) = self.children.get_mut(i) {
639 child.set_label_color(color);
640 }
641 }
642 }
643
644 fn on_event(&mut self, event: &Event) -> EventResult {
645 if !self.is_enabled() {
646 if self.hovered.is_some() || self.pressed.is_some() {
649 self.hovered = None;
650 self.pressed = None;
651 crate::animation::request_draw();
652 }
653 return EventResult::Ignored;
654 }
655 match event {
656 Event::MouseMove { pos } => {
657 let was = self.hovered;
658 self.hovered = self.segment_at(*pos).filter(|&i| self.segment_enabled(i));
659 if self.hovered.is_none() {
660 self.pressed = None;
661 }
662 if was != self.hovered {
663 crate::animation::request_draw();
664 return EventResult::Consumed;
665 }
666 EventResult::Ignored
667 }
668 Event::MouseDown {
669 button: MouseButton::Left,
670 pos,
671 ..
672 } => match self.segment_at(*pos) {
673 Some(i) if self.segment_enabled(i) => {
674 self.pressed = Some(i);
675 crate::animation::request_draw();
676 EventResult::Consumed
677 }
678 _ => EventResult::Ignored,
679 },
680 Event::MouseUp {
681 button: MouseButton::Left,
682 pos,
683 ..
684 } => {
685 let was = self.pressed.take();
686 match was {
687 Some(i) => {
688 if self.segment_at(*pos) == Some(i) {
691 self.choose(i);
692 }
693 crate::animation::request_draw();
694 EventResult::Consumed
695 }
696 None => EventResult::Ignored,
697 }
698 }
699 Event::KeyDown { key, .. } => self.handle_key(key),
700 Event::FocusGained => {
701 let was = self.focused;
702 self.focused = true;
703 if !was {
704 crate::animation::request_draw();
705 EventResult::Consumed
706 } else {
707 EventResult::Ignored
708 }
709 }
710 Event::FocusLost => {
711 let was = self.focused || self.pressed.is_some();
712 self.focused = false;
713 self.pressed = None;
714 if was {
715 crate::animation::request_draw();
716 EventResult::Consumed
717 } else {
718 EventResult::Ignored
719 }
720 }
721 _ => EventResult::Ignored,
722 }
723 }
724
725 fn properties(&self) -> Vec<(&'static str, String)> {
726 vec![
727 ("segments", self.labels.join(" | ")),
728 ("selected", self.selected().to_string()),
729 ("compact", self.compact.to_string()),
730 ("fit_width", self.fit_width.to_string()),
731 ]
732 }
733}