use crate::event::{Key, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
use crate::layout::Rect;
use crate::render::Cell;
use crate::style::Color;
use crate::widget::theme::{EDITOR_BG, SECONDARY_TEXT, SEPARATOR_COLOR};
use crate::widget::traits::{
EventResult, Interactive, RenderContext, View, WidgetProps, WidgetState,
};
use crate::{impl_styled_view, impl_widget_builders};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ButtonVariant {
#[default]
Default,
Primary,
Danger,
Ghost,
Success,
}
#[derive(Clone, Debug)]
pub struct Button {
label: String,
icon: Option<char>,
variant: ButtonVariant,
state: WidgetState,
props: WidgetProps,
width: Option<u16>,
}
impl Button {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
icon: None,
variant: ButtonVariant::Default,
state: WidgetState::new(),
props: WidgetProps::new(),
width: None,
}
}
pub fn icon(mut self, icon: char) -> Self {
self.icon = Some(icon);
self
}
pub fn primary(label: impl Into<String>) -> Self {
Self::new(label).variant(ButtonVariant::Primary)
}
pub fn danger(label: impl Into<String>) -> Self {
Self::new(label).variant(ButtonVariant::Danger)
}
pub fn ghost(label: impl Into<String>) -> Self {
Self::new(label).variant(ButtonVariant::Ghost)
}
pub fn success(label: impl Into<String>) -> Self {
Self::new(label).variant(ButtonVariant::Success)
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
}
pub fn width(mut self, width: u16) -> Self {
self.width = Some(width);
self
}
pub fn handle_key(&mut self, key: &Key) -> bool {
if self.state.disabled {
return false;
}
matches!(key, Key::Enter | Key::Char(' '))
}
pub fn handle_mouse(&mut self, event: &MouseEvent, area: Rect) -> (bool, bool) {
if self.state.disabled {
return (false, false);
}
let inside = area.contains(event.x, event.y);
let mut needs_render = false;
let mut was_clicked = false;
match event.kind {
MouseEventKind::Down(MouseButton::Left) if inside && !self.state.pressed => {
self.state.pressed = true;
needs_render = true;
}
MouseEventKind::Up(MouseButton::Left) if self.state.pressed => {
self.state.pressed = false;
needs_render = true;
if inside {
was_clicked = true;
}
}
MouseEventKind::Move => {
let was_hovered = self.state.hovered;
self.state.hovered = inside;
if was_hovered != self.state.hovered {
needs_render = true;
}
}
_ => {}
}
(needs_render, was_clicked)
}
pub fn is_pressed(&self) -> bool {
self.state.is_pressed()
}
pub fn is_hovered(&self) -> bool {
self.state.is_hovered()
}
fn get_variant_base_colors(&self) -> (Color, Color) {
match self.variant {
ButtonVariant::Default => (Color::WHITE, SEPARATOR_COLOR),
ButtonVariant::Primary => (Color::WHITE, Color::rgb(37, 99, 235)),
ButtonVariant::Danger => (Color::WHITE, Color::rgb(220, 38, 38)),
ButtonVariant::Ghost => (SECONDARY_TEXT, EDITOR_BG),
ButtonVariant::Success => (Color::WHITE, Color::rgb(22, 163, 74)),
}
}
fn get_colors_from_ctx(&self, ctx: &RenderContext) -> (Color, Color) {
let (variant_fg, variant_bg) = self.get_variant_base_colors();
self.state
.resolve_colors_interactive(ctx.style, variant_fg, variant_bg)
}
}
impl Default for Button {
fn default() -> Self {
Self::new("")
}
}
impl View for Button {
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width == 0 || area.height == 0 {
return;
}
let (fg, bg) = self.get_colors_from_ctx(ctx);
let icon_width = if self.icon.is_some() { 2u16 } else { 0 }; let label_width = self.label.chars().count() as u16;
let content_width = icon_width + label_width;
let padding = 2; let min_width = self.width.unwrap_or(0);
let button_width = (content_width + padding * 2).max(min_width).min(area.width);
for x in 0..button_width {
let mut cell = Cell::new(' ');
cell.bg = Some(bg);
ctx.set(x, 0, cell);
}
let content_start = (button_width.saturating_sub(content_width)) / 2;
let mut x = content_start;
if let Some(icon) = self.icon {
if x < button_width {
let mut cell = Cell::new(icon);
cell.fg = Some(fg);
cell.bg = Some(bg);
if self.state.focused && !self.state.disabled {
cell.modifier = crate::render::Modifier::BOLD;
}
ctx.set(x, 0, cell);
x += 1;
if x < button_width {
let mut space = Cell::new(' ');
space.bg = Some(bg);
ctx.set(x, 0, space);
x += 1;
}
}
}
if self.state.focused && !self.state.disabled {
ctx.draw_text_bg_bold(x, 0, &self.label, fg, bg);
} else {
ctx.draw_text_bg(x, 0, &self.label, fg, bg);
}
if self.state.focused && !self.state.disabled {
ctx.draw_focus_brackets(0, button_width, Color::CYAN);
}
}
crate::impl_view_meta!("Button");
}
impl Interactive for Button {
fn handle_key(&mut self, event: &KeyEvent) -> EventResult {
if self.state.disabled {
return EventResult::Ignored;
}
match event.key {
Key::Enter | Key::Char(' ') => EventResult::ConsumedAndRender,
_ => EventResult::Ignored,
}
}
fn handle_mouse(&mut self, event: &MouseEvent, area: Rect) -> EventResult {
if self.state.disabled {
return EventResult::Ignored;
}
let inside = area.contains(event.x, event.y);
match event.kind {
MouseEventKind::Down(MouseButton::Left) if inside => {
if !self.state.pressed {
self.state.pressed = true;
return EventResult::ConsumedAndRender;
}
EventResult::Consumed
}
MouseEventKind::Up(MouseButton::Left) => {
if self.state.pressed {
self.state.pressed = false;
return if inside {
EventResult::ConsumedAndRender
} else {
EventResult::Consumed
};
}
EventResult::Ignored
}
MouseEventKind::Move => {
let was_hovered = self.state.hovered;
self.state.hovered = inside;
if was_hovered != self.state.hovered {
EventResult::ConsumedAndRender
} else {
EventResult::Ignored
}
}
_ => EventResult::Ignored,
}
}
fn focusable(&self) -> bool {
!self.state.disabled
}
fn on_focus(&mut self) {
self.state.focused = true;
}
fn on_blur(&mut self) {
self.state.focused = false;
self.state.reset_transient();
}
}
pub fn button(label: impl Into<String>) -> Button {
Button::new(label)
}
impl_styled_view!(Button);
impl_widget_builders!(Button);