1use std::sync::Arc;
2use rosace_core::types::{Point, Rect, Size};
3use rosace_render::Color;
4use super::{Widget, LayoutCtx, PaintCtx};
5
6pub struct SegmentedControl {
9 segments: Vec<String>,
10 selected: usize,
11 disabled: bool,
12 height: f32,
13 track_color: Option<Color>,
14 selected_color: Option<Color>,
15 color: Option<Color>,
16 selected_text_color: Option<Color>,
17 on_change: Option<Arc<dyn Fn(usize) + Send + Sync>>,
18}
19
20impl SegmentedControl {
21 pub fn new(segments: Vec<impl Into<String>>, selected: usize) -> Self {
22 Self {
23 segments: segments.into_iter().map(Into::into).collect(), selected, disabled: false, height: 34.0,
24 track_color: None, selected_color: None, color: None, selected_text_color: None,
25 on_change: None,
26 }
27 }
28 pub fn height(mut self, h: f32) -> Self { self.height = h; self }
29 pub fn disabled(mut self) -> Self { self.disabled = true; self }
30 pub fn track_color(mut self, c: Color) -> Self { self.track_color = Some(c); self }
32 pub fn selected_color(mut self, c: Color) -> Self { self.selected_color = Some(c); self }
34 pub fn color(mut self, c: Color) -> Self { self.color = Some(c); self }
36 pub fn selected_text_color(mut self, c: Color) -> Self { self.selected_text_color = Some(c); self }
38 pub fn on_change(mut self, f: impl Fn(usize) + Send + Sync + 'static) -> Self {
39 self.on_change = Some(Arc::new(f)); self
40 }
41}
42
43impl Widget for SegmentedControl {
44 fn layout(&self, ctx: &LayoutCtx) -> Size {
45 let w: f32 = self.segments.iter()
46 .map(|s| ctx.font.measure_text(s, 13.0) + 28.0)
47 .sum();
48 ctx.constraints.constrain(Size { width: w.max(120.0), height: self.height })
49 }
50 fn paint(&self, ctx: &mut PaintCtx) {
51 let r = ctx.rect;
52 let n = self.segments.len().max(1);
53 let seg_w = r.size.width / n as f32;
54 let radius = self.height / 2.0;
55 let (track, fg, sel_bg, sel_fg) = {
56 let t = &ctx.theme.colors;
57 (self.track_color.unwrap_or_else(|| ctx.tc(t.surface_variant)),
58 self.color.unwrap_or_else(|| ctx.tc(t.on_surface)),
59 self.selected_color.unwrap_or_else(|| ctx.tc(t.primary)),
60 self.selected_text_color.unwrap_or_else(|| ctx.tc(t.on_primary)))
61 };
62 let dim = if self.disabled { 0.45 } else { 1.0 };
63 let with_alpha = |c: Color, a: f32| Color::rgba(c.r, c.g, c.b, (a.clamp(0.0, 1.0) * 255.0).round() as u8);
64 let focused = !self.disabled && ctx.focus_node().is_focused();
65
66 ctx.fill_rrect(r, radius, with_alpha(track, dim));
68
69 let pos = ctx.animate_to(self.selected as f32, 0.0);
71 let px = r.origin.x + pos * seg_w;
72 let pill = Rect {
73 origin: Point { x: px + 3.0, y: r.origin.y + 3.0 },
74 size: Size { width: seg_w - 6.0, height: r.size.height - 6.0 },
75 };
76 ctx.fill_rrect(pill, radius - 3.0, with_alpha(sel_bg, dim));
77
78 for (i, label) in self.segments.iter().enumerate() {
79 let x = r.origin.x + i as f32 * seg_w;
80 let seg_rect = Rect { origin: Point { x, y: r.origin.y }, size: Size { width: seg_w, height: r.size.height } };
81 let mut child = ctx.child(seg_rect);
82
83 let hov = !self.disabled && child.hovered();
86 let prs = !self.disabled && child.pressed();
87 let nearness = (1.0 - (pos - i as f32).abs()).clamp(0.0, 1.0);
88 if (hov || prs) && nearness < 0.5 {
89 let wash = if prs { 0.12 } else { 0.07 };
90 child.fill_rrect(Rect {
91 origin: Point { x: x + 3.0, y: r.origin.y + 3.0 },
92 size: Size { width: seg_w - 6.0, height: r.size.height - 6.0 },
93 }, radius - 3.0, with_alpha(Color::rgb(255, 255, 255), wash));
94 }
95
96 let tw = child.font.measure_text(label, 13.0);
97 let lh = child.font.line_height(13.0);
98 let tx = x + (seg_w - tw) / 2.0;
99 let ty = r.origin.y + (r.size.height - lh) / 2.0;
100 let col = super::lerp_color(fg, sel_fg, nearness);
102 child.draw_text_at(label, Point { x: tx, y: ty }, with_alpha(col, dim), 13.0);
103 child.semantics(
104 super::Semantics::new(rosace_core::Role::Tab)
105 .label(label)
106 .value(if i == self.selected { "selected" } else { "not selected" }),
107 );
108 match (&self.on_change, self.disabled) {
109 (Some(cb), false) => { let cb = cb.clone(); let idx = i; child.register_hit(Arc::new(move || cb(idx))); }
110 _ => child.register_hit(Arc::new(|| {})),
111 }
112 }
113
114 if focused {
116 let with_alpha = |c: Color, a: f32| Color::rgba(c.r, c.g, c.b, (a.clamp(0.0, 1.0) * 255.0).round() as u8);
117 ctx.stroke_rrect(r, radius, with_alpha(sel_bg, 0.9), 2.0);
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use rosace_layout::Constraints;
126
127 #[test]
128 fn customization_builders_do_not_change_layout_size() {
129 let font = rosace_render::FontCache::embedded();
130 let theme = rosace_theme::built_in::dark_theme();
131 let ctx = LayoutCtx::new(Constraints::loose(400.0, 400.0), &font, &theme);
132 let base = SegmentedControl::new(vec!["Day", "Week"], 0);
133 let customized = SegmentedControl::new(vec!["Day", "Week"], 0)
134 .track_color(Color::rgb(10, 10, 10))
135 .selected_color(Color::rgb(255, 0, 0))
136 .color(Color::rgb(255, 255, 255))
137 .selected_text_color(Color::rgb(0, 0, 0));
138 assert_eq!(base.layout(&ctx), customized.layout(&ctx));
139 }
140}