1use bevy::prelude::*;
4use bevy::picking::Pickable;
5use crate::styles::{colors, dimensions, ButtonStyle, ButtonSize};
6use super::types::{StyledButton, ButtonStateColors, SelectableButton, Selected, Active, ButtonSelectionColors, StateColorSet};
7use crate::systems::hover::{HoverScale, HoverBrightness, OriginalColors};
8use crate::relationships::InButtonGroup;
9
10pub struct ButtonBuilder {
12 text: String,
13 style: ButtonStyle,
14 size: ButtonSize,
15 width: Option<Val>,
16 custom_height: Option<Val>,
17 margin: Option<UiRect>,
18 hover_scale: Option<f32>,
19 hover_brightness: Option<f32>,
20 disabled: bool,
21 icon: Option<String>,
22 selectable: bool,
24 auto_toggle: bool,
25 is_selected: bool,
26 is_active: bool,
27 button_group: Option<Entity>,
28 custom_selection_colors: Option<(StateColorSet, StateColorSet)>, }
30
31impl ButtonBuilder {
32 pub fn new(text: impl Into<String>) -> Self {
34 Self {
35 text: text.into(),
36 style: ButtonStyle::Primary,
37 size: ButtonSize::Medium,
38 width: None,
39 custom_height: None,
40 margin: None,
41 hover_scale: None,
42 hover_brightness: None,
43 disabled: false,
44 icon: None,
45 selectable: false,
46 auto_toggle: true,
47 is_selected: false,
48 is_active: false,
49 button_group: None,
50 custom_selection_colors: None,
51 }
52 }
53
54 pub fn style(mut self, style: ButtonStyle) -> Self {
56 self.style = style;
57 self
58 }
59
60 pub fn size(mut self, size: ButtonSize) -> Self {
62 self.size = size;
63 self
64 }
65
66 pub fn width(mut self, width: Val) -> Self {
68 self.width = Some(width);
69 self
70 }
71
72 pub fn height(mut self, height: Val) -> Self {
74 self.custom_height = Some(height);
75 self
76 }
77
78 pub fn margin(mut self, margin: UiRect) -> Self {
80 self.margin = Some(margin);
81 self
82 }
83
84 pub fn hover_scale(mut self, scale: f32) -> Self {
86 self.hover_scale = Some(scale);
87 self
88 }
89
90 pub fn hover_brightness(mut self, brightness: f32) -> Self {
92 self.hover_brightness = Some(brightness);
93 self
94 }
95
96 pub fn disabled(mut self) -> Self {
98 self.disabled = true;
99 self
100 }
101
102 pub fn enabled(mut self, enabled: bool) -> Self {
104 self.disabled = !enabled;
105 self
106 }
107
108 pub fn icon(mut self, icon: impl Into<String>) -> Self {
110 self.icon = Some(icon.into());
111 self
112 }
113
114 pub fn selectable(mut self) -> Self {
116 self.selectable = true;
117 self
118 }
119
120 pub fn selected(mut self, selected: bool) -> Self {
122 self.selectable = true;
123 self.is_selected = selected;
124 self
125 }
126
127 pub fn active(mut self, active: bool) -> Self {
130 self.selectable = true;
131 self.is_active = active;
132 self
133 }
134
135 pub fn manual_toggle(mut self) -> Self {
137 self.auto_toggle = false;
138 self
139 }
140
141 pub fn in_group(mut self, group_entity: Entity) -> Self {
144 self.selectable = true;
145 self.button_group = Some(group_entity);
146 self
147 }
148
149 pub fn selection_colors(mut self, selected: StateColorSet, active: StateColorSet) -> Self {
152 self.custom_selection_colors = Some((selected, active));
153 self
154 }
155
156 pub fn with_marker<M: Component>(self, marker: M) -> ButtonBuilderWithMarker<M> {
158 ButtonBuilderWithMarker {
159 builder: self,
160 marker,
161 }
162 }
163
164 pub fn build_in(self, parent: &mut ChildSpawnerCommands) -> Entity {
166 self.build(parent)
167 }
168
169 pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
171 let (bg_color, border_color, text_color) = get_style_colors(&self.style, self.disabled);
172 let (width, height) = get_size_dimensions(&self.size);
173 let font_size = get_font_size(&self.size);
174
175 let button_width = self.width.unwrap_or(Val::Px(width));
176 let button_height = self.custom_height.unwrap_or(Val::Px(height));
177 let button_margin = self.margin.unwrap_or_default();
178
179 let mut button = parent.spawn((
180 Button,
181 Node {
182 width: button_width,
183 height: button_height,
184 margin: button_margin,
185 justify_content: JustifyContent::Center,
186 align_items: AlignItems::Center,
187 border: UiRect::all(Val::Px(dimensions::BORDER_WIDTH_MEDIUM)),
188 padding: UiRect::horizontal(Val::Px(dimensions::PADDING_MEDIUM)),
189 ..default()
190 },
191 BackgroundColor(bg_color),
192 BorderColor::all(border_color),
193 BorderRadius::all(Val::Px(dimensions::BORDER_RADIUS_MEDIUM)),
194 StyledButton,
195 Transform::default(), ));
197
198 button.insert(ButtonStateColors {
200 normal_bg: bg_color,
201 hover_bg: self.style.hover_color(),
202 pressed_bg: self.style.pressed_color(),
203 normal_border: border_color,
204 hover_border: self.style.border_color(),
205 pressed_border: self.style.border_color(),
206 });
207
208 button.insert(OriginalColors {
210 background: bg_color,
211 border: border_color,
212 });
213
214 let scale = self.hover_scale.unwrap_or(1.015); button.insert(HoverScale(scale));
217
218 button.insert(super::types::ButtonAnimationState {
220 current_scale: 1.0,
221 target_scale: 1.0,
222 current_color_blend: 0.0,
223 target_color_blend: 0.0,
224 animation_speed: 12.0, });
226
227 if let Some(brightness) = self.hover_brightness {
229 button.insert(HoverBrightness(brightness));
230 }
231
232 if self.disabled {
233 button.insert(Interaction::None);
234 }
235
236 if self.selectable {
238 button.insert(SelectableButton {
239 auto_toggle: self.auto_toggle,
240 });
241
242 let selection_colors = if let Some((selected, active)) = self.custom_selection_colors {
244 let mut base_colors = generate_selection_colors(&self.style);
245 base_colors.selected = selected;
246 base_colors.active = active;
247 base_colors
248 } else {
249 generate_selection_colors(&self.style)
250 };
251
252 button.insert(selection_colors);
253
254 if self.is_selected {
256 button.insert(Selected);
257 }
258 if self.is_active {
259 button.insert(Active);
260 }
261
262 if let Some(group_entity) = self.button_group {
264 button.insert(InButtonGroup(group_entity));
265 }
266 }
267
268 let button_entity = button.id();
269
270 button.with_children(|button| {
272 if let Some(icon) = self.icon {
273 button.spawn((
275 Node {
276 flex_direction: FlexDirection::Row,
277 align_items: AlignItems::Center,
278 column_gap: Val::Px(dimensions::SPACING_SMALL),
279 ..default()
280 },
281 BackgroundColor(Color::NONE),
282 Pickable::IGNORE, )).with_children(|container| {
284 container.spawn((
286 Text::new(icon),
287 TextFont {
288 font_size,
289 ..default()
290 },
291 TextColor(text_color),
292 Pickable::IGNORE, ));
294
295 container.spawn((
297 Text::new(&self.text),
298 TextFont {
299 font_size,
300 ..default()
301 },
302 TextColor(text_color),
303 Pickable::IGNORE, ));
305 });
306 } else {
307 button.spawn((
309 Text::new(&self.text),
310 TextFont {
311 font_size,
312 ..default()
313 },
314 TextColor(text_color),
315 Pickable::IGNORE, ));
317 }
318 });
319
320 button_entity
321 }
322}
323
324fn get_style_colors(style: &ButtonStyle, disabled: bool) -> (Color, Color, Color) {
326 if disabled {
327 return (
328 colors::BACKGROUND_TERTIARY,
329 colors::BORDER_DEFAULT,
330 colors::TEXT_DISABLED,
331 );
332 }
333
334 match style {
335 ButtonStyle::Primary => (colors::PRIMARY, colors::PRIMARY_DARK, colors::TEXT_ON_PRIMARY),
336 ButtonStyle::Secondary => (colors::SECONDARY, colors::SECONDARY_DARK, colors::TEXT_ON_SECONDARY),
337 ButtonStyle::Success => (colors::SUCCESS, colors::SUCCESS_DARK, colors::TEXT_ON_SUCCESS),
338 ButtonStyle::Danger => (colors::DANGER, colors::DANGER_DARK, colors::TEXT_ON_DANGER),
339 ButtonStyle::Warning => (colors::WARNING, colors::WARNING_PRESSED, Color::BLACK),
340 ButtonStyle::Ghost => (Color::NONE, colors::BORDER_DEFAULT, colors::TEXT_PRIMARY),
341 }
342}
343
344fn get_size_dimensions(size: &ButtonSize) -> (f32, f32) {
346 match size {
347 ButtonSize::Small => (dimensions::BUTTON_WIDTH_SMALL, dimensions::BUTTON_HEIGHT_SMALL),
348 ButtonSize::Medium => (dimensions::BUTTON_WIDTH_MEDIUM, dimensions::BUTTON_HEIGHT_MEDIUM),
349 ButtonSize::Large => (dimensions::BUTTON_WIDTH_LARGE, dimensions::BUTTON_HEIGHT_LARGE),
350 ButtonSize::XLarge => (dimensions::BUTTON_WIDTH_XLARGE, dimensions::BUTTON_HEIGHT_XLARGE),
351 }
352}
353
354fn get_font_size(size: &ButtonSize) -> f32 {
356 match size {
357 ButtonSize::Small => dimensions::FONT_SIZE_SMALL,
358 ButtonSize::Medium => dimensions::FONT_SIZE_MEDIUM,
359 ButtonSize::Large => dimensions::FONT_SIZE_LARGE,
360 ButtonSize::XLarge => dimensions::FONT_SIZE_XLARGE,
361 }
362}
363
364fn generate_selection_colors(style: &ButtonStyle) -> ButtonSelectionColors {
368 let (style_bg, style_border, _text_color) = get_style_colors(style, false);
369
370 let normal = StateColorSet::new(
373 colors::SECONDARY, colors::SECONDARY_HOVER, colors::SECONDARY_PRESSED, colors::BORDER_DEFAULT,
377 colors::BORDER_DEFAULT,
378 colors::BORDER_DEFAULT,
379 );
380
381 let selected = StateColorSet::new(
383 style_bg, style.hover_color(), style.pressed_color(), style_border,
387 style_border,
388 style_border,
389 );
390
391 let active_bg = adjust_color_for_selection(style_bg, 0.85, 1.15);
393 let active_border = adjust_color_for_selection(style_border, 0.85, 1.1);
394 let active = StateColorSet::new(
395 active_bg,
396 adjust_color_for_selection(active_bg, 1.1, 1.0), adjust_color_for_selection(active_bg, 0.9, 1.0), active_border,
399 active_border,
400 active_border,
401 );
402
403 ButtonSelectionColors {
404 normal,
405 selected,
406 active,
407 }
408}
409
410fn adjust_color_for_selection(color: Color, brightness: f32, saturation: f32) -> Color {
412 let linear = color.to_linear();
413
414 let max = linear.red.max(linear.green).max(linear.blue);
416 let min = linear.red.min(linear.green).min(linear.blue);
417 let delta = max - min;
418
419 if delta < 0.00001 {
420 return Color::LinearRgba(LinearRgba {
422 red: (linear.red * brightness).min(1.0),
423 green: (linear.green * brightness).min(1.0),
424 blue: (linear.blue * brightness).min(1.0),
425 alpha: linear.alpha,
426 });
427 }
428
429 let avg = (linear.red + linear.green + linear.blue) / 3.0;
431
432 Color::LinearRgba(LinearRgba {
433 red: ((linear.red - avg) * saturation + avg) * brightness,
434 green: ((linear.green - avg) * saturation + avg) * brightness,
435 blue: ((linear.blue - avg) * saturation + avg) * brightness,
436 alpha: linear.alpha,
437 })
438}
439
440pub fn primary_button(text: impl Into<String>) -> ButtonBuilder {
442 ButtonBuilder::new(text).style(ButtonStyle::Primary)
443}
444
445pub fn secondary_button(text: impl Into<String>) -> ButtonBuilder {
447 ButtonBuilder::new(text).style(ButtonStyle::Secondary)
448}
449
450pub fn success_button(text: impl Into<String>) -> ButtonBuilder {
452 ButtonBuilder::new(text).style(ButtonStyle::Success)
453}
454
455pub fn danger_button(text: impl Into<String>) -> ButtonBuilder {
457 ButtonBuilder::new(text).style(ButtonStyle::Danger)
458}
459
460pub fn ghost_button(text: impl Into<String>) -> ButtonBuilder {
462 ButtonBuilder::new(text).style(ButtonStyle::Ghost)
463}
464
465pub struct ButtonBuilderWithMarker<M: Component> {
467 builder: ButtonBuilder,
468 marker: M,
469}
470
471impl<M: Component> ButtonBuilderWithMarker<M> {
472 pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
474 let entity = self.builder.build(parent);
475 parent.commands().entity(entity).insert(self.marker);
476 entity
477 }
478
479 pub fn build_in(self, parent: &mut ChildSpawnerCommands) -> Entity {
481 self.build(parent)
482 }
483}