1use std::sync::Arc;
2
3use rosace_core::types::{Point, Rect, Size};
4use rosace_layout::Constraints;
5use rosace_render::Color;
6use super::{Widget, LayoutCtx, PaintCtx};
7
8#[derive(Debug, Clone, Copy, Default)]
9pub enum ButtonVariant {
10 #[default]
11 Primary,
12 Secondary,
13 Ghost,
14 Danger,
15 Success,
16 Link,
17}
18
19pub struct Button {
24 pub label: String,
25 pub variant: ButtonVariant,
26 pub disabled: bool,
27 pub icon: Option<Box<dyn Widget>>,
28 pub width: Option<f32>,
29 pub height: f32,
30 pub font_size: Option<f32>,
34 pub radius: f32,
35 background: Option<Color>,
36 color: Option<Color>,
37 on_press: Option<Arc<dyn Fn() + Send + Sync>>,
38}
39
40impl Button {
41 pub fn new(label: impl Into<String>) -> Self {
42 Self {
43 label: label.into(),
44 variant: ButtonVariant::Primary,
45 disabled: false,
46 icon: None,
47 width: None,
48 height: 34.0,
49 font_size: None,
50 radius: 6.0,
51 background: None,
52 color: None,
53 on_press: None,
54 }
55 }
56
57 pub fn variant(mut self, v: ButtonVariant) -> Self { self.variant = v; self }
58 pub fn disabled(mut self) -> Self { self.disabled = true; self }
59 pub fn disabled_if(mut self, condition: bool) -> Self {
63 if condition { self.disabled = true; }
64 self
65 }
66 pub fn width(mut self, w: f32) -> Self { self.width = Some(w); self }
67 pub fn height(mut self, h: f32) -> Self { self.height = h; self }
68 pub fn font_size(mut self, s: f32) -> Self { self.font_size = Some(s); self }
69
70 fn resolved_font_size(&self, theme: &rosace_theme::ThemeData) -> f32 {
71 self.font_size.unwrap_or(theme.typography.label_large.size)
72 }
73 pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
76 pub fn color(mut self, c: Color) -> Self { self.color = Some(c); self }
78 pub fn radius(mut self, r: f32) -> Self { self.radius = r; self }
79 pub fn icon(mut self, w: impl Widget + 'static) -> Self { self.icon = Some(Box::new(w)); self }
80
81 pub fn on_press(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
84 self.on_press = Some(Arc::new(f));
85 self
86 }
87}
88
89impl Widget for Button {
90 fn layout(&self, ctx: &LayoutCtx) -> Size {
91 let constraints = ctx.constraints;
92 let font_size = self.resolved_font_size(ctx.theme);
93 let text_w = self.label.len() as f32 * font_size * 0.6;
94 let icon_w = if self.icon.is_some() { font_size + 4.0 + 6.0 } else { 0.0 };
100 let w = self.width.unwrap_or(text_w + icon_w + 32.0);
101 constraints.constrain(Size { width: w, height: self.height })
102 }
103
104 fn paint(&self, ctx: &mut PaintCtx) {
105 ctx.semantics(super::Semantics::new(rosace_core::Role::Button).label(&self.label));
106 let t = &ctx.theme.colors;
107 let variant = if self.disabled { ButtonVariant::Secondary } else { self.variant };
108
109 let (bg, fg, border) = match variant {
110 ButtonVariant::Primary => (ctx.tc(t.primary), ctx.tc(t.on_primary), None),
111 ButtonVariant::Secondary => (ctx.tc(t.secondary), ctx.tc(t.on_secondary), None),
112 ButtonVariant::Ghost => (Color::rgba(0,0,0,0), ctx.tc(t.primary), Some(ctx.tc(t.outline))),
113 ButtonVariant::Link => (Color::rgba(0,0,0,0), ctx.tc(t.primary), None),
114 ButtonVariant::Danger => (Color::rgb(180, 50, 50), Color::rgb(255, 230, 230), None),
115 ButtonVariant::Success => (Color::rgb( 40, 160, 80), Color::rgb(220, 255, 230), None),
116 };
117
118 let bg = if self.disabled { bg } else { self.background.unwrap_or(bg) };
119 let fg = if self.disabled { ctx.tc(t.outline) } else { self.color.unwrap_or(fg) };
120
121 let target = if self.disabled { 0.0 } else if ctx.pressed() { 1.0 } else if ctx.hovered() { 0.5 } else { 0.0 };
126 let emphasis = ctx.animate_to(target, 0.0);
127 let bg = if emphasis > 0.0 {
128 if bg.a == 0 {
129 Color::rgba(255, 255, 255, (22.0 * emphasis * 2.0).min(255.0) as u8)
130 } else {
131 lighten(bg, (0.12 * emphasis * 2.0).min(1.0))
132 }
133 } else {
134 bg
135 };
136
137 let r = ctx.rect;
138 super::container::draw_rounded_rect_pub(ctx, r, bg, self.radius);
139
140 if let Some(bc) = border {
141 ctx.stroke_rrect(r, self.radius, bc, 1.0);
142 }
143
144 let font_size = self.resolved_font_size(&ctx.theme);
145 let text_w = ctx.font.measure_text(&self.label, font_size);
146 let line_h = ctx.font.line_height(font_size);
147 let ty = ((r.size.height - line_h) / 2.0).max(0.0);
148
149 const ICON_GAP: f32 = 6.0;
153 if let Some(icon) = &self.icon {
154 let is = icon.layout(&ctx.layout_ctx(Constraints::loose(font_size + 4.0, font_size + 4.0)));
155 let content_w = is.width + ICON_GAP + text_w;
156 let start_x = ((r.size.width - content_w) / 2.0).max(4.0);
157 let iy = r.origin.y + (r.size.height - is.height) / 2.0;
158 icon.paint(&mut ctx.child(Rect {
159 origin: Point { x: r.origin.x + start_x, y: iy },
160 size: is,
161 }));
162 ctx.text(&self.label, start_x + is.width + ICON_GAP, ty, fg, font_size);
163 } else {
164 let tx = ((r.size.width - text_w) / 2.0).max(4.0);
165 ctx.text(&self.label, tx, ty, fg, font_size);
166 }
167
168 if !self.disabled {
173 match &self.on_press {
174 Some(cb) => ctx.register_hit(Arc::clone(cb)),
175 None => ctx.register_hit(Arc::new(|| {})),
176 }
177 }
178 }
179}
180
181pub(super) fn lighten(c: Color, t: f32) -> Color {
183 let mix = |v: u8| (v as f32 + (255.0 - v as f32) * t).round() as u8;
184 Color::rgba(mix(c.r), mix(c.g), mix(c.b), c.a)
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use rosace_layout::Constraints;
191
192 #[test]
193 fn background_and_color_builders_do_not_change_layout_size() {
194 let font = rosace_render::FontCache::embedded();
195 let theme = rosace_theme::built_in::dark_theme();
196 let ctx = LayoutCtx::new(Constraints::loose(400.0, 400.0), &font, &theme);
197 let base = Button::new("Save").width(90.0);
198 let customized = Button::new("Save").width(90.0)
199 .background(Color::rgb(20, 20, 20))
200 .color(Color::rgb(255, 255, 255));
201 assert_eq!(base.layout(&ctx), customized.layout(&ctx));
202 }
203}