use std::cell::Cell;
use crate::anim::{Easing, Transition};
use crate::core::{EventCtx, Widget};
use crate::event::{Event, Key, PointerKind};
use crate::geometry::{Rect, Size};
use crate::render::{Canvas, Paint};
use crate::signal::Signal;
use crate::spec::Align;
use crate::style::Style;
use crate::text::TextEngine;
const SEG_PAD_X: i32 = 14;
pub struct SegmentedControl {
options: Vec<String>,
selected: Signal<usize>,
hover: Option<usize>,
sel_pos: Cell<Transition<f32>>,
}
impl SegmentedControl {
pub fn new(options: Vec<String>, selected: Signal<usize>) -> Self {
let init = selected.get().min(options.len().saturating_sub(1)) as f32;
Self {
options,
selected,
hover: None,
sel_pos: Cell::new(Transition::new(init)),
}
}
fn len(&self) -> usize {
self.options.len()
}
fn sel(&self) -> usize {
self.selected.get().min(self.len().saturating_sub(1))
}
fn seg_x(&self, bounds: Rect, i: usize) -> (i32, i32) {
let n = self.len().max(1) as i32;
let x0 = bounds.x + (bounds.w * i as i32) / n;
let x1 = bounds.x + (bounds.w * (i as i32 + 1)) / n;
(x0, x1)
}
fn seg_at(&self, bounds: Rect, x: i32) -> usize {
let n = self.len();
if n == 0 || bounds.w <= 0 {
return 0;
}
let rel = (x - bounds.x).clamp(0, bounds.w - 1);
((rel * n as i32) / bounds.w).clamp(0, n as i32 - 1) as usize
}
fn select(&self, ctx: &mut EventCtx, i: usize) {
if i < self.len() && self.selected.get() != i {
self.selected.set(i);
ctx.mark_dirty();
}
}
}
impl Widget for SegmentedControl {
fn measure(&self, _avail: Size, style: &Style, text: &mut dyn TextEngine) -> Size {
let mut tw = 0;
let mut th = 0;
for o in &self.options {
let t = text.measure(o, style.font_family.as_deref(), style.font_size, None);
tw = tw.max(t.w);
th = th.max(t.h);
}
let seg_w = tw + 2 * SEG_PAD_X;
let h = th.max(style.font_size as i32) + 14;
Size::new(seg_w * self.len().max(1) as i32, h)
}
fn paint(
&self,
bounds: Rect,
_content: Rect,
focused: bool,
enabled: bool,
canvas: &mut dyn Canvas,
style: &Style,
) {
let t = crate::theme::current();
let (pal, sg) = (&t.palette, &t.segment);
let (x, y, w, h) = (
bounds.x as f32,
bounds.y as f32,
bounds.w as f32,
bounds.h as f32,
);
let corner = sg.corner(&t.metrics);
let n = self.len();
let sel = self.sel();
canvas.fill_round_rect(x, y, w, h, corner, &Paint::fill(sg.bg(pal)));
let mut sp = self.sel_pos.get();
let sel_target = sel as f32;
if sp.target() != sel_target {
sp.retarget(sel_target, t.anim.normal(), Easing::EaseInOut);
}
let fi = sp.animate();
self.sel_pos.set(sp);
let family = style.font_family.as_deref();
let fsize = style.font_size;
for i in 0..n {
if self.hover == Some(i) && enabled && i != sel {
let (x0, x1) = self.seg_x(bounds, i);
canvas.fill_round_rect(
(x0 + 2) as f32,
(bounds.y + 2) as f32,
(x1 - x0 - 4) as f32,
(bounds.h - 4) as f32,
(corner - 2.0).max(0.0),
&Paint::fill(sg.hover_bg(pal)),
);
}
}
if n > 0 {
let i0 = (fi.floor() as usize).min(n - 1);
let i1 = (i0 + 1).min(n - 1);
let frac = fi - i0 as f32;
let (a0, a1) = self.seg_x(bounds, i0);
let (b0, b1) = self.seg_x(bounds, i1);
let px0 = a0 as f32 + (b0 - a0) as f32 * frac;
let px1 = a1 as f32 + (b1 - a1) as f32 * frac;
let pill_bg = if enabled {
sg.selected_bg(pal)
} else {
pal.surface_alt
};
let (rx, ry, rw, rh) = (
px0 + 2.0,
(bounds.y + 2) as f32,
px1 - px0 - 4.0,
(bounds.h - 4) as f32,
);
let pr = (corner - 2.0).max(0.0);
canvas.fill_round_rect(rx, ry, rw, rh, pr, &Paint::fill(pill_bg));
}
for i in 0..n {
let (x0, x1) = self.seg_x(bounds, i);
let tc = if i == sel {
if enabled {
sg.selected_text(pal)
} else {
pal.text_disabled
}
} else if enabled {
sg.text(pal)
} else {
pal.text_disabled
};
let seg = Rect::new(x0, bounds.y, x1 - x0, bounds.h);
canvas.draw_text(&self.options[i], seg, tc, Align::Center, family, fsize);
}
let divider = sg.divider(pal);
for i in 1..n {
if i == sel || i - 1 == sel {
continue;
}
let (x0, _) = self.seg_x(bounds, i);
canvas.draw_line(
x0 as f32,
(bounds.y + 6) as f32,
x0 as f32,
(bounds.y + bounds.h - 6) as f32,
1.0,
&Paint::fill(divider),
);
}
let _ = focused;
let (border, bw) = if !enabled {
(pal.track, 1.5)
} else {
(sg.border(pal), 1.5)
};
canvas.stroke_round_rect(x, y, w, h, corner, bw, &Paint::fill(border));
}
fn on_event(&mut self, ctx: &mut EventCtx, ev: &Event) -> bool {
match ev {
Event::Pointer(p) => match p.kind {
PointerKind::Down => {
ctx.request_focus();
true
}
PointerKind::Up => {
if ctx.bounds().contains(p.pos) {
let i = self.seg_at(ctx.bounds(), p.pos.x);
self.select(ctx, i);
}
true
}
PointerKind::Move | PointerKind::Enter => {
let i = self.seg_at(ctx.bounds(), p.pos.x);
if self.hover != Some(i) {
self.hover = Some(i);
ctx.mark_dirty();
}
true
}
PointerKind::Leave => {
if self.hover.is_some() {
self.hover = None;
ctx.mark_dirty();
}
true
}
_ => false,
},
Event::Key(k) if k.pressed => match k.key {
Key::Left => {
let cur = self.sel();
if cur > 0 {
self.select(ctx, cur - 1);
}
true
}
Key::Right => {
let cur = self.sel();
if cur + 1 < self.len() {
self.select(ctx, cur + 1);
}
true
}
_ => false,
},
_ => false,
}
}
fn focusable(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{MouseButton, PointerEvent};
use crate::geometry::Point;
use crate::signal::signal;
use crate::ui::Element;
fn layout(el: Element) -> Tree {
let mut tree = Tree::new();
let root = el.build(&mut tree);
tree.root = Some(root);
tree.layout_root(Size::new(200, 200), &mut crate::text::NullTextEngine);
tree
}
use crate::core::Tree;
fn click(tree: &mut Tree, at: Point) -> crate::core::DispatchResult {
let mut hover = None;
let mut capture = None;
tree.dispatch_pointer(
PointerEvent::single(PointerKind::Down, at, MouseButton::Left),
&mut hover,
&mut capture,
);
tree.dispatch_pointer(
PointerEvent::single(PointerKind::Up, at, MouseButton::Left),
&mut hover,
&mut capture,
)
}
#[test]
fn click_selects_segment_under_pointer() {
let sel = signal(0usize);
let mut tree = layout(
Element::segmented(vec!["简体", "繁体", "其它"], sel)
.width(180)
.height(32),
);
click(&mut tree, Point::new(150, 16)); assert_eq!(sel.get(), 2, "点击第三段应选中索引 2");
click(&mut tree, Point::new(30, 16)); assert_eq!(sel.get(), 0, "点击第一段应选中索引 0");
}
#[test]
fn arrow_keys_move_selection() {
let sel = signal(0usize);
let mut tree = layout(
Element::segmented(vec!["A", "B", "C"], sel)
.width(180)
.height(32),
);
let root = tree.root;
let mut hover = None;
let mut capture = None;
let res = tree.dispatch_pointer(
PointerEvent::single(PointerKind::Down, Point::new(30, 16), MouseButton::Left),
&mut hover,
&mut capture,
);
let focus = res.focus.or(root);
let right = crate::event::KeyEvent {
key: Key::Right,
pressed: true,
shift: false,
ctrl: false,
};
tree.dispatch_key(right, focus);
assert_eq!(sel.get(), 1, "右键应移到下一段");
tree.dispatch_key(right, focus);
assert_eq!(sel.get(), 2);
let left = crate::event::KeyEvent {
key: Key::Left,
pressed: true,
shift: false,
ctrl: false,
};
tree.dispatch_key(left, focus);
assert_eq!(sel.get(), 1, "左键应移回上一段");
}
fn paint_at(sc: &SegmentedControl, clock: u64) {
use crate::render::SkiaCanvas;
use tiny_skia::Pixmap;
crate::anim::set_clock_ms(clock);
let mut pm = Pixmap::new(180, 32).unwrap();
let mut c = SkiaCanvas::new(&mut pm);
sc.paint(
Rect::new(0, 0, 180, 32),
Rect::new(0, 0, 180, 32),
false,
true,
&mut c,
&Style::default(),
);
}
#[test]
fn selected_pill_slides_then_settles_when_animated() {
crate::anim::set_enabled(true);
let sel = signal(0usize);
let sc = SegmentedControl::new(vec!["A".into(), "B".into(), "C".into()], sel);
paint_at(&sc, 0);
assert_eq!(sc.sel_pos.get().value(), 0.0, "初始胶囊在第 0 段");
sel.set(2); paint_at(&sc, 0); assert_eq!(sc.sel_pos.get().target(), 2.0, "选中变 2 后胶囊目标应为 2");
assert!(sc.sel_pos.get().is_active(), "动画开启时胶囊应在滑动中");
paint_at(&sc, 5000); assert_eq!(sc.sel_pos.get().value(), 2.0);
}
#[test]
fn selected_pill_snaps_when_animation_disabled() {
crate::anim::set_enabled(false);
let sel = signal(0usize);
let sc = SegmentedControl::new(vec!["A".into(), "B".into(), "C".into()], sel);
sel.set(2);
paint_at(&sc, 0);
assert_eq!(sc.sel_pos.get().value(), 2.0, "关闭动画应瞬时到选中段");
assert!(!sc.sel_pos.get().is_active());
crate::anim::set_enabled(true);
}
#[test]
fn seg_at_maps_boundaries() {
let sc = SegmentedControl::new(vec!["a".into(), "b".into(), "c".into()], signal(0));
let b = Rect::new(0, 0, 180, 32);
assert_eq!(sc.seg_at(b, 0), 0);
assert_eq!(sc.seg_at(b, 59), 0);
assert_eq!(sc.seg_at(b, 60), 1);
assert_eq!(sc.seg_at(b, 179), 2);
assert_eq!(sc.seg_at(b, 999), 2);
assert_eq!(sc.seg_at(b, -5), 0);
}
}