use crate::lowering::{LoweringContext, NodeBuilder};
use crate::ui::traits::Lower;
use crate::ui::Node;
use crate::{ActionEnvelope, Env, InteractionStateMap};
use fission_ir::{
op::{BoxShadow, Color as IrColor, Fill, LayoutOp, Op, PaintOp, Stroke},
ActionEntry, ActionSet, NodeId, Role, Semantics,
};
use fission_theme::{ButtonHierarchy, ComponentSize, ComponentState};
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum ButtonVariant {
#[default]
Filled,
Outline,
Ghost,
Primary,
SecondaryColor,
SecondaryGray,
TertiaryColor,
TertiaryGray,
LinkColor,
LinkGray,
Destructive,
}
impl ButtonVariant {
fn hierarchy(self) -> ButtonHierarchy {
match self {
ButtonVariant::Filled | ButtonVariant::Primary => ButtonHierarchy::Primary,
ButtonVariant::Outline | ButtonVariant::SecondaryGray => ButtonHierarchy::SecondaryGray,
ButtonVariant::Ghost | ButtonVariant::TertiaryGray => ButtonHierarchy::TertiaryGray,
ButtonVariant::SecondaryColor => ButtonHierarchy::SecondaryColor,
ButtonVariant::TertiaryColor => ButtonHierarchy::TertiaryColor,
ButtonVariant::LinkColor => ButtonHierarchy::LinkColor,
ButtonVariant::LinkGray => ButtonHierarchy::LinkGray,
ButtonVariant::Destructive => ButtonHierarchy::Destructive,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum ButtonContentAlign {
#[default]
Center,
Start,
End,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Button {
pub id: Option<NodeId>,
pub child: Option<Box<Node>>,
pub on_press: Option<ActionEnvelope>,
pub semantics: Option<Semantics>,
pub width: Option<f32>,
pub height: Option<f32>,
pub min_width: Option<f32>,
pub max_width: Option<f32>,
pub flex_grow: f32,
pub flex_shrink: f32,
pub padding: Option<[f32; 4]>,
pub style: Option<ButtonStyleOverride>,
pub variant: ButtonVariant,
#[serde(default)]
pub size: ComponentSize,
pub background_fill: Option<Fill>,
pub text_color: Option<IrColor>,
#[serde(default)]
pub content_align: ButtonContentAlign,
pub disabled: bool,
}
impl Button {
pub fn background_fill(mut self, fill: Fill) -> Self {
self.background_fill = Some(fill);
self
}
pub fn text_color(mut self, color: IrColor) -> Self {
self.text_color = Some(color);
self
}
pub fn flex_grow(mut self, grow: f32) -> Self {
self.flex_grow = grow;
self
}
pub fn flex_shrink(mut self, shrink: f32) -> Self {
self.flex_shrink = shrink;
self
}
pub fn min_width(mut self, width: f32) -> Self {
self.min_width = Some(width);
self
}
pub fn max_width(mut self, width: f32) -> Self {
self.max_width = Some(width);
self
}
pub fn into_node(self) -> crate::ui::Node {
crate::ui::Node::Button(self)
}
}
impl Default for Button {
fn default() -> Self {
Self {
id: None,
child: None,
on_press: None,
semantics: None,
width: None,
height: None,
min_width: None,
max_width: None,
flex_grow: 0.0,
flex_shrink: 1.0,
padding: None,
style: None,
variant: ButtonVariant::Filled,
size: ComponentSize::Md,
background_fill: None,
text_color: None,
content_align: ButtonContentAlign::Center,
disabled: false,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct ButtonStyleOverride {}
struct ButtonStyleResolved {
background_fill: Option<Fill>,
text_color: IrColor,
padding_horizontal: f32,
padding_vertical: f32,
height: f32,
corner_radius: f32,
shadow: Option<BoxShadow>,
shadows: Vec<BoxShadow>,
stroke: Option<Stroke>,
font_size: f32,
font_weight: u16,
line_height: Option<f32>,
}
impl Button {
fn resolve_style(
&self,
env: &Env,
interaction: &InteractionStateMap,
self_id: NodeId,
) -> ButtonStyleResolved {
let default_style = &env.theme.components.button;
let tokens = &env.theme.tokens.colors;
let is_hovered = interaction.is_hovered(self_id) && !self.disabled;
let is_pressed = interaction.is_pressed(self_id) && !self.disabled;
let is_focused = interaction.is_focused(self_id) && !self.disabled;
let component_state = if self.disabled {
ComponentState::Disabled
} else if is_pressed {
ComponentState::Active
} else if is_focused {
ComponentState::Focus
} else if is_hovered {
ComponentState::Hover
} else {
ComponentState::Default
};
let component_style =
default_style.resolve(self.variant.hierarchy(), self.size, component_state);
let stroke = component_style
.border
.clone()
.or_else(|| component_style.inset_border())
.map(|border| Stroke {
fill: border.fill,
width: border.width,
dash_array: None,
line_cap: fission_ir::op::LineCap::Butt,
line_join: fission_ir::op::LineJoin::Miter,
})
.or_else(|| {
if is_focused {
default_style.focus_stroke.clone()
} else {
None
}
});
let shadows = component_style.outer_shadows();
let shadow = shadows.first().copied().or_else(|| {
if matches!(self.variant, ButtonVariant::Filled | ButtonVariant::Primary) {
if is_pressed {
default_style.elevation_pressed
} else if is_hovered {
default_style.elevation_hover
} else {
default_style.elevation_rest
}
} else {
None
}
});
ButtonStyleResolved {
background_fill: self
.background_fill
.clone()
.or_else(|| component_style.background.clone()),
text_color: self
.text_color
.unwrap_or(component_style.text_color.unwrap_or(tokens.primary)),
padding_horizontal: component_style
.padding_x
.unwrap_or(default_style.padding_horizontal),
padding_vertical: component_style
.padding_y
.unwrap_or(default_style.padding_vertical),
height: component_style.height.unwrap_or(default_style.height),
corner_radius: component_style.radius.unwrap_or(default_style.radius),
shadow,
shadows,
stroke,
font_size: component_style.font_size.unwrap_or(default_style.text_size),
font_weight: component_style
.font_weight
.unwrap_or(default_style.font_weight),
line_height: component_style.line_height,
}
}
fn should_attach_semantics(&self) -> bool {
self.semantics.is_some() || self.on_press.is_some()
}
fn build_semantics(&self) -> Option<Semantics> {
if !self.should_attach_semantics() {
return None;
}
let mut semantics = self
.semantics
.clone()
.unwrap_or_else(default_button_semantics);
semantics.disabled = self.disabled;
if let Some(action_envelope) = &self.on_press {
if !self.disabled {
semantics.actions.entries.push(ActionEntry {
trigger: fission_ir::semantics::ActionTrigger::Default,
action_id: action_envelope.id.as_u128(),
payload_data: Some(action_envelope.payload.clone()),
});
}
}
Some(semantics)
}
}
impl Lower for Button {
fn lower(&self, cx: &mut LoweringContext) -> NodeId {
let semantics_op = self.build_semantics();
let outermost_id = self.id.unwrap_or_else(|| cx.next_node_id());
let (layout_node_id, final_id) = if let Some(_) = semantics_op {
(cx.next_node_id(), outermost_id)
} else {
(outermost_id, outermost_id)
};
let resolved_style = self.resolve_style(cx.env, &cx.runtime_state.interaction, final_id);
cx.push_scope(layout_node_id);
let mut button_builder = NodeBuilder::new(
layout_node_id,
Op::Layout(LayoutOp::Box {
width: self.width,
height: self.height,
min_width: self.min_width,
max_width: self.max_width,
min_height: if self.height.is_some() {
None
} else {
Some(resolved_style.height)
},
max_height: None,
padding: self.padding.unwrap_or([
resolved_style.padding_horizontal,
resolved_style.padding_horizontal,
resolved_style.padding_vertical,
resolved_style.padding_vertical,
]),
flex_grow: self.flex_grow,
flex_shrink: self.flex_shrink,
aspect_ratio: None,
}),
);
for shadow in &resolved_style.shadows {
let shadow_id = NodeBuilder::new(
cx.next_node_id(),
Op::Paint(PaintOp::DrawRect {
fill: None,
stroke: None,
corner_radius: resolved_style.corner_radius,
shadow: Some(*shadow),
}),
)
.build(cx);
button_builder.add_child(shadow_id);
}
let background_id = NodeBuilder::new(
cx.next_node_id(),
Op::Paint(PaintOp::DrawRect {
fill: resolved_style.background_fill,
stroke: resolved_style.stroke,
corner_radius: resolved_style.corner_radius,
shadow: if resolved_style.shadows.is_empty() {
resolved_style.shadow
} else {
None
},
}),
)
.build(cx);
button_builder.add_child(background_id);
if let Some(child_widget) = &self.child {
let child_id = if let Node::Text(mut text_widget) = *child_widget.clone() {
text_widget.color = Some(resolved_style.text_color);
text_widget.font_size = Some(resolved_style.font_size);
text_widget.font_weight = Some(resolved_style.font_weight);
text_widget.line_height = resolved_style.line_height;
text_widget.lower(cx)
} else {
child_widget.lower(cx)
};
let aligned_id = match self.content_align {
ButtonContentAlign::Center => {
let mut align_builder =
NodeBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::Align));
align_builder.add_child(child_id);
align_builder.build(cx)
}
ButtonContentAlign::Start | ButtonContentAlign::End => {
let justify = match self.content_align {
ButtonContentAlign::Start => fission_ir::op::JustifyContent::Start,
ButtonContentAlign::End => fission_ir::op::JustifyContent::End,
ButtonContentAlign::Center => fission_ir::op::JustifyContent::Center,
};
let mut flex_builder = NodeBuilder::new(
cx.next_node_id(),
Op::Layout(LayoutOp::Flex {
direction: fission_ir::FlexDirection::Row,
wrap: fission_ir::FlexWrap::NoWrap,
flex_grow: 1.0,
flex_shrink: 0.0,
padding: [0.0; 4],
gap: None,
align_items: fission_ir::op::AlignItems::Center,
justify_content: justify,
}),
);
flex_builder.add_child(child_id);
flex_builder.build(cx)
}
};
button_builder.add_child(aligned_id);
}
let button_node_id = button_builder.build(cx);
if let Some(op) = semantics_op {
let mut semantics_builder = NodeBuilder::new(final_id, Op::Semantics(op));
semantics_builder.add_child(button_node_id);
let res_id = semantics_builder.build(cx);
cx.pop_scope();
return res_id;
}
cx.pop_scope();
button_node_id
}
}
fn default_button_semantics() -> Semantics {
Semantics {
role: Role::Button,
label: None,
identifier: None,
value: None,
actions: ActionSet::default(),
action_scope_id: None,
focusable: true,
multiline: false,
masked: false,
input_mask: None,
ime_preedit_range: None,
checked: None,
disabled: false,
read_only: false,
autofocus: false,
draggable: false,
scrollable_x: false,
scrollable_y: false,
min_value: None,
max_value: None,
current_value: None,
is_focus_scope: false,
is_focus_barrier: false,
drag_payload: None,
hero_tag: None,
focus_index: None,
text_input_type: fission_ir::semantics::TextInputType::Text,
text_input_action: fission_ir::semantics::TextInputAction::Done,
text_capitalization: fission_ir::semantics::TextCapitalization::None,
max_length: None,
max_length_enforcement: fission_ir::semantics::MaxLengthEnforcement::Enforced,
input_formatters: Vec::new(),
autocorrect: true,
enable_suggestions: true,
spell_check: true,
smart_dashes: true,
smart_quotes: true,
autofill_hints: Vec::new(),
scroll_padding: None,
capture_tab: false,
auto_indent: false,
}
}