1use crate::lowering::{LoweringContext, NodeBuilder};
2use crate::ui::traits::Lower;
3use crate::ui::Node;
4use crate::{ActionEnvelope, Env, InteractionStateMap};
5use fission_ir::{
6 op::{BoxShadow, Color as IrColor, Fill, LayoutOp, Op, PaintOp, Stroke},
7 ActionEntry, ActionSet, NodeId, Role, Semantics,
8};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
27pub enum ButtonVariant {
28 #[default]
30 Filled,
31 Outline,
33 Ghost,
35}
36
37#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
41pub enum ButtonContentAlign {
42 #[default]
44 Center,
45 Start,
47 End,
49}
50
51#[derive(Debug, Default, Clone, Serialize, Deserialize)]
71pub struct Button {
72 pub id: Option<NodeId>,
74 pub child: Option<Box<Node>>,
76 pub on_press: Option<ActionEnvelope>,
78 pub semantics: Option<Semantics>,
80 pub width: Option<f32>,
82 pub height: Option<f32>,
84 pub padding: Option<[f32; 4]>,
86 pub style: Option<ButtonStyleOverride>,
88 pub variant: ButtonVariant,
90 #[serde(default)]
92 pub content_align: ButtonContentAlign,
93 pub disabled: bool,
96}
97
98impl Button {
99 pub fn into_node(self) -> crate::ui::Node {
100 crate::ui::Node::Button(self)
101 }
102}
103
104#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
105pub struct ButtonStyleOverride {}
106
107struct ButtonStyleResolved {
108 background_color: Option<IrColor>,
109 text_color: IrColor,
110 padding_horizontal: f32,
111 padding_vertical: f32,
112 height: f32,
113 corner_radius: f32,
114 shadow: Option<BoxShadow>,
115 stroke: Option<Stroke>,
116}
117
118impl Button {
119 fn resolve_style(
120 &self,
121 env: &Env,
122 interaction: &InteractionStateMap,
123 self_id: NodeId,
124 ) -> ButtonStyleResolved {
125 let default_style = &env.theme.components.button;
126 let tokens = &env.theme.tokens.colors;
127
128 let is_hovered = interaction.is_hovered(self_id) && !self.disabled;
129 let is_pressed = interaction.is_pressed(self_id) && !self.disabled;
130 let is_focused = interaction.is_focused(self_id) && !self.disabled;
131
132 let (bg_color, text_color, border_stroke) = if self.disabled {
133 (
134 if self.variant == ButtonVariant::Filled { Some(tokens.border) } else { None }, tokens.text_secondary, if self.variant == ButtonVariant::Outline { Some(Stroke { color: tokens.border, width: 1.0 }) } else { None },
137 )
138 } else {
139 match self.variant {
140 ButtonVariant::Filled => (
141 Some(tokens.primary),
142 tokens.on_primary,
143 if is_focused { default_style.focus_stroke } else { None },
144 ),
145 ButtonVariant::Outline => (
146 if is_hovered { Some(tokens.surface) } else { None },
147 tokens.primary,
148 Some(Stroke { color: tokens.border, width: 1.0 }),
149 ),
150 ButtonVariant::Ghost => (
151 if is_hovered { Some(tokens.surface) } else { None },
152 tokens.primary,
153 None,
154 ),
155 }
156 };
157
158 let shadow = if self.variant == ButtonVariant::Filled {
159 if is_pressed {
160 default_style.elevation_pressed
161 } else if is_hovered {
162 default_style.elevation_hover
163 } else {
164 default_style.elevation_rest
165 }
166 } else {
167 None
168 };
169
170 ButtonStyleResolved {
171 background_color: bg_color,
172 text_color,
173 padding_horizontal: default_style.padding_horizontal,
174 padding_vertical: default_style.padding_vertical,
175 height: default_style.height,
176 corner_radius: default_style.radius,
177 shadow,
178 stroke: border_stroke,
179 }
180 }
181
182 fn should_attach_semantics(&self) -> bool {
183 self.semantics.is_some() || self.on_press.is_some()
184 }
185
186 fn build_semantics(&self) -> Option<Semantics> {
187 if !self.should_attach_semantics() {
188 return None;
189 }
190
191 let mut semantics = self
192 .semantics
193 .clone()
194 .unwrap_or_else(default_button_semantics);
195
196 semantics.disabled = self.disabled;
197
198 if let Some(action_envelope) = &self.on_press {
199 if !self.disabled {
200 semantics.actions.entries.push(ActionEntry {
201 trigger: fission_ir::semantics::ActionTrigger::Default,
202 action_id: action_envelope.id.as_u128(),
203 payload_data: Some(action_envelope.payload.clone()),
204 });
205 }
206 }
207
208 Some(semantics)
209 }
210}
211
212impl Lower for Button {
213 fn lower(&self, cx: &mut LoweringContext) -> NodeId {
214 let semantics_op = self.build_semantics();
215 let outermost_id = self.id.unwrap_or_else(|| cx.next_node_id());
216
217 let (layout_node_id, final_id) = if let Some(_) = semantics_op {
218 (cx.next_node_id(), outermost_id)
219 } else {
220 (outermost_id, outermost_id)
221 };
222
223 let resolved_style = self.resolve_style(cx.env, &cx.runtime_state.interaction, final_id);
224
225 cx.push_scope(layout_node_id);
226
227 let background_id = NodeBuilder::new(
228 cx.next_node_id(),
229 Op::Paint(PaintOp::DrawRect {
230 fill: resolved_style.background_color.map(|c| Fill { color: c }),
231 stroke: resolved_style.stroke,
232 corner_radius: resolved_style.corner_radius,
233 shadow: resolved_style.shadow,
234 }),
235 )
236 .build(cx);
237
238 let mut button_builder = NodeBuilder::new(
239 layout_node_id,
240 Op::Layout(LayoutOp::Box {
241 width: self.width,
242 height: self.height,
243 min_width: None,
244 max_width: None,
245 min_height: if self.height.is_some() { None } else { Some(resolved_style.height) },
246 max_height: None,
247 padding: self.padding.unwrap_or([
248 resolved_style.padding_horizontal,
249 resolved_style.padding_horizontal,
250 resolved_style.padding_vertical,
251 resolved_style.padding_vertical,
252 ]),
253 flex_grow: 0.0,
254 flex_shrink: 0.0,
255 aspect_ratio: None,
256 }),
257 );
258 button_builder.add_child(background_id);
259
260 if let Some(child_widget) = &self.child {
261 let child_id = if let Node::Text(mut text_widget) = *child_widget.clone() {
262 text_widget.color = Some(resolved_style.text_color);
263 text_widget.lower(cx)
264 } else {
265 child_widget.lower(cx)
266 };
267 let aligned_id = match self.content_align {
268 ButtonContentAlign::Center => {
269 let mut align_builder =
271 NodeBuilder::new(cx.next_node_id(), Op::Layout(LayoutOp::Align));
272 align_builder.add_child(child_id);
273 align_builder.build(cx)
274 }
275 ButtonContentAlign::Start | ButtonContentAlign::End => {
276 let justify = match self.content_align {
277 ButtonContentAlign::Start => fission_ir::op::JustifyContent::Start,
278 ButtonContentAlign::End => fission_ir::op::JustifyContent::End,
279 ButtonContentAlign::Center => fission_ir::op::JustifyContent::Center,
280 };
281 let mut flex_builder = NodeBuilder::new(
282 cx.next_node_id(),
283 Op::Layout(LayoutOp::Flex {
284 direction: fission_ir::FlexDirection::Row,
285 wrap: fission_ir::FlexWrap::NoWrap,
286 flex_grow: 1.0,
287 flex_shrink: 0.0,
288 padding: [0.0; 4],
289 gap: None,
290 align_items: fission_ir::op::AlignItems::Center,
291 justify_content: justify,
292 }),
293 );
294 flex_builder.add_child(child_id);
295 flex_builder.build(cx)
296 }
297 };
298 button_builder.add_child(aligned_id);
299 }
300
301 let button_node_id = button_builder.build(cx);
302
303 if let Some(op) = semantics_op {
304 let mut semantics_builder =
305 NodeBuilder::new(final_id, Op::Semantics(op));
306 semantics_builder.add_child(button_node_id);
307 let res_id = semantics_builder.build(cx);
308 cx.pop_scope();
309 return res_id;
310 }
311
312 cx.pop_scope();
313 button_node_id
314 }
315}
316
317fn default_button_semantics() -> Semantics {
318 Semantics {
319 role: Role::Button,
320 label: None,
321 value: None,
322 actions: ActionSet::default(),
323 focusable: true,
324 multiline: false,
325 masked: false,
326 input_mask: None,
327 ime_preedit_range: None,
328 checked: None,
329 disabled: false,
330 draggable: false,
331 scrollable_x: false,
332 scrollable_y: false,
333 min_value: None,
334 max_value: None,
335 current_value: None,
336 is_focus_scope: false,
337 is_focus_barrier: false,
338 drag_payload: None,
339 hero_tag: None,
340 focus_index: None, capture_tab: false, auto_indent: false,
341 }
342}