use crate::event::EventContext;
use crate::geometry::Color;
use crate::layout::style::FlexStyle;
use crate::layout::types::FlexAlign;
use crate::layout_methods;
use crate::theme;
use crate::view::node::{Listener, ViewNode};
use crate::view::paint::{FontWeight, PaintStyle, TextStyle};
use crate::widget::icon::{Icon, IconName};
use crate::widget::tooltip::Tooltip;
use crate::widget::{BuildContext, Widget};
use std::rc::Rc;
type HoverCallback = Rc<dyn Fn(&mut EventContext)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ButtonVariant {
#[default]
Primary,
Secondary,
Tertiary,
Danger,
Warning,
Link,
Plain,
Control,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ButtonSize {
Sm,
#[default]
Md,
Lg,
}
fn resolve_variant(t: theme::Theme, v: ButtonVariant) -> (PaintStyle, Color) {
let r = t.radius.small;
match v {
ButtonVariant::Primary => (
PaintStyle::new()
.background(t.background.brand_default)
.hover_background(t.background.brand_hover)
.pressed_background(t.background.brand_clicked)
.radius(r),
t.text.on_brand_default,
),
ButtonVariant::Secondary => (
PaintStyle::new()
.background(t.background.primary_default)
.hover_background(t.background.secondary_default)
.pressed_background(t.background.secondary_default)
.border(1.0, t.background.brand_default)
.radius(r),
t.background.brand_default,
),
ButtonVariant::Tertiary => (
PaintStyle::new()
.hover_background(t.background.secondary_default)
.pressed_background(t.background.secondary_default)
.border(1.0, t.background.brand_default)
.radius(r),
t.background.brand_default,
),
ButtonVariant::Danger => (
PaintStyle::new()
.background(t.status.danger)
.hover_background(Color::from_hex("#a30000"))
.pressed_background(Color::from_hex("#a30000"))
.radius(r),
Color::WHITE,
),
ButtonVariant::Warning => (
PaintStyle::new()
.background(t.status.warning)
.hover_background(Color::from_hex("#c98a00"))
.pressed_background(Color::from_hex("#c98a00"))
.radius(r),
t.text.regular_default,
),
ButtonVariant::Link => (
PaintStyle::new()
.hover_background(t.background.secondary_default)
.radius(r),
t.text.link_default,
),
ButtonVariant::Plain => (
PaintStyle::new()
.hover_background(t.background.secondary_default)
.radius(r),
t.text.regular_default,
),
ButtonVariant::Control => (
PaintStyle::new()
.background(t.background.primary_default)
.hover_background(t.background.secondary_default)
.pressed_background(t.background.secondary_default)
.border(1.0, t.border.default)
.radius(r),
t.text.regular_default,
),
}
}
fn button_size_metrics(t: theme::Theme, s: ButtonSize) -> (f32, f32, f64) {
match s {
ButtonSize::Sm => (4.0, 12.0, t.font.sm),
ButtonSize::Md => (6.0, 16.0, t.font.sm),
ButtonSize::Lg => (10.0, 24.0, t.font.md),
}
}
pub enum ButtonContent {
Text {
label: String,
style: Option<TextStyle>,
wrap: bool,
},
Icon {
name: IconName,
size: f64,
color: Option<Color>,
hover_color: Option<Color>,
pressed_color: Option<Color>,
},
Widget(Box<dyn Widget>),
}
pub struct Button {
content: ButtonContent,
listeners: Vec<Listener>,
layout: crate::widget::layout::LayoutAttr,
fixed_size: Option<f32>,
paint: Option<PaintStyle>,
padding_h: Option<f32>,
padding_v: Option<f32>,
variant: ButtonVariant,
size: ButtonSize,
tooltip: Option<String>,
on_enter: Option<HoverCallback>,
on_leave: Option<HoverCallback>,
disabled: bool,
}
impl Button {
pub fn new(l: impl Into<String>) -> Self {
Self {
content: ButtonContent::Text {
label: l.into(),
style: None,
wrap: false,
},
listeners: Vec::new(),
layout: crate::widget::layout::LayoutAttr::new().flex_shrink(0.0),
fixed_size: None,
paint: None,
padding_h: None,
padding_v: None,
variant: ButtonVariant::Primary,
size: ButtonSize::Md,
tooltip: None,
on_enter: None,
on_leave: None,
disabled: false,
}
}
pub fn icon(name: IconName, size: f64) -> Self {
Self {
content: ButtonContent::Icon {
name,
size,
color: None,
hover_color: None,
pressed_color: None,
},
listeners: Vec::new(),
layout: crate::widget::layout::LayoutAttr::new().flex_shrink(0.0),
fixed_size: None,
paint: None,
padding_h: None,
padding_v: None,
variant: ButtonVariant::Plain,
size: ButtonSize::Md,
tooltip: None,
on_enter: None,
on_leave: None,
disabled: false,
}
}
pub fn child<W: Widget + 'static>(w: W) -> Self {
Self {
content: ButtonContent::Widget(Box::new(w)),
listeners: Vec::new(),
layout: crate::widget::layout::LayoutAttr::new().flex_shrink(0.0),
fixed_size: None,
paint: None,
padding_h: None,
padding_v: None,
variant: ButtonVariant::Plain,
size: ButtonSize::Md,
tooltip: None,
on_enter: None,
on_leave: None,
disabled: false,
}
}
pub fn fixed_size(mut self, v: f32) -> Self {
self.fixed_size = Some(v);
self
}
layout_methods!(for Button);
pub fn set_text_style(&mut self, s: TextStyle) {
if let ButtonContent::Text { style, .. } = &mut self.content {
*style = Some(s);
}
}
pub fn icon_color(mut self, c: Color) -> Self {
if let ButtonContent::Icon { color, .. } = &mut self.content {
*color = Some(c);
}
self
}
pub fn icon_hover_color(mut self, c: Color) -> Self {
if let ButtonContent::Icon { hover_color, .. } = &mut self.content {
*hover_color = Some(c);
}
self
}
pub fn icon_pressed_color(mut self, c: Color) -> Self {
if let ButtonContent::Icon { pressed_color, .. } = &mut self.content {
*pressed_color = Some(c);
}
self
}
pub fn disabled(mut self, v: bool) -> Self {
self.disabled = v;
self
}
pub fn on_click<F: Fn() + 'static>(mut self, f: F) -> Self {
self.listeners.push(Listener::on_click(Rc::new(f)));
self
}
pub fn on_click_with_ctx<F: Fn(&mut EventContext) + 'static>(mut self, f: F) -> Self {
self.listeners.push(Listener::on_click_with_ctx(Rc::new(f)));
self
}
pub fn icon_size(mut self, v: f64) -> Self {
if let ButtonContent::Icon { size, .. } = &mut self.content {
*size = v;
}
self
}
pub fn on_mouse_enter<F: Fn(&mut EventContext) + 'static>(mut self, f: F) -> Self {
self.on_enter = Some(Rc::new(f));
self
}
pub fn on_mouse_leave<F: Fn(&mut EventContext) + 'static>(mut self, f: F) -> Self {
self.on_leave = Some(Rc::new(f));
self
}
pub fn variant(mut self, v: ButtonVariant) -> Self {
self.variant = v;
self
}
pub fn size(mut self, s: ButtonSize) -> Self {
self.size = s;
self
}
pub fn text_style(mut self, s: TextStyle) -> Self {
self.set_text_style(s);
self
}
pub fn font_size(mut self, v: f64) -> Self {
if let ButtonContent::Text { style, .. } = &mut self.content {
style.get_or_insert_with(TextStyle::default).font_size = v;
}
self
}
pub fn color(mut self, c: Color) -> Self {
if let ButtonContent::Text { style, .. } = &mut self.content {
style.get_or_insert_with(TextStyle::default).color = c;
}
self
}
pub fn wrap(mut self, v: bool) -> Self {
if let ButtonContent::Text { wrap, .. } = &mut self.content {
*wrap = v;
}
self
}
pub fn font_weight(mut self, w: impl Into<FontWeight>) -> Self {
if let ButtonContent::Text { style, .. } = &mut self.content {
style.get_or_insert_with(TextStyle::default).font_weight = w.into();
}
self
}
pub fn paint_style(mut self, p: PaintStyle) -> Self {
self.paint = Some(p);
self
}
pub fn background(mut self, c: Color) -> Self {
self.paint
.get_or_insert_with(PaintStyle::new)
.background_color = Some(c);
self
}
pub fn hover_background(mut self, c: Color) -> Self {
self.paint
.get_or_insert_with(PaintStyle::new)
.hover_background = Some(c);
self
}
pub fn pressed_background(mut self, c: Color) -> Self {
self.paint
.get_or_insert_with(PaintStyle::new)
.pressed_background = Some(c);
self
}
pub fn radius(mut self, r: f32) -> Self {
self.paint.get_or_insert_with(PaintStyle::new).border_radius = r;
self
}
pub fn border(mut self, width: f32, color: Color) -> Self {
let p = self.paint.get_or_insert_with(PaintStyle::new);
p.border_width = width;
p.border_color = Some(color);
self
}
pub fn opacity(mut self, o: f32) -> Self {
self.paint.get_or_insert_with(PaintStyle::new).opacity = o.clamp(0.0, 1.0);
self
}
pub fn padding_h(mut self, v: f32) -> Self {
self.padding_h = Some(v);
self
}
pub fn padding_v(mut self, v: f32) -> Self {
self.padding_v = Some(v);
self
}
pub fn tooltip(mut self, text: impl Into<String>) -> Self {
self.tooltip = Some(text.into());
self
}
}
struct ButtonNode {
layout: FlexStyle,
paint: PaintStyle,
children: Vec<ViewNode>,
listeners: Vec<Listener>,
}
impl Widget for ButtonNode {
fn build(&self, _ctx: &mut BuildContext) -> ViewNode {
ViewNode::Div {
layout: self.layout.clone(),
paint: self.paint.clone(),
key: None,
children: self.children.clone(),
listeners: self.listeners.clone(),
}
}
}
impl Widget for Button {
fn build(&self, ctx: &mut BuildContext) -> ViewNode {
let t = theme::current();
let (base_paint, base_text) = resolve_variant(t, self.variant);
let (def_pv, def_ph, def_font) = button_size_metrics(t, self.size);
let content_node = self.build_content(ctx, base_text, def_font, t);
let content = ViewNode::Div {
layout: FlexStyle::row()
.justify_content(FlexAlign::Center)
.align_items(FlexAlign::Center)
.flex_grow(1.0),
paint: PaintStyle::default(),
key: None,
children: vec![content_node],
listeners: vec![],
};
let mut layout = FlexStyle::block();
let ph = self.padding_h.unwrap_or(def_ph);
let pv = self.padding_v.unwrap_or(def_pv);
if let Some(px) = self.fixed_size {
layout = layout.width(px).height(px);
} else {
layout = layout
.padding_left(ph)
.padding_right(ph)
.padding_top(pv)
.padding_bottom(pv);
}
layout = self.layout.apply(layout);
let mut paint = base_paint;
if self.disabled {
paint = PaintStyle::new()
.background(t.background.disabled_default)
.radius(t.radius.small);
} else if let Some(custom) = &self.paint {
if custom.background_color.is_some() {
paint.background_color = custom.background_color;
}
if custom.hover_background.is_some() {
paint.hover_background = custom.hover_background;
}
if custom.pressed_background.is_some() {
paint.pressed_background = custom.pressed_background;
}
if custom.border_color.is_some() {
paint.border_color = custom.border_color;
paint.border_width = custom.border_width;
}
if custom.border_radius != PaintStyle::new().border_radius {
paint.border_radius = custom.border_radius;
}
if custom.opacity != PaintStyle::new().opacity {
paint.opacity = custom.opacity;
}
paint.clip_content = custom.clip_content;
}
let btn_layout = layout;
let btn_paint = paint;
let btn_children = vec![content];
let btn_listeners = if self.disabled {
Vec::new()
} else {
let mut l = self.listeners.clone();
if let Some(cb) = &self.on_enter {
l.push(Listener::on_mouse_enter(Rc::clone(cb)));
}
if let Some(cb) = &self.on_leave {
l.push(Listener::on_mouse_leave(Rc::clone(cb)));
}
l
};
let btn_node = ViewNode::Div {
layout: btn_layout.clone(),
paint: btn_paint.clone(),
key: None,
children: btn_children.clone(),
listeners: btn_listeners.clone(),
};
if let Some(tip) = &self.tooltip {
let wrapper = ButtonNode {
layout: btn_layout,
paint: btn_paint,
children: btn_children,
listeners: btn_listeners,
};
return ctx.child(0, &Tooltip::new(Box::new(wrapper), tip.clone()));
}
btn_node
}
}
impl Button {
fn build_content(
&self,
ctx: &mut BuildContext,
base_text: Color,
def_font: f64,
t: theme::Theme,
) -> ViewNode {
match &self.content {
ButtonContent::Text { label, style, wrap } => {
let label_style = {
let mut s = TextStyle {
font_size: def_font,
color: if self.disabled {
t.text.subtle_default
} else {
base_text
},
..TextStyle::default()
};
if let Some(custom) = style {
if custom.font_size != TextStyle::default().font_size {
s.font_size = custom.font_size;
}
if custom.color != TextStyle::default().color {
s.color = custom.color;
}
if custom.font_family != TextStyle::default().font_family {
s.font_family = custom.font_family.clone();
}
if custom.font_weight != TextStyle::default().font_weight {
s.font_weight = custom.font_weight.clone();
}
if custom.line_height != TextStyle::default().line_height {
s.line_height = custom.line_height;
}
if custom.max_width != TextStyle::default().max_width {
s.max_width = custom.max_width;
}
if custom.text_align != TextStyle::default().text_align {
s.text_align = custom.text_align;
}
}
s.wrap = *wrap;
s
};
ViewNode::Text {
content: label.clone(),
style: label_style,
layout: FlexStyle::default(),
key: None,
listeners: vec![],
}
}
ButtonContent::Icon {
name,
size,
color,
hover_color,
pressed_color,
} => {
let is_primary = self.variant == ButtonVariant::Primary;
let icon_color = color.unwrap_or(base_text);
let icon_hover = hover_color.unwrap_or(if self.disabled {
t.text.subtle_default
} else if is_primary {
Color::WHITE
} else {
t.background.brand_default
});
let icon_pressed = pressed_color.unwrap_or(if self.disabled {
t.text.subtle_default
} else if is_primary {
Color::WHITE
} else {
t.background.brand_clicked
});
let icon = Icon::new(*name, *size)
.color(if self.disabled {
t.text.subtle_default
} else {
icon_color
})
.hover_color(icon_hover)
.pressed_color(icon_pressed);
ctx.child(0, &icon)
}
ButtonContent::Widget(w) => ctx.child(0, w.as_ref()),
}
}
}