use std::sync::Arc;
use rosace_core::types::{Point, Rect, Size};
use rosace_render::Color;
use super::{Widget, LayoutCtx, PaintCtx};
pub struct SegmentedControl {
segments: Vec<String>,
selected: usize,
disabled: bool,
height: f32,
track_color: Option<Color>,
selected_color: Option<Color>,
color: Option<Color>,
selected_text_color: Option<Color>,
on_change: Option<Arc<dyn Fn(usize) + Send + Sync>>,
}
impl SegmentedControl {
pub fn new(segments: Vec<impl Into<String>>, selected: usize) -> Self {
Self {
segments: segments.into_iter().map(Into::into).collect(), selected, disabled: false, height: 34.0,
track_color: None, selected_color: None, color: None, selected_text_color: None,
on_change: None,
}
}
pub fn height(mut self, h: f32) -> Self { self.height = h; self }
pub fn disabled(mut self) -> Self { self.disabled = true; self }
pub fn track_color(mut self, c: Color) -> Self { self.track_color = Some(c); self }
pub fn selected_color(mut self, c: Color) -> Self { self.selected_color = Some(c); self }
pub fn color(mut self, c: Color) -> Self { self.color = Some(c); self }
pub fn selected_text_color(mut self, c: Color) -> Self { self.selected_text_color = Some(c); self }
pub fn on_change(mut self, f: impl Fn(usize) + Send + Sync + 'static) -> Self {
self.on_change = Some(Arc::new(f)); self
}
}
impl Widget for SegmentedControl {
fn layout(&self, ctx: &LayoutCtx) -> Size {
let w: f32 = self.segments.iter()
.map(|s| ctx.font.measure_text(s, 13.0) + 28.0)
.sum();
ctx.constraints.constrain(Size { width: w.max(120.0), height: self.height })
}
fn paint(&self, ctx: &mut PaintCtx) {
let r = ctx.rect;
let n = self.segments.len().max(1);
let seg_w = r.size.width / n as f32;
let radius = self.height / 2.0;
let (track, fg, sel_bg, sel_fg) = {
let t = &ctx.theme.colors;
(self.track_color.unwrap_or_else(|| ctx.tc(t.surface_variant)),
self.color.unwrap_or_else(|| ctx.tc(t.on_surface)),
self.selected_color.unwrap_or_else(|| ctx.tc(t.primary)),
self.selected_text_color.unwrap_or_else(|| ctx.tc(t.on_primary)))
};
let dim = if self.disabled { 0.45 } else { 1.0 };
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);
let focused = !self.disabled && ctx.focus_node().is_focused();
ctx.fill_rrect(r, radius, with_alpha(track, dim));
let pos = ctx.animate_to(self.selected as f32, 0.0);
let px = r.origin.x + pos * seg_w;
let pill = Rect {
origin: Point { x: px + 3.0, y: r.origin.y + 3.0 },
size: Size { width: seg_w - 6.0, height: r.size.height - 6.0 },
};
ctx.fill_rrect(pill, radius - 3.0, with_alpha(sel_bg, dim));
for (i, label) in self.segments.iter().enumerate() {
let x = r.origin.x + i as f32 * seg_w;
let seg_rect = Rect { origin: Point { x, y: r.origin.y }, size: Size { width: seg_w, height: r.size.height } };
let mut child = ctx.child(seg_rect);
let hov = !self.disabled && child.hovered();
let prs = !self.disabled && child.pressed();
let nearness = (1.0 - (pos - i as f32).abs()).clamp(0.0, 1.0);
if (hov || prs) && nearness < 0.5 {
let wash = if prs { 0.12 } else { 0.07 };
child.fill_rrect(Rect {
origin: Point { x: x + 3.0, y: r.origin.y + 3.0 },
size: Size { width: seg_w - 6.0, height: r.size.height - 6.0 },
}, radius - 3.0, with_alpha(Color::rgb(255, 255, 255), wash));
}
let tw = child.font.measure_text(label, 13.0);
let lh = child.font.line_height(13.0);
let tx = x + (seg_w - tw) / 2.0;
let ty = r.origin.y + (r.size.height - lh) / 2.0;
let col = super::lerp_color(fg, sel_fg, nearness);
child.draw_text_at(label, Point { x: tx, y: ty }, with_alpha(col, dim), 13.0);
child.semantics(
super::Semantics::new(rosace_core::Role::Tab)
.label(label)
.value(if i == self.selected { "selected" } else { "not selected" }),
);
match (&self.on_change, self.disabled) {
(Some(cb), false) => { let cb = cb.clone(); let idx = i; child.register_hit(Arc::new(move || cb(idx))); }
_ => child.register_hit(Arc::new(|| {})),
}
}
if focused {
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);
ctx.stroke_rrect(r, radius, with_alpha(sel_bg, 0.9), 2.0);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rosace_layout::Constraints;
#[test]
fn customization_builders_do_not_change_layout_size() {
let font = rosace_render::FontCache::embedded();
let theme = rosace_theme::built_in::dark_theme();
let ctx = LayoutCtx::new(Constraints::loose(400.0, 400.0), &font, &theme);
let base = SegmentedControl::new(vec!["Day", "Week"], 0);
let customized = SegmentedControl::new(vec!["Day", "Week"], 0)
.track_color(Color::rgb(10, 10, 10))
.selected_color(Color::rgb(255, 0, 0))
.color(Color::rgb(255, 255, 255))
.selected_text_color(Color::rgb(0, 0, 0));
assert_eq!(base.layout(&ctx), customized.layout(&ctx));
}
}