use blinc_core::Color;
use blinc_layout::div::ElementBuilder;
use blinc_layout::prelude::*;
use blinc_layout::stateful::{use_shared_state, ButtonState, SharedState};
use blinc_layout::tree::{LayoutNodeId, LayoutTree};
use blinc_layout::widgets::button as layout_button;
use blinc_layout::InstanceKey;
use blinc_theme::{ColorToken, ThemeState};
use std::sync::Arc;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ButtonVariant {
#[default]
Primary,
Secondary,
Destructive,
Outline,
Ghost,
Link,
}
impl ButtonVariant {
fn css_class(&self) -> &'static str {
match self {
ButtonVariant::Primary => "cn-button--primary",
ButtonVariant::Secondary => "cn-button--secondary",
ButtonVariant::Destructive => "cn-button--destructive",
ButtonVariant::Outline => "cn-button--outline",
ButtonVariant::Ghost => "cn-button--ghost",
ButtonVariant::Link => "cn-button--link",
}
}
pub(crate) fn background(&self, theme: &ThemeState, state: ButtonState) -> Color {
match (self, state) {
(_, ButtonState::Disabled) => self.base_background(theme).with_alpha(0.5),
(ButtonVariant::Primary, ButtonState::Pressed) => {
theme.color(ColorToken::PrimaryActive)
}
(ButtonVariant::Secondary, ButtonState::Pressed) => {
theme.color(ColorToken::SecondaryActive)
}
(ButtonVariant::Destructive, ButtonState::Pressed) => {
darken(theme.color(ColorToken::Error), 0.15)
}
(ButtonVariant::Outline | ButtonVariant::Ghost, ButtonState::Pressed) => {
theme.color(ColorToken::TextPrimary).with_alpha(0.1)
}
(ButtonVariant::Link, ButtonState::Pressed) => Color::TRANSPARENT,
(ButtonVariant::Primary, ButtonState::Hovered) => theme.color(ColorToken::PrimaryHover),
(ButtonVariant::Secondary, ButtonState::Hovered) => {
theme.color(ColorToken::SecondaryHover)
}
(ButtonVariant::Destructive, ButtonState::Hovered) => {
darken(theme.color(ColorToken::Error), 0.1)
}
(ButtonVariant::Outline | ButtonVariant::Ghost, ButtonState::Hovered) => {
theme.color(ColorToken::TextPrimary).with_alpha(0.05)
}
(ButtonVariant::Link, ButtonState::Hovered) => Color::TRANSPARENT,
_ => self.base_background(theme),
}
}
fn base_background(&self, theme: &ThemeState) -> Color {
match self {
ButtonVariant::Primary => theme.color(ColorToken::Primary),
ButtonVariant::Secondary => theme.color(ColorToken::Secondary),
ButtonVariant::Destructive => theme.color(ColorToken::Error),
ButtonVariant::Outline | ButtonVariant::Ghost | ButtonVariant::Link => {
Color::TRANSPARENT
}
}
}
pub(crate) fn foreground(&self, theme: &ThemeState) -> Color {
match self {
ButtonVariant::Primary | ButtonVariant::Destructive => {
theme.color(ColorToken::TextInverse)
}
ButtonVariant::Secondary | ButtonVariant::Outline | ButtonVariant::Ghost => {
theme.color(ColorToken::TextPrimary)
}
ButtonVariant::Link => theme.color(ColorToken::Primary),
}
}
pub(crate) fn border(&self, theme: &ThemeState) -> Option<Color> {
match self {
ButtonVariant::Outline => Some(theme.color(ColorToken::Border)),
_ => None,
}
}
}
fn darken(color: Color, amount: f32) -> Color {
Color::rgba(
(color.r * (1.0 - amount)).max(0.0),
(color.g * (1.0 - amount)).max(0.0),
(color.b * (1.0 - amount)).max(0.0),
color.a,
)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ButtonSize {
Small,
#[default]
Medium,
Large,
Icon,
}
impl ButtonSize {
fn css_class(&self) -> &'static str {
match self {
ButtonSize::Small => "cn-button--sm",
ButtonSize::Medium => "cn-button--md",
ButtonSize::Large => "cn-button--lg",
ButtonSize::Icon => "cn-button--icon",
}
}
fn height(&self) -> f32 {
match self {
ButtonSize::Small => 32.0,
ButtonSize::Medium => 40.0,
ButtonSize::Large => 44.0,
ButtonSize::Icon => 40.0,
}
}
fn padding_x(&self) -> f32 {
match self {
ButtonSize::Small => 12.0,
ButtonSize::Medium => 16.0,
ButtonSize::Large => 24.0,
ButtonSize::Icon => 8.0,
}
}
fn padding_y(&self) -> f32 {
match self {
ButtonSize::Small => 4.0,
ButtonSize::Medium => 8.0,
ButtonSize::Large => 12.0,
ButtonSize::Icon => 8.0,
}
}
fn font_size(&self) -> f32 {
match self {
ButtonSize::Small => 13.0,
ButtonSize::Medium => 14.0,
ButtonSize::Large => 16.0,
ButtonSize::Icon => 14.0,
}
}
fn border_radius(&self) -> f32 {
match self {
ButtonSize::Small => 4.0,
ButtonSize::Medium => 6.0,
ButtonSize::Large => 8.0,
ButtonSize::Icon => 6.0,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum IconPosition {
#[default]
Start,
End,
}
pub(crate) fn use_button_state(key: &str) -> SharedState<ButtonState> {
use_shared_state::<ButtonState>(key)
}
pub(crate) fn reset_button_state(key: &str) {
let state = use_button_state(key);
let mut inner = state.lock().unwrap();
inner.state = ButtonState::Idle;
}
#[track_caller]
pub fn button(label: impl Into<String>) -> ButtonBuilder {
ButtonBuilder {
key: InstanceKey::new("button"),
config: ButtonConfig {
label: label.into(),
variant: ButtonVariant::default(),
btn_size: ButtonSize::default(),
disabled: false,
icon: None,
icon_position: IconPosition::default(),
icon_size: None,
text_color: None,
on_click: None,
},
built: std::cell::OnceCell::new(),
}
}
#[derive(Clone)]
#[allow(clippy::type_complexity)]
struct ButtonConfig {
label: String,
variant: ButtonVariant,
btn_size: ButtonSize,
disabled: bool,
icon: Option<String>,
icon_position: IconPosition,
icon_size: Option<f32>,
text_color: Option<Color>,
on_click: Option<Arc<dyn Fn(&blinc_layout::event_handler::EventContext) + Send + Sync>>,
}
pub struct Button {
inner: layout_button::Button,
}
impl Button {
fn from_config(instance_key: &str, config: ButtonConfig) -> Self {
let theme = ThemeState::get();
let font_size = config.btn_size.font_size();
let variant = config.variant;
let disabled = config.disabled;
let state_key = format!("_cn_btn_{}", instance_key);
let btn_state = use_button_state(&state_key);
if disabled {
let mut inner = btn_state.lock().unwrap();
inner.state = ButtonState::Disabled;
}
let bg = variant.base_background(theme);
let hover_bg = variant.background(theme, ButtonState::Hovered);
let pressed_bg = variant.background(theme, ButtonState::Pressed);
let label = config.label.clone();
let icon = config.icon.clone();
let icon_position = config.icon_position;
let custom_icon_size = config.icon_size;
let btn_size = config.btn_size;
let default_fg = config
.text_color
.unwrap_or_else(|| variant.foreground(theme));
let is_icon_only = config.icon.is_some() && config.label.is_empty();
let resolved_icon_size = config.icon_size.unwrap_or(font_size + 2.0);
let mut btn = layout_button::Button::with_content(btn_state, |_state| div())
.text_color(default_fg)
.bg_color(bg)
.hover_color(hover_bg)
.pressed_color(pressed_bg)
.rounded(config.btn_size.border_radius())
.items_center()
.justify_center()
.class("cn-button")
.class(variant.css_class())
.class(config.btn_size.css_class());
if is_icon_only {
let pad = config.btn_size.padding_y();
let dim = resolved_icon_size + pad * 2.0;
btn = btn.w(dim).h(dim);
} else {
btn = btn.w_fit();
}
let cfg_arc = btn.config_arc();
btn = btn.on_state(move |_state, container| {
let fg = cfg_arc.lock().unwrap().text_color;
if is_icon_only {
if let Some(ref icon_str) = icon {
let icon_size = custom_icon_size.unwrap_or(font_size + 2.0);
let svg_str = blinc_icons::to_svg(icon_str, icon_size);
let icon_svg = svg(&svg_str).size(icon_size, icon_size).color(fg);
container.merge(div().child(icon_svg));
}
} else {
let label_text = text(&label)
.size(font_size)
.color(fg)
.no_wrap()
.v_center()
.pointer_events_none()
.no_cursor();
let pad_x = btn_size.padding_x();
let pad_y = btn_size.padding_y();
let mut content = div()
.flex_row()
.items_center()
.justify_center()
.gap_px(6.0)
.padding_x_px(pad_x)
.padding_y_px(pad_y)
.pointer_events_none();
if let Some(ref icon_str) = icon {
let icon_size = custom_icon_size.unwrap_or(font_size + 2.0);
let svg_str = blinc_icons::to_svg(icon_str, icon_size);
let icon_svg = svg(&svg_str).size(icon_size, icon_size).color(fg);
match icon_position {
IconPosition::Start => {
content = content.child(icon_svg).child(label_text);
}
IconPosition::End => {
content = content.child(label_text).child(icon_svg);
}
}
} else {
content = content.child(label_text);
}
container.merge(div().child(content));
}
});
if disabled {
btn = btn.class("cn-button--disabled").opacity(0.5).disabled(true);
}
if variant != ButtonVariant::Link && variant != ButtonVariant::Ghost {
btn = btn.shadow_md();
}
if variant == ButtonVariant::Outline {
btn = btn.shadow_sm();
}
if let Some(border_color) = variant.border(theme) {
btn = btn.border(1.0, border_color);
}
if let Some(handler) = config.on_click {
btn = btn.on_click(move |ctx| handler(ctx));
}
Self { inner: btn }
}
pub fn class(mut self, name: impl Into<String>) -> Self {
self.inner = self.inner.class(&name.into());
self
}
pub fn id(mut self, id: &str) -> Self {
self.inner = self.inner.id(id);
self
}
}
impl ElementBuilder for Button {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> blinc_layout::element::RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_type_id(&self) -> blinc_layout::div::ElementTypeId {
self.inner.element_type_id()
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
self.inner.event_handlers()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.inner.layout_style()
}
fn element_classes(&self) -> &[String] {
self.inner.element_classes()
}
fn element_id(&self) -> Option<&str> {
self.inner.element_id()
}
}
pub struct ButtonBuilder {
key: InstanceKey,
config: ButtonConfig,
built: std::cell::OnceCell<Button>,
}
impl ButtonBuilder {
pub fn with_key(key: impl Into<String>, label: impl Into<String>) -> Self {
Self {
key: InstanceKey::explicit(key),
config: ButtonConfig {
label: label.into(),
variant: ButtonVariant::default(),
btn_size: ButtonSize::default(),
disabled: false,
icon: None,
icon_position: IconPosition::Start,
icon_size: None,
text_color: None,
on_click: None,
},
built: std::cell::OnceCell::new(),
}
}
fn get_or_build(&self) -> &Button {
self.built
.get_or_init(|| Button::from_config(self.key.get(), self.config.clone()))
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.config.variant = variant;
self
}
pub fn size(mut self, size: ButtonSize) -> Self {
self.config.btn_size = size;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.config.disabled = disabled;
self
}
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.config.icon = Some(icon.into());
self
}
pub fn icon_position(mut self, position: IconPosition) -> Self {
self.config.icon_position = position;
self
}
pub fn icon_size(mut self, size: f32) -> Self {
self.config.icon_size = Some(size);
self
}
pub fn color(mut self, color: impl Into<Color>) -> Self {
self.config.text_color = Some(color.into());
self
}
pub fn on_click<F>(mut self, handler: F) -> Self
where
F: Fn(&blinc_layout::event_handler::EventContext) + Send + Sync + 'static,
{
self.config.on_click = Some(Arc::new(handler));
self
}
pub fn build_component(self) -> Button {
Button::from_config(self.key.get(), self.config)
}
}
impl ElementBuilder for ButtonBuilder {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.get_or_build().build(tree)
}
fn render_props(&self) -> blinc_layout::element::RenderProps {
self.get_or_build().render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.get_or_build().children_builders()
}
fn element_type_id(&self) -> blinc_layout::div::ElementTypeId {
self.get_or_build().element_type_id()
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
self.get_or_build().event_handlers()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.get_or_build().layout_style()
}
fn element_classes(&self) -> &[String] {
self.get_or_build().element_classes()
}
fn element_id(&self) -> Option<&str> {
self.get_or_build().element_id()
}
}