bevy_ui_builders/button/
builder.rs

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