bevy_ui_builders/dialog/
builder.rs

1//! DialogBuilder implementation
2
3use bevy::prelude::*;
4use crate::button::{ButtonBuilder, ButtonSize};
5use crate::styles::{colors, dimensions, ButtonStyle};
6use crate::relationships::BelongsToDialog;
7use super::types::*;
8use std::collections::HashMap;
9use std::cell::RefCell;
10use std::rc::Rc;
11
12/// Builder for creating dialogs
13pub struct DialogBuilder {
14    title: String,
15    body: String,
16    dialog_type: DialogType,
17    width: Val,
18    min_width: Val,
19    max_width: Val,
20    height: Val,
21    min_height: Val,
22    max_height: Val,
23    buttons: Vec<DialogButton>,
24    dismissible: bool,
25    z_index: i32,
26}
27
28impl DialogBuilder {
29    /// Create a new dialog builder
30    pub fn new(dialog_type: DialogType) -> Self {
31        Self {
32            title: String::new(),
33            body: String::new(),
34            dialog_type,
35            width: Val::Px(dimensions::DIALOG_WIDTH_MEDIUM),
36            min_width: Val::Auto,
37            max_width: Val::Auto,
38            height: Val::Auto,
39            min_height: Val::Auto,
40            max_height: Val::Auto,
41            buttons: Vec::new(),
42            dismissible: true,
43            z_index: dimensions::Z_INDEX_MODAL,
44        }
45    }
46
47    /// Set the dialog title
48    pub fn title(mut self, title: impl Into<String>) -> Self {
49        self.title = title.into();
50        self
51    }
52
53    /// Set the dialog body text
54    pub fn body(mut self, body: impl Into<String>) -> Self {
55        self.body = body.into();
56        self
57    }
58
59    /// Set the dialog width
60    pub fn width(mut self, width: Val) -> Self {
61        self.width = width;
62        self
63    }
64
65    /// Set minimum width
66    pub fn min_width(mut self, min_width: Val) -> Self {
67        self.min_width = min_width;
68        self
69    }
70
71    /// Set maximum width
72    pub fn max_width(mut self, max_width: Val) -> Self {
73        self.max_width = max_width;
74        self
75    }
76
77    /// Set the dialog height
78    pub fn height(mut self, height: Val) -> Self {
79        self.height = height;
80        self
81    }
82
83    /// Set minimum height
84    pub fn min_height(mut self, min_height: Val) -> Self {
85        self.min_height = min_height;
86        self
87    }
88
89    /// Set maximum height
90    pub fn max_height(mut self, max_height: Val) -> Self {
91        self.max_height = max_height;
92        self
93    }
94
95    /// Set whether the dialog can be dismissed by clicking outside
96    pub fn dismissible(mut self, dismissible: bool) -> Self {
97        self.dismissible = dismissible;
98        self
99    }
100
101    /// Set the z-index for layering
102    pub fn z_index(mut self, z_index: i32) -> Self {
103        self.z_index = z_index;
104        self
105    }
106
107    /// Add a confirm button
108    pub fn confirm_button(mut self, text: impl Into<String>) -> Self {
109        self.buttons.push(DialogButton {
110            text: text.into(),
111            style: ButtonStyle::Primary,
112            marker: DialogButtonMarker::Confirm,
113        });
114        self
115    }
116
117    /// Add a cancel button
118    pub fn cancel_button(mut self, text: impl Into<String>) -> Self {
119        self.buttons.push(DialogButton {
120            text: text.into(),
121            style: ButtonStyle::Secondary,
122            marker: DialogButtonMarker::Cancel,
123        });
124        self
125    }
126
127    /// Add a danger button
128    pub fn danger_button(mut self, text: impl Into<String>) -> Self {
129        self.buttons.push(DialogButton {
130            text: text.into(),
131            style: ButtonStyle::Danger,
132            marker: DialogButtonMarker::Confirm,
133        });
134        self
135    }
136
137    /// Add a save button
138    pub fn save_button(mut self, text: impl Into<String>) -> Self {
139        self.buttons.push(DialogButton {
140            text: text.into(),
141            style: ButtonStyle::Success,
142            marker: DialogButtonMarker::Save,
143        });
144        self
145    }
146
147    /// Add a discard button
148    pub fn discard_button(mut self, text: impl Into<String>) -> Self {
149        self.buttons.push(DialogButton {
150            text: text.into(),
151            style: ButtonStyle::Warning,
152            marker: DialogButtonMarker::Discard,
153        });
154        self
155    }
156
157    /// Add an OK button
158    pub fn ok_button(mut self) -> Self {
159        self.buttons.push(DialogButton {
160            text: "OK".to_string(),
161            style: ButtonStyle::Primary,
162            marker: DialogButtonMarker::Ok,
163        });
164        self
165    }
166
167    /// Add Yes/No buttons
168    pub fn yes_no_buttons(mut self) -> Self {
169        self.buttons.push(DialogButton {
170            text: "Yes".to_string(),
171            style: ButtonStyle::Primary,
172            marker: DialogButtonMarker::Yes,
173        });
174        self.buttons.push(DialogButton {
175            text: "No".to_string(),
176            style: ButtonStyle::Secondary,
177            marker: DialogButtonMarker::No,
178        });
179        self
180    }
181
182    /// Add a custom button
183    pub fn custom_button(
184        mut self,
185        text: impl Into<String>,
186        style: ButtonStyle,
187        marker: DialogButtonMarker,
188    ) -> Self {
189        self.buttons.push(DialogButton {
190            text: text.into(),
191            style,
192            marker,
193        });
194        self
195    }
196
197    /// Helper method to build a dialog and add a custom marker to a specific button
198    ///
199    /// # Example
200    /// ```ignore
201    /// DialogBuilder::new(DialogType::Custom)
202    ///     .title("Delete Item")
203    ///     .danger_button("Delete")
204    ///     .cancel_button("Cancel")
205    ///     .build_and_mark(commands, DialogButtonMarker::Confirm, MyCustomMarker);
206    /// ```
207    pub fn build_and_mark<M: Component>(
208        self,
209        commands: &mut Commands,
210        button_marker: DialogButtonMarker,
211        component: M,
212    ) -> Entity {
213        let (dialog, buttons) = self.build_with_buttons(commands);
214        if let Some(button_entity) = buttons.get(&button_marker) {
215            commands.entity(*button_entity).insert(component);
216        }
217        dialog
218    }
219
220
221    /// Build the dialog entity and return button entities
222    ///
223    /// Returns a tuple of (dialog_entity, button_entities) where button_entities
224    /// is a HashMap mapping DialogButtonMarker to Entity for each button created.
225    ///
226    /// # Example
227    /// ```ignore
228    /// let (dialog, buttons) = DialogBuilder::new(DialogType::Custom)
229    ///     .title("Confirm Action")
230    ///     .danger_button("Delete")
231    ///     .cancel_button("Cancel")
232    ///     .build_with_buttons(commands);
233    ///
234    /// // Add custom components to buttons
235    /// if let Some(confirm_btn) = buttons.get(&DialogButtonMarker::Confirm) {
236    ///     commands.entity(*confirm_btn).insert(MyCustomMarker);
237    /// }
238    /// ```
239    pub fn build_with_buttons(self, commands: &mut Commands) -> (Entity, HashMap<DialogButtonMarker, Entity>) {
240        self.build_internal(commands, true)
241    }
242
243    /// Build the dialog entity
244    pub fn build(self, commands: &mut Commands) -> Entity {
245        let (entity, _) = self.build_internal(commands, false);
246        entity
247    }
248
249    /// Internal build implementation
250    fn build_internal(self, commands: &mut Commands, return_buttons: bool) -> (Entity, HashMap<DialogButtonMarker, Entity>) {
251        // Create overlay that blocks clicks
252        let overlay_entity = commands
253            .spawn((
254                Button, // Block clicks to elements behind
255                Node {
256                    position_type: PositionType::Absolute,
257                    width: Val::Percent(100.0),
258                    height: Val::Percent(100.0),
259                    justify_content: JustifyContent::Center,
260                    align_items: AlignItems::Center,
261                    ..default()
262                },
263                BackgroundColor(colors::OVERLAY_BACKDROP),
264                DialogOverlay {
265                    dialog_type: self.dialog_type,
266                    dismissible: self.dismissible,
267                },
268                ZIndex(self.z_index),
269            ))
270            .id();
271
272        // Add type-specific marker
273        match self.dialog_type {
274            DialogType::ExitConfirmation => {
275                commands.entity(overlay_entity).insert(ExitConfirmationDialog);
276            }
277            DialogType::UnsavedChanges => {
278                commands.entity(overlay_entity).insert(UnsavedChangesDialog);
279            }
280            DialogType::Resolution => {
281                commands.entity(overlay_entity).insert(ResolutionDialog);
282            }
283            DialogType::Error => {
284                commands.entity(overlay_entity).insert(ErrorDialog);
285            }
286            DialogType::Info => {
287                commands.entity(overlay_entity).insert(InfoDialog);
288            }
289            DialogType::Warning => {
290                commands.entity(overlay_entity).insert(WarningDialog);
291            }
292            DialogType::Success => {
293                commands.entity(overlay_entity).insert(SuccessDialog);
294            }
295            DialogType::Custom => {}
296        }
297
298        // Track button entities if needed (use RefCell for interior mutability)
299        let button_entities = Rc::new(RefCell::new(HashMap::new()));
300        let button_entities_clone = button_entities.clone();
301
302        // Create container with relationship to overlay
303        let container_entity = commands
304            .spawn((
305                Node {
306                    width: self.width,
307                    height: self.height,
308                    min_width: self.min_width,
309                    min_height: self.min_height,
310                    max_width: self.max_width,
311                    max_height: self.max_height,
312                    padding: UiRect::all(Val::Px(dimensions::PADDING_LARGE)),
313                    flex_direction: FlexDirection::Column,
314                    align_items: AlignItems::Center,
315                    border: UiRect::all(Val::Px(dimensions::BORDER_WIDTH_THIN)),
316                    ..default()
317                },
318                BackgroundColor(colors::BACKGROUND_SECONDARY),
319                BorderColor(colors::BORDER_DEFAULT),
320                BorderRadius::all(Val::Px(dimensions::BORDER_RADIUS_LARGE)),
321                DialogContainer {
322                    dialog_type: self.dialog_type,
323                },
324                ZIndex(self.z_index + 50),
325                BelongsToDialog(overlay_entity),  // Relationship to dialog overlay
326            ))
327            .id();
328
329        commands.entity(container_entity).with_children(|parent| {
330            // Title
331            if !self.title.is_empty() {
332                parent
333                    .spawn((
334                        Node {
335                            width: Val::Percent(100.0),
336                            margin: UiRect::bottom(Val::Px(dimensions::SPACING_LARGE)),
337                            justify_content: JustifyContent::Center,
338                            align_items: AlignItems::Center,
339                            ..default()
340                        },
341                        BackgroundColor(Color::NONE),
342                    ))
343                    .with_children(|title_parent| {
344                        title_parent.spawn((
345                            Text::new(self.title.clone()),
346                            TextFont {
347                                font_size: dimensions::FONT_SIZE_HEADING,
348                                ..default()
349                            },
350                            TextColor(colors::TEXT_PRIMARY),
351                            DialogTitle,
352                        ));
353                    });
354            }
355
356            // Body
357            if !self.body.is_empty() {
358                parent
359                    .spawn((
360                        Node {
361                            width: Val::Percent(100.0),
362                            margin: UiRect::bottom(Val::Px(dimensions::SPACING_LARGE)),
363                            justify_content: JustifyContent::Center,
364                            align_items: AlignItems::Center,
365                            ..default()
366                        },
367                        BackgroundColor(Color::NONE),
368                    ))
369                    .with_children(|body_parent| {
370                        body_parent.spawn((
371                            Text::new(self.body.clone()),
372                            TextFont {
373                                font_size: dimensions::FONT_SIZE_MEDIUM,
374                                ..default()
375                            },
376                            TextColor(colors::TEXT_SECONDARY),
377                            DialogBody,
378                        ));
379                    });
380            }
381
382            // Buttons
383            if !self.buttons.is_empty() {
384                parent
385                    .spawn((
386                        Node {
387                            width: Val::Percent(100.0),
388                            flex_direction: FlexDirection::Row,
389                            justify_content: JustifyContent::Center,
390                            column_gap: Val::Px(dimensions::SPACING_MEDIUM),
391                            ..default()
392                        },
393                        BackgroundColor(Color::NONE),
394                        DialogButtonRow,
395                    ))
396                    .with_children(|button_row| {
397                        for button in self.buttons {
398                            let button_entity = ButtonBuilder::new(button.text)
399                                .style(button.style)
400                                .size(ButtonSize::Medium)
401                                .build(button_row);
402
403                            // Track button entity if needed
404                            if return_buttons {
405                                button_entities_clone.borrow_mut().insert(button.marker.clone(), button_entity);
406                            }
407
408                            // Add standard marker based on type
409                            match &button.marker {
410                                DialogButtonMarker::Confirm => {
411                                    button_row.commands().entity(button_entity).insert(ConfirmButton);
412                                }
413                                DialogButtonMarker::Cancel => {
414                                    button_row.commands().entity(button_entity).insert(CancelButton);
415                                }
416                                DialogButtonMarker::Save => {
417                                    button_row.commands().entity(button_entity).insert(SaveButton);
418                                }
419                                DialogButtonMarker::Discard => {
420                                    button_row.commands().entity(button_entity).insert(DiscardButton);
421                                }
422                                DialogButtonMarker::Ok => {
423                                    button_row.commands().entity(button_entity).insert(OkButton);
424                                }
425                                DialogButtonMarker::Yes => {
426                                    button_row.commands().entity(button_entity).insert(YesButton);
427                                }
428                                DialogButtonMarker::No => {
429                                    button_row.commands().entity(button_entity).insert(NoButton);
430                                }
431                                DialogButtonMarker::Custom(_) => {
432                                    // Custom markers can be added by the caller using build_with_buttons()
433                                }
434                            }
435                        }
436                    });
437            }
438        });
439
440        // Set up parent-child relationship for visual hierarchy
441        // The BelongsToDialog relationship handles logical grouping and cleanup
442        commands.entity(overlay_entity).add_child(container_entity);
443
444        // Extract the HashMap from RefCell
445        let final_button_entities = Rc::try_unwrap(button_entities)
446            .map(|refcell| refcell.into_inner())
447            .unwrap_or_else(|rc| rc.borrow().clone());
448
449        (overlay_entity, final_button_entities)
450    }
451}
452
453/// Convenience functions for common dialogs
454pub mod presets {
455    use super::*;
456
457    /// Create an exit confirmation dialog
458    pub fn exit_confirmation(commands: &mut Commands) -> Entity {
459        DialogBuilder::new(DialogType::ExitConfirmation)
460            .title("Exit Application")
461            .body("Are you sure you want to exit?")
462            .danger_button("Exit")
463            .cancel_button("Cancel")
464            .build(commands)
465    }
466
467    /// Create an unsaved changes dialog
468    pub fn unsaved_changes(commands: &mut Commands) -> Entity {
469        DialogBuilder::new(DialogType::UnsavedChanges)
470            .title("Unsaved Changes")
471            .body("You have unsaved changes. What would you like to do?")
472            .save_button("Save")
473            .discard_button("Discard")
474            .cancel_button("Cancel")
475            .build(commands)
476    }
477
478    /// Create an error dialog
479    pub fn error(commands: &mut Commands, message: impl Into<String>) -> Entity {
480        DialogBuilder::new(DialogType::Error)
481            .title("Error")
482            .body(message)
483            .ok_button()
484            .build(commands)
485    }
486
487    /// Create an info dialog
488    pub fn info(commands: &mut Commands, title: impl Into<String>, message: impl Into<String>) -> Entity {
489        DialogBuilder::new(DialogType::Info)
490            .title(title)
491            .body(message)
492            .ok_button()
493            .build(commands)
494    }
495
496    /// Create a warning dialog
497    pub fn warning(commands: &mut Commands, message: impl Into<String>) -> Entity {
498        DialogBuilder::new(DialogType::Warning)
499            .title("Warning")
500            .body(message)
501            .ok_button()
502            .build(commands)
503    }
504
505    /// Create a success dialog
506    pub fn success(commands: &mut Commands, message: impl Into<String>) -> Entity {
507        DialogBuilder::new(DialogType::Success)
508            .title("Success")
509            .body(message)
510            .ok_button()
511            .build(commands)
512    }
513
514    /// Create a confirmation dialog
515    pub fn confirm(commands: &mut Commands, title: impl Into<String>, message: impl Into<String>) -> Entity {
516        DialogBuilder::new(DialogType::Custom)
517            .title(title)
518            .body(message)
519            .yes_no_buttons()
520            .build(commands)
521    }
522}