bevy_ui_builders/button/
builder.rs

1//! ButtonBuilder implementation
2
3use bevy::prelude::*;
4use bevy::picking::Pickable;
5use crate::styles::{colors, dimensions, ButtonStyle, ButtonSize};
6use super::types::{StyledButton, ButtonStateColors};
7use crate::systems::hover::{HoverScale, HoverBrightness, OriginalColors};
8
9/// Builder for creating buttons with consistent styling
10pub struct ButtonBuilder {
11    text: String,
12    style: ButtonStyle,
13    size: ButtonSize,
14    width: Option<Val>,
15    custom_height: Option<Val>,
16    margin: Option<UiRect>,
17    hover_scale: Option<f32>,
18    hover_brightness: Option<f32>,
19    disabled: bool,
20    icon: Option<String>,
21}
22
23impl ButtonBuilder {
24    /// Create a new button builder with text
25    pub fn new(text: impl Into<String>) -> Self {
26        Self {
27            text: text.into(),
28            style: ButtonStyle::Primary,
29            size: ButtonSize::Medium,
30            width: None,
31            custom_height: None,
32            margin: None,
33            hover_scale: None,
34            hover_brightness: None,
35            disabled: false,
36            icon: None,
37        }
38    }
39
40    /// Set the button style
41    pub fn style(mut self, style: ButtonStyle) -> Self {
42        self.style = style;
43        self
44    }
45
46    /// Set the button size
47    pub fn size(mut self, size: ButtonSize) -> Self {
48        self.size = size;
49        self
50    }
51
52    /// Set a custom width
53    pub fn width(mut self, width: Val) -> Self {
54        self.width = Some(width);
55        self
56    }
57
58    /// Set a custom height
59    pub fn height(mut self, height: Val) -> Self {
60        self.custom_height = Some(height);
61        self
62    }
63
64    /// Set the margin
65    pub fn margin(mut self, margin: UiRect) -> Self {
66        self.margin = Some(margin);
67        self
68    }
69
70    /// Enable hover scale effect
71    pub fn hover_scale(mut self, scale: f32) -> Self {
72        self.hover_scale = Some(scale);
73        self
74    }
75
76    /// Enable hover brightness effect
77    pub fn hover_brightness(mut self, brightness: f32) -> Self {
78        self.hover_brightness = Some(brightness);
79        self
80    }
81
82    /// Set the button as disabled
83    pub fn disabled(mut self) -> Self {
84        self.disabled = true;
85        self
86    }
87
88    /// Set whether the button is enabled
89    pub fn enabled(mut self, enabled: bool) -> Self {
90        self.disabled = !enabled;
91        self
92    }
93
94    /// Add an icon (emoji or symbol)
95    pub fn icon(mut self, icon: impl Into<String>) -> Self {
96        self.icon = Some(icon.into());
97        self
98    }
99
100    /// Attach a marker component to the button
101    pub fn with_marker<M: Component>(self, marker: M) -> ButtonBuilderWithMarker<M> {
102        ButtonBuilderWithMarker {
103            builder: self,
104            marker,
105        }
106    }
107
108    /// Build the button entity (alias for build)
109    pub fn build_in(self, parent: &mut ChildSpawnerCommands) -> Entity {
110        self.build(parent)
111    }
112
113    /// Build the button entity
114    pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
115        let (bg_color, border_color, text_color) = get_style_colors(&self.style, self.disabled);
116        let (width, height) = get_size_dimensions(&self.size);
117        let font_size = get_font_size(&self.size);
118
119        let button_width = self.width.unwrap_or(Val::Px(width));
120        let button_height = self.custom_height.unwrap_or(Val::Px(height));
121        let button_margin = self.margin.unwrap_or_default();
122
123        let mut button = parent.spawn((
124            Button,
125            Node {
126                width: button_width,
127                height: button_height,
128                margin: button_margin,
129                justify_content: JustifyContent::Center,
130                align_items: AlignItems::Center,
131                border: UiRect::all(Val::Px(dimensions::BORDER_WIDTH_MEDIUM)),
132                padding: UiRect::horizontal(Val::Px(dimensions::PADDING_MEDIUM)),
133                ..default()
134            },
135            BackgroundColor(bg_color),
136            BorderColor::all(border_color),
137            BorderRadius::all(Val::Px(dimensions::BORDER_RADIUS_MEDIUM)),
138            StyledButton,
139            Transform::default(), // Required for scale animations
140        ));
141
142        // Store state colors for automatic hover effects
143        button.insert(ButtonStateColors {
144            normal_bg: bg_color,
145            hover_bg: self.style.hover_color(),
146            pressed_bg: self.style.pressed_color(),
147            normal_border: border_color,
148            hover_border: self.style.border_color(),
149            pressed_border: self.style.border_color(),
150        });
151
152        // Store original colors for custom hover effects
153        button.insert(OriginalColors {
154            background: bg_color,
155            border: border_color,
156        });
157
158        // Add hover scale - use default if not specified
159        let scale = self.hover_scale.unwrap_or(1.015); // Even more subtle default scale
160        button.insert(HoverScale(scale));
161
162        // Add animation state for smooth transitions
163        button.insert(super::types::ButtonAnimationState {
164            current_scale: 1.0,
165            target_scale: 1.0,
166            current_color_blend: 0.0,
167            target_color_blend: 0.0,
168            animation_speed: 12.0, // Smooth but responsive
169        });
170
171        // Add hover brightness if specified (optional)
172        if let Some(brightness) = self.hover_brightness {
173            button.insert(HoverBrightness(brightness));
174        }
175
176        if self.disabled {
177            button.insert(Interaction::None);
178        }
179
180        let button_entity = button.id();
181
182        // Add text content
183        button.with_children(|button| {
184            if let Some(icon) = self.icon {
185                // Icon + Text layout
186                button.spawn((
187                    Node {
188                        flex_direction: FlexDirection::Row,
189                        align_items: AlignItems::Center,
190                        column_gap: Val::Px(dimensions::SPACING_SMALL),
191                        ..default()
192                    },
193                    BackgroundColor(Color::NONE),
194                    Pickable::IGNORE, // Don't block button interaction
195                )).with_children(|container| {
196                    // Icon
197                    container.spawn((
198                        Text::new(icon),
199                        TextFont {
200                            font_size,
201                            ..default()
202                        },
203                        TextColor(text_color),
204                        Pickable::IGNORE, // Don't block button interaction
205                    ));
206
207                    // Text
208                    container.spawn((
209                        Text::new(&self.text),
210                        TextFont {
211                            font_size,
212                            ..default()
213                        },
214                        TextColor(text_color),
215                        Pickable::IGNORE, // Don't block button interaction
216                    ));
217                });
218            } else {
219                // Just text
220                button.spawn((
221                    Text::new(&self.text),
222                    TextFont {
223                        font_size,
224                        ..default()
225                    },
226                    TextColor(text_color),
227                    Pickable::IGNORE, // Don't block button interaction
228                ));
229            }
230        });
231
232        button_entity
233    }
234}
235
236/// Get colors for a button style
237fn get_style_colors(style: &ButtonStyle, disabled: bool) -> (Color, Color, Color) {
238    if disabled {
239        return (
240            colors::BACKGROUND_TERTIARY,
241            colors::BORDER_DEFAULT,
242            colors::TEXT_DISABLED,
243        );
244    }
245
246    match style {
247        ButtonStyle::Primary => (colors::PRIMARY, colors::PRIMARY_DARK, colors::TEXT_ON_PRIMARY),
248        ButtonStyle::Secondary => (colors::SECONDARY, colors::SECONDARY_DARK, colors::TEXT_ON_SECONDARY),
249        ButtonStyle::Success => (colors::SUCCESS, colors::SUCCESS_DARK, colors::TEXT_ON_SUCCESS),
250        ButtonStyle::Danger => (colors::DANGER, colors::DANGER_DARK, colors::TEXT_ON_DANGER),
251        ButtonStyle::Warning => (colors::WARNING, colors::WARNING_PRESSED, Color::BLACK),
252        ButtonStyle::Ghost => (Color::NONE, colors::BORDER_DEFAULT, colors::TEXT_PRIMARY),
253    }
254}
255
256/// Get dimensions for a button size
257fn get_size_dimensions(size: &ButtonSize) -> (f32, f32) {
258    match size {
259        ButtonSize::Small => (dimensions::BUTTON_WIDTH_SMALL, dimensions::BUTTON_HEIGHT_SMALL),
260        ButtonSize::Medium => (dimensions::BUTTON_WIDTH_MEDIUM, dimensions::BUTTON_HEIGHT_MEDIUM),
261        ButtonSize::Large => (dimensions::BUTTON_WIDTH_LARGE, dimensions::BUTTON_HEIGHT_LARGE),
262        ButtonSize::XLarge => (dimensions::BUTTON_WIDTH_XLARGE, dimensions::BUTTON_HEIGHT_XLARGE),
263    }
264}
265
266/// Get font size for a button size
267fn get_font_size(size: &ButtonSize) -> f32 {
268    match size {
269        ButtonSize::Small => dimensions::FONT_SIZE_SMALL,
270        ButtonSize::Medium => dimensions::FONT_SIZE_MEDIUM,
271        ButtonSize::Large => dimensions::FONT_SIZE_LARGE,
272        ButtonSize::XLarge => dimensions::FONT_SIZE_XLARGE,
273    }
274}
275
276/// Convenience function for creating a primary button
277pub fn primary_button(text: impl Into<String>) -> ButtonBuilder {
278    ButtonBuilder::new(text).style(ButtonStyle::Primary)
279}
280
281/// Convenience function for creating a secondary button
282pub fn secondary_button(text: impl Into<String>) -> ButtonBuilder {
283    ButtonBuilder::new(text).style(ButtonStyle::Secondary)
284}
285
286/// Convenience function for creating a success button
287pub fn success_button(text: impl Into<String>) -> ButtonBuilder {
288    ButtonBuilder::new(text).style(ButtonStyle::Success)
289}
290
291/// Convenience function for creating a danger button
292pub fn danger_button(text: impl Into<String>) -> ButtonBuilder {
293    ButtonBuilder::new(text).style(ButtonStyle::Danger)
294}
295
296/// Convenience function for creating a ghost button
297pub fn ghost_button(text: impl Into<String>) -> ButtonBuilder {
298    ButtonBuilder::new(text).style(ButtonStyle::Ghost)
299}
300
301/// A ButtonBuilder with an attached marker component
302pub struct ButtonBuilderWithMarker<M: Component> {
303    builder: ButtonBuilder,
304    marker: M,
305}
306
307impl<M: Component> ButtonBuilderWithMarker<M> {
308    /// Build the button with the marker component
309    pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
310        let entity = self.builder.build(parent);
311        parent.commands().entity(entity).insert(self.marker);
312        entity
313    }
314
315    /// Build the button with the marker component (alias for build)
316    pub fn build_in(self, parent: &mut ChildSpawnerCommands) -> Entity {
317        self.build(parent)
318    }
319}