use crate::core::{Color, Element};
use super::display::Text;
use super::theme::{ComponentState, ComponentVariant, Theme, get_theme};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ActionRole {
#[default]
Primary,
Secondary,
Destructive,
}
impl ActionRole {
pub(crate) fn variant(self) -> ComponentVariant {
match self {
Self::Primary => ComponentVariant::Primary,
Self::Secondary => ComponentVariant::Secondary,
Self::Destructive => ComponentVariant::Error,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ActionState {
#[default]
Rest,
Focused,
Disabled,
}
impl From<ActionState> for ComponentState {
fn from(state: ActionState) -> Self {
match state {
ActionState::Rest => ComponentState::Rest,
ActionState::Focused => ComponentState::Focused,
ActionState::Disabled => ComponentState::Disabled,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ActionShape {
#[default]
Brackets,
Angles,
Parens,
Plain,
Padded,
}
impl ActionShape {
pub fn format_label(self, label: &str, hint: Option<char>, show_hint: bool) -> String {
let hint = if show_hint {
hint.map(|c| format!("({})", c)).unwrap_or_default()
} else {
String::new()
};
match self {
Self::Brackets => format!("[{}]{}", label, hint),
Self::Angles => format!("<{}>{}", label, hint),
Self::Parens => format!("({}){}", label, hint),
Self::Plain => format!("{}{}", label, hint),
Self::Padded => format!("[ {} ]{}", label, hint),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ActionStyle {
pub fg: Color,
pub bg: Option<Color>,
pub bold: bool,
pub dim: bool,
}
#[derive(Debug, Clone)]
pub struct ActionButton {
label: String,
role: ActionRole,
state: ActionState,
shape: ActionShape,
hint: Option<char>,
show_hint: bool,
}
impl ActionButton {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
role: ActionRole::Primary,
state: ActionState::Rest,
shape: ActionShape::Brackets,
hint: None,
show_hint: false,
}
}
pub fn role(mut self, role: ActionRole) -> Self {
self.role = role;
self
}
pub fn state(mut self, state: ActionState) -> Self {
self.state = state;
self
}
pub fn shape(mut self, shape: ActionShape) -> Self {
self.shape = shape;
self
}
pub fn hint(mut self, hint: Option<char>) -> Self {
self.hint = hint;
self
}
pub fn show_hint(mut self, show: bool) -> Self {
self.show_hint = show;
self
}
pub fn style(&self) -> ActionStyle {
self.style_with_theme(&get_theme())
}
pub fn style_with_theme(&self, theme: &Theme) -> ActionStyle {
theme.action_style(self.role, self.state)
}
pub fn formatted_label(&self) -> String {
self.shape
.format_label(&self.label, self.hint, self.show_hint)
}
pub fn into_text(self) -> Text {
let theme = get_theme();
self.into_text_with_theme(&theme)
}
pub fn into_text_with_theme(self, theme: &Theme) -> Text {
let style = self.style_with_theme(theme);
let mut text = Text::new(self.formatted_label()).color(style.fg);
if let Some(bg) = style.bg {
text = text.background(bg);
}
if style.bold {
text = text.bold();
}
if style.dim {
text = text.dim();
}
text
}
pub fn into_element(self) -> Element {
self.into_text().into_element()
}
}
impl Theme {
pub fn action_style(&self, role: ActionRole, state: ActionState) -> ActionStyle {
let variant = self.variant_style(role.variant(), state.into());
let disabled = state == ActionState::Disabled;
ActionStyle {
fg: variant.fg,
bg: Some(variant.bg),
bold: variant.bold,
dim: disabled && self.design_tokens().states.disabled_dim,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::Theme;
#[test]
fn test_action_shape_formats_hints() {
assert_eq!(
ActionShape::Padded.format_label("Save", Some('S'), true),
"[ Save ](S)"
);
assert_eq!(
ActionShape::Plain.format_label("Cancel", Some('C'), false),
"Cancel"
);
}
#[test]
fn test_action_button_resolves_theme_tokens() {
let theme = Theme::dark();
let style = ActionButton::new("Delete")
.role(ActionRole::Destructive)
.state(ActionState::Focused)
.style_with_theme(&theme);
assert_eq!(style.fg, theme.components.button.danger_text);
assert_eq!(style.bg, Some(theme.error));
assert!(style.bold);
}
}