rosace_widgets/tree/
chip.rs1use std::sync::Arc;
2
3use rosace_core::types::Size;
4use rosace_render::Color;
5use super::{Widget, LayoutCtx, PaintCtx};
6use super::container::draw_rounded_rect_pub;
7
8pub struct Chip {
19 label: String,
20 selected: bool,
21 disabled: bool,
22 font_size: Option<f32>,
26 height: f32,
27 color: Option<Color>, radius: Option<f32>,
30 on_toggle: Option<Arc<dyn Fn(bool) + Send + Sync>>,
31}
32
33impl Chip {
34 pub fn new(label: impl Into<String>) -> Self {
35 Self { label: label.into(), selected: false, disabled: false, font_size: None, height: 30.0, color: None, radius: None, on_toggle: None }
36 }
37
38 fn resolved_font_size(&self, theme: &rosace_theme::ThemeData) -> f32 {
39 self.font_size.unwrap_or(theme.typography.label_large.size)
40 }
41 pub fn selected(mut self) -> Self { self.selected = true; self }
42 pub fn selected_if(mut self, c: bool) -> Self { self.selected = c; self }
43 pub fn disabled(mut self) -> Self { self.disabled = true; self }
44 pub fn color(mut self, c: Color) -> Self { self.color = Some(c); self }
46 pub fn radius(mut self, r: f32) -> Self { self.radius = Some(r.max(0.0)); self }
49 pub fn on_toggle(mut self, f: impl Fn(bool) + Send + Sync + 'static) -> Self {
51 self.on_toggle = Some(Arc::new(f)); self
52 }
53}
54
55fn with_alpha(c: Color, a: f32) -> Color {
56 Color::rgba(c.r, c.g, c.b, (a.clamp(0.0, 1.0) * 255.0).round() as u8)
57}
58
59impl Widget for Chip {
60 fn layout(&self, ctx: &LayoutCtx) -> Size {
61 let w = ctx.font.measure_text(&self.label, self.resolved_font_size(ctx.theme)) + 28.0;
62 Size { width: w, height: self.height }
63 }
64
65 fn paint(&self, ctx: &mut PaintCtx) {
66 ctx.semantics(super::Semantics::new(rosace_core::Role::Checkbox)
67 .label(&self.label)
68 .value(if self.selected { "selected" } else { "not selected" }));
69
70 match (&self.on_toggle, self.disabled) {
71 (Some(f), false) => { let f = f.clone(); let next = !self.selected; ctx.on_press(move || f(next)); }
72 _ => ctx.on_press(|| {}),
73 }
74 let focused = !self.disabled && ctx.focus_node().is_focused();
75 let hovered = !self.disabled && ctx.hovered();
76 let pressed = !self.disabled && ctx.pressed();
77
78 let t = ctx.animate_channel(0, if self.selected { 1.0 } else { 0.0 }, 0.0);
79 let wash_t = if pressed { 0.14 } else if focused { 0.10 } else if hovered { 0.07 } else { 0.0 };
80 let wash = ctx.animate_channel(1, wash_t, 0.0);
81
82 let colors = ctx.theme.colors.clone();
83 let sel_fill = self.color.unwrap_or_else(|| ctx.tc(colors.primary));
84 let unsel_fill = ctx.tc(colors.surface_variant);
85 let outline = ctx.tc(colors.outline);
86 let unsel_text = ctx.tc(colors.on_surface);
87 let sel_text = Color::rgb(252, 252, 255);
88 let dim = if self.disabled { 0.4 } else { 1.0 };
89
90 let r = ctx.rect;
91 let radius = self.radius.unwrap_or(r.size.height / 2.0);
92
93 let mut fill = super::lerp_color(unsel_fill, sel_fill, t);
95 if wash > 0.001 { fill = super::lerp_color(fill, Color::rgb(255, 255, 255), wash); }
96 draw_rounded_rect_pub(ctx, r, with_alpha(fill, dim), radius);
97
98 if t < 0.99 {
100 ctx.stroke_rrect(r, radius, with_alpha(outline, (1.0 - t) * dim), 1.0);
101 }
102 if focused {
104 ctx.stroke_rrect(r, radius, with_alpha(sel_fill, 0.9), 2.0);
105 }
106
107 let fg = super::lerp_color(unsel_text, sel_text, t);
108 let font_size = self.resolved_font_size(&ctx.theme);
109 let text_w = ctx.font.measure_text(&self.label, font_size);
110 let tx = ((r.size.width - text_w) / 2.0).max(0.0);
111 let line_h = ctx.font.line_height(font_size);
112 let ty = ((r.size.height - line_h) / 2.0).max(0.0);
113 ctx.text(&self.label, tx, ty, with_alpha(fg, dim), font_size);
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use rosace_core::types::{Point, Rect};
121 use rosace_render::{FontCache, PictureRecorder};
122 use rosace_render::draw_command::DrawCommand;
123 use std::cell::RefCell;
124 use std::rc::Rc;
125 use crate::tree::RenderTree;
126
127 fn paint(selected: bool) -> Vec<DrawCommand> {
128 let font = FontCache::embedded();
129 let mut rec = PictureRecorder::new();
130 let tree = Rc::new(RefCell::new(RenderTree::new()));
131 let mut ctx = PaintCtx::root(
132 &mut rec,
133 Rect { origin: Point { x: 0.0, y: 0.0 }, size: Size { width: 80.0, height: 30.0 } },
134 &font, rosace_theme::built_in::dark_theme(), tree,
135 );
136 let mut c = Chip::new("Filter");
137 if selected { c = c.selected(); }
138 c.paint(&mut ctx);
139 rec.finish().commands
140 }
141
142 #[test]
143 fn draws_a_pill_and_its_label() {
144 let cmds = paint(false);
145 assert!(cmds.iter().any(|c| matches!(c, DrawCommand::FillRRect { .. })), "chip is a rounded pill");
146 assert!(cmds.iter().any(|c| matches!(c, DrawCommand::DrawText { text, .. } if text == "Filter")), "shows its label");
147 }
148
149 #[test]
150 fn selected_chip_has_no_outline_but_unselected_does() {
151 assert!(paint(false).iter().any(|c| matches!(c, DrawCommand::StrokeRRect { .. })), "unselected chip is outlined");
152 }
153}