use std::sync::Arc;
use rosace_core::types::{Point, Rect, Size};
use rosace_layout::Constraints;
use rosace_render::Color;
use super::{Widget, LayoutCtx, PaintCtx};
#[derive(Debug, Clone, Copy, Default)]
pub enum ButtonVariant {
#[default]
Primary,
Secondary,
Ghost,
Danger,
Success,
Link,
}
pub struct Button {
pub label: String,
pub variant: ButtonVariant,
pub disabled: bool,
pub icon: Option<Box<dyn Widget>>,
pub width: Option<f32>,
pub height: f32,
pub font_size: Option<f32>,
pub radius: f32,
background: Option<Color>,
color: Option<Color>,
on_press: Option<Arc<dyn Fn() + Send + Sync>>,
}
impl Button {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
variant: ButtonVariant::Primary,
disabled: false,
icon: None,
width: None,
height: 34.0,
font_size: None,
radius: 6.0,
background: None,
color: None,
on_press: None,
}
}
pub fn variant(mut self, v: ButtonVariant) -> Self { self.variant = v; self }
pub fn disabled(mut self) -> Self { self.disabled = true; self }
pub fn disabled_if(mut self, condition: bool) -> Self {
if condition { self.disabled = true; }
self
}
pub fn width(mut self, w: f32) -> Self { self.width = Some(w); self }
pub fn height(mut self, h: f32) -> Self { self.height = h; self }
pub fn font_size(mut self, s: f32) -> Self { self.font_size = Some(s); self }
fn resolved_font_size(&self, theme: &rosace_theme::ThemeData) -> f32 {
self.font_size.unwrap_or(theme.typography.label_large.size)
}
pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
pub fn color(mut self, c: Color) -> Self { self.color = Some(c); self }
pub fn radius(mut self, r: f32) -> Self { self.radius = r; self }
pub fn icon(mut self, w: impl Widget + 'static) -> Self { self.icon = Some(Box::new(w)); self }
pub fn on_press(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
self.on_press = Some(Arc::new(f));
self
}
}
impl Widget for Button {
fn layout(&self, ctx: &LayoutCtx) -> Size {
let constraints = ctx.constraints;
let font_size = self.resolved_font_size(ctx.theme);
let text_w = self.label.len() as f32 * font_size * 0.6;
let icon_w = if self.icon.is_some() { font_size + 4.0 + 6.0 } else { 0.0 };
let w = self.width.unwrap_or(text_w + icon_w + 32.0);
constraints.constrain(Size { width: w, height: self.height })
}
fn paint(&self, ctx: &mut PaintCtx) {
ctx.semantics(super::Semantics::new(rosace_core::Role::Button).label(&self.label));
let t = &ctx.theme.colors;
let variant = if self.disabled { ButtonVariant::Secondary } else { self.variant };
let (bg, fg, border) = match variant {
ButtonVariant::Primary => (ctx.tc(t.primary), ctx.tc(t.on_primary), None),
ButtonVariant::Secondary => (ctx.tc(t.secondary), ctx.tc(t.on_secondary), None),
ButtonVariant::Ghost => (Color::rgba(0,0,0,0), ctx.tc(t.primary), Some(ctx.tc(t.outline))),
ButtonVariant::Link => (Color::rgba(0,0,0,0), ctx.tc(t.primary), None),
ButtonVariant::Danger => (Color::rgb(180, 50, 50), Color::rgb(255, 230, 230), None),
ButtonVariant::Success => (Color::rgb( 40, 160, 80), Color::rgb(220, 255, 230), None),
};
let bg = if self.disabled { bg } else { self.background.unwrap_or(bg) };
let fg = if self.disabled { ctx.tc(t.outline) } else { self.color.unwrap_or(fg) };
let target = if self.disabled { 0.0 } else if ctx.pressed() { 1.0 } else if ctx.hovered() { 0.5 } else { 0.0 };
let emphasis = ctx.animate_to(target, 0.0);
let bg = if emphasis > 0.0 {
if bg.a == 0 {
Color::rgba(255, 255, 255, (22.0 * emphasis * 2.0).min(255.0) as u8)
} else {
lighten(bg, (0.12 * emphasis * 2.0).min(1.0))
}
} else {
bg
};
let r = ctx.rect;
super::container::draw_rounded_rect_pub(ctx, r, bg, self.radius);
if let Some(bc) = border {
ctx.stroke_rrect(r, self.radius, bc, 1.0);
}
let font_size = self.resolved_font_size(&ctx.theme);
let text_w = ctx.font.measure_text(&self.label, font_size);
let line_h = ctx.font.line_height(font_size);
let ty = ((r.size.height - line_h) / 2.0).max(0.0);
const ICON_GAP: f32 = 6.0;
if let Some(icon) = &self.icon {
let is = icon.layout(&ctx.layout_ctx(Constraints::loose(font_size + 4.0, font_size + 4.0)));
let content_w = is.width + ICON_GAP + text_w;
let start_x = ((r.size.width - content_w) / 2.0).max(4.0);
let iy = r.origin.y + (r.size.height - is.height) / 2.0;
icon.paint(&mut ctx.child(Rect {
origin: Point { x: r.origin.x + start_x, y: iy },
size: is,
}));
ctx.text(&self.label, start_x + is.width + ICON_GAP, ty, fg, font_size);
} else {
let tx = ((r.size.width - text_w) / 2.0).max(4.0);
ctx.text(&self.label, tx, ty, fg, font_size);
}
if !self.disabled {
match &self.on_press {
Some(cb) => ctx.register_hit(Arc::clone(cb)),
None => ctx.register_hit(Arc::new(|| {})),
}
}
}
}
pub(super) fn lighten(c: Color, t: f32) -> Color {
let mix = |v: u8| (v as f32 + (255.0 - v as f32) * t).round() as u8;
Color::rgba(mix(c.r), mix(c.g), mix(c.b), c.a)
}
#[cfg(test)]
mod tests {
use super::*;
use rosace_layout::Constraints;
#[test]
fn background_and_color_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 = Button::new("Save").width(90.0);
let customized = Button::new("Save").width(90.0)
.background(Color::rgb(20, 20, 20))
.color(Color::rgb(255, 255, 255));
assert_eq!(base.layout(&ctx), customized.layout(&ctx));
}
}