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