use bevy::ecs::system::ParamSet;
use bevy::picking::Pickable;
use bevy::prelude::*;
use bevy::ui::BoxShadow;
use bevy::ui::FocusPolicy;
use crate::{
elevation::Elevation,
i18n::LocalizedText,
telemetry::{InsertTestIdIfExists, TelemetryConfig, TestId},
theme::MaterialTheme,
tokens::{CornerRadius, Spacing},
};
pub struct DialogPlugin;
impl Plugin for DialogPlugin {
fn build(&self, app: &mut App) {
if !app.is_plugin_added::<crate::MaterialUiCorePlugin>() {
app.add_plugins(crate::MaterialUiCorePlugin);
}
app.add_message::<DialogOpenEvent>()
.add_message::<DialogCloseEvent>()
.add_message::<DialogConfirmEvent>()
.init_resource::<DialogSpawnCounter>()
.init_resource::<DialogOpenStack>()
.add_systems(Startup, setup_dialog_overlay)
.add_systems(
Update,
(
dialog_promote_to_overlay_system,
dialog_bring_to_front_on_open_system,
dialog_layer_z_index_system,
dialog_layer_visibility_system,
dialog_position_system,
dialog_mark_just_opened_system,
dialog_dismiss_on_scrim_click_system,
dialog_clear_just_opened_system,
dialog_dismiss_on_escape_system,
dialog_visibility_system,
dialog_scrim_visibility_system,
dialog_pickable_system,
dialog_scrim_pickable_system,
dialog_shadow_system,
dialog_telemetry_system,
dialog_scrim_telemetry_system,
),
);
}
}
#[derive(Component)]
struct DialogOverlay;
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
struct DialogLayerRootFor(Entity);
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
pub struct MaterialDialogAnchor(pub Entity);
#[derive(Component, Debug, Clone, Copy, PartialEq)]
pub enum MaterialDialogPlacement {
CenterInAnchor,
CenterInViewport,
BelowAnchor {
gap_px: f32,
},
AboveAnchor {
gap_px: f32,
},
RightOfAnchor {
gap_px: f32,
},
LeftOfAnchor {
gap_px: f32,
},
}
impl Default for MaterialDialogPlacement {
fn default() -> Self {
Self::CenterInAnchor
}
}
impl MaterialDialogPlacement {
pub fn center_in_viewport() -> Self {
Self::CenterInViewport
}
pub fn below_anchor(gap_px: f32) -> Self {
Self::BelowAnchor { gap_px }
}
pub fn above_anchor(gap_px: f32) -> Self {
Self::AboveAnchor { gap_px }
}
pub fn right_of_anchor(gap_px: f32) -> Self {
Self::RightOfAnchor { gap_px }
}
pub fn left_of_anchor(gap_px: f32) -> Self {
Self::LeftOfAnchor { gap_px }
}
}
#[derive(Resource, Default)]
struct DialogSpawnCounter(u64);
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct DialogSpawnOrder(u64);
#[derive(Component)]
struct DialogJustOpened;
#[derive(Resource, Default)]
struct DialogOpenStack {
ordered: Vec<Entity>,
}
fn setup_dialog_overlay(mut commands: Commands) {
commands.spawn((
DialogOverlay,
Node {
position_type: PositionType::Absolute,
width: Val::Percent(100.0),
height: Val::Percent(100.0),
top: Val::Px(0.0),
left: Val::Px(0.0),
..default()
},
GlobalZIndex(9000),
Pickable::IGNORE,
));
}
fn dialog_promote_to_overlay_system(
mut commands: Commands,
theme: Res<MaterialTheme>,
overlay_query: Query<Entity, With<DialogOverlay>>,
mut spawn_counter: ResMut<DialogSpawnCounter>,
added_dialogs: Query<(Entity, Option<&ChildOf>, &MaterialDialog), Added<MaterialDialog>>,
existing_scrims: Query<(Entity, &DialogScrimFor), With<DialogScrim>>,
existing_anchors: Query<&MaterialDialogAnchor>,
existing_placements: Query<&MaterialDialogPlacement>,
) {
let Some(overlay) = overlay_query.iter().next() else {
return;
};
for (dialog_entity, parent, dialog) in added_dialogs.iter() {
let anchor = existing_anchors
.get(dialog_entity)
.map(|a| a.0)
.unwrap_or_else(|_| parent.map(|p| p.parent()).unwrap_or(overlay));
commands
.entity(dialog_entity)
.insert(MaterialDialogAnchor(anchor));
if existing_placements.get(dialog_entity).is_err() {
commands
.entity(dialog_entity)
.insert(MaterialDialogPlacement::default());
}
spawn_counter.0 += 1;
let spawn_order = DialogSpawnOrder(spawn_counter.0);
commands.entity(dialog_entity).insert(spawn_order);
let layer_root = commands
.spawn((
DialogLayerRootFor(dialog_entity),
Node {
position_type: PositionType::Absolute,
left: Val::Px(0.0),
top: Val::Px(0.0),
width: Val::Percent(100.0),
height: Val::Percent(100.0),
display: Display::None,
..default()
},
))
.id();
commands.entity(overlay).add_child(layer_root);
commands.entity(layer_root).add_child(dialog_entity);
commands.entity(dialog_entity).insert((
ZIndex(1),
FocusPolicy::Block,
Pickable {
should_block_lower: true,
is_hoverable: false,
},
));
let scrim_entity = existing_scrims
.iter()
.find_map(|(entity, for_dialog)| (for_dialog.0 == dialog_entity).then_some(entity))
.unwrap_or_else(|| {
commands
.spawn((
DialogScrim,
DialogScrimFor(dialog_entity),
Node {
display: Display::None,
position_type: PositionType::Absolute,
left: Val::Px(0.0),
top: Val::Px(0.0),
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
},
BackgroundColor(dialog.scrim_color(&theme)),
ZIndex(0),
))
.id()
});
commands.entity(scrim_entity).remove::<GlobalZIndex>();
commands.entity(scrim_entity).insert((
Button,
Interaction::None,
FocusPolicy::Block,
if dialog.modal {
Pickable {
should_block_lower: true,
is_hoverable: true,
}
} else {
Pickable::IGNORE
},
ZIndex(0),
));
commands.entity(layer_root).add_child(scrim_entity);
}
}
fn dialog_layer_visibility_system(
dialogs: Query<&MaterialDialog>,
mut sets: ParamSet<(
Query<(&DialogLayerRootFor, &mut Node)>,
Query<(&DialogScrimFor, &mut Node), With<DialogScrim>>,
)>,
) {
for (for_dialog, mut root_node) in sets.p0().iter_mut() {
let Ok(dialog) = dialogs.get(for_dialog.0) else {
root_node.display = Display::None;
continue;
};
root_node.display = if dialog.open {
Display::Flex
} else {
Display::None
};
}
for (for_dialog, mut scrim_node) in sets.p1().iter_mut() {
let Ok(dialog) = dialogs.get(for_dialog.0) else {
scrim_node.display = Display::None;
continue;
};
scrim_node.display = if dialog.open && dialog.modal {
Display::Flex
} else {
Display::None
};
}
}
fn dialog_layer_z_index_system(
mut commands: Commands,
dialogs: Query<(Entity, &MaterialDialog, &DialogSpawnOrder)>,
mut roots: Query<(Entity, &DialogLayerRootFor)>,
scrims: Query<(Entity, &DialogScrimFor), With<DialogScrim>>,
mut open_stack: ResMut<DialogOpenStack>,
) {
let mut open_dialogs: Vec<(Entity, DialogSpawnOrder)> = dialogs
.iter()
.filter_map(|(entity, dialog, order)| dialog.open.then_some((entity, *order)))
.collect();
open_dialogs.sort_by_key(|(_, order)| *order);
open_stack.ordered = open_dialogs.iter().map(|(e, _)| *e).collect();
for (root_entity, for_dialog) in roots.iter_mut() {
let Some((idx, _)) = open_dialogs
.iter()
.enumerate()
.find(|(_, (dialog_entity, _))| *dialog_entity == for_dialog.0)
else {
continue;
};
let z = 10000 + (idx as i32 * 2);
commands.entity(root_entity).insert(GlobalZIndex(z));
if let Some((scrim_entity, _)) = scrims
.iter()
.find(|(_, scrim_for)| scrim_for.0 == for_dialog.0)
{
commands.entity(scrim_entity).insert(GlobalZIndex(z));
}
commands.entity(for_dialog.0).insert(GlobalZIndex(z + 1));
}
}
fn dialog_bring_to_front_on_open_system(
mut commands: Commands,
mut spawn_counter: ResMut<DialogSpawnCounter>,
dialogs: Query<(Entity, &MaterialDialog), Changed<MaterialDialog>>,
mut orders: Query<&mut DialogSpawnOrder>,
) {
for (entity, dialog) in dialogs.iter() {
if !dialog.open {
continue;
}
spawn_counter.0 += 1;
let new_order = DialogSpawnOrder(spawn_counter.0);
if let Ok(mut order) = orders.get_mut(entity) {
*order = new_order;
} else {
commands.entity(entity).insert(new_order);
}
}
}
fn dialog_position_system(
mut dialogs: Query<(
&MaterialDialog,
Option<&MaterialDialogAnchor>,
Option<&MaterialDialogPlacement>,
&mut Node,
&ComputedNode,
)>,
anchors: Query<(&UiGlobalTransform, &ComputedNode)>,
roots: Query<&DialogLayerRootFor>,
overlay_query: Query<(&UiGlobalTransform, &ComputedNode), With<DialogOverlay>>,
windows: Query<&Window>,
) {
let window = windows.iter().next();
let scale_factor = window.map(|w| w.scale_factor()).unwrap_or(1.0);
let window_physical_size = window.map(|w| {
let mut size = w.physical_size().as_vec2();
if size.x <= 0.0 || size.y <= 0.0 {
size = Vec2::new(w.resolution.width(), w.resolution.height()) * w.scale_factor() as f32;
}
size
});
let (mut overlay_center, mut overlay_size) = overlay_query
.iter()
.next()
.map(|(t, c)| (t.translation, c.size()))
.unwrap_or((Vec2::ZERO, Vec2::ZERO));
if overlay_size.x <= 0.0 || overlay_size.y <= 0.0 {
if let Some(window) = windows.iter().next() {
let mut window_size = window.physical_size().as_vec2();
if window_size.x <= 0.0 || window_size.y <= 0.0 {
let logical = Vec2::new(window.resolution.width(), window.resolution.height());
window_size = logical * window.scale_factor() as f32;
}
overlay_size = window_size;
overlay_center = window_size / 2.0;
}
}
let overlay_top_left = overlay_center - overlay_size / 2.0;
for for_dialog in roots.iter() {
let Ok((dialog, anchor, placement, mut dialog_node, dialog_computed)) =
dialogs.get_mut(for_dialog.0)
else {
continue;
};
if !dialog.open {
continue;
}
if dialog.dialog_type == DialogType::FullScreen {
dialog_node.left = Val::Px(0.0);
dialog_node.top = Val::Px(0.0);
continue;
}
let scale = scale_factor;
let dialog_size_physical = dialog_computed.size();
if dialog_size_physical.x <= 0.0 || dialog_size_physical.y <= 0.0 {
continue;
}
let placement = placement.copied().unwrap_or_default();
let center_in_viewport = || {
if let Some(window_size_physical) = window_physical_size {
(
(window_size_physical.x - dialog_size_physical.x) / 2.0,
(window_size_physical.y - dialog_size_physical.y) / 2.0,
)
} else {
(
overlay_top_left.x + (overlay_size.x - dialog_size_physical.x) / 2.0,
overlay_top_left.y + (overlay_size.y - dialog_size_physical.y) / 2.0,
)
}
};
let (screen_left_physical, screen_top_physical) = match placement {
MaterialDialogPlacement::CenterInViewport => center_in_viewport(),
other => {
if let Some(anchor) = anchor {
match anchors.get(anchor.0) {
Ok((anchor_transform, anchor_computed)) => {
let anchor_center_physical = anchor_transform.translation;
let anchor_size_physical = anchor_computed.size();
if anchor_size_physical.x <= 0.0 || anchor_size_physical.y <= 0.0 {
center_in_viewport()
} else {
let anchor_top_left_physical =
anchor_center_physical - anchor_size_physical / 2.0;
match other {
MaterialDialogPlacement::CenterInViewport => {
center_in_viewport()
}
MaterialDialogPlacement::CenterInAnchor => (
anchor_top_left_physical.x
+ (anchor_size_physical.x - dialog_size_physical.x)
/ 2.0,
anchor_top_left_physical.y
+ (anchor_size_physical.y - dialog_size_physical.y)
/ 2.0,
),
MaterialDialogPlacement::BelowAnchor { gap_px } => {
let gap_physical = gap_px * scale;
(
anchor_top_left_physical.x
+ (anchor_size_physical.x
- dialog_size_physical.x)
/ 2.0,
anchor_top_left_physical.y
+ anchor_size_physical.y
+ gap_physical,
)
}
MaterialDialogPlacement::AboveAnchor { gap_px } => {
let gap_physical = gap_px * scale;
(
anchor_top_left_physical.x
+ (anchor_size_physical.x
- dialog_size_physical.x)
/ 2.0,
anchor_top_left_physical.y
- dialog_size_physical.y
- gap_physical,
)
}
MaterialDialogPlacement::RightOfAnchor { gap_px } => {
let gap_physical = gap_px * scale;
(
anchor_top_left_physical.x
+ anchor_size_physical.x
+ gap_physical,
anchor_top_left_physical.y
+ (anchor_size_physical.y
- dialog_size_physical.y)
/ 2.0,
)
}
MaterialDialogPlacement::LeftOfAnchor { gap_px } => {
let gap_physical = gap_px * scale;
(
anchor_top_left_physical.x
- dialog_size_physical.x
- gap_physical,
anchor_top_left_physical.y
+ (anchor_size_physical.y
- dialog_size_physical.y)
/ 2.0,
)
}
}
}
}
Err(_) => center_in_viewport(),
}
} else {
center_in_viewport()
}
}
};
let left = (screen_left_physical - overlay_top_left.x) / scale;
let top = (screen_top_physical - overlay_top_left.y) / scale;
dialog_node.left = Val::Px(left);
dialog_node.top = Val::Px(top);
}
}
fn dialog_dismiss_on_scrim_click_system(
mouse: Res<ButtonInput<MouseButton>>,
mut close_events: MessageWriter<DialogCloseEvent>,
mut dialogs: Query<&mut MaterialDialog>,
just_opened: Query<(), With<DialogJustOpened>>,
mut scrims: Query<(&DialogScrimFor, &Interaction), (With<DialogScrim>, Changed<Interaction>)>,
) {
for (for_dialog, interaction) in scrims.iter_mut() {
if *interaction != Interaction::Pressed {
continue;
}
if just_opened.get(for_dialog.0).is_ok() {
continue;
}
if !mouse.just_pressed(MouseButton::Left) {
continue;
}
let Ok(mut dialog) = dialogs.get_mut(for_dialog.0) else {
continue;
};
if !dialog.open || !dialog.dismiss_on_scrim_click {
continue;
}
dialog.open = false;
close_events.write(DialogCloseEvent {
entity: for_dialog.0,
dismissed: true,
});
}
}
fn dialog_mark_just_opened_system(
mut commands: Commands,
dialogs: Query<(Entity, &MaterialDialog), Changed<MaterialDialog>>,
) {
for (entity, dialog) in dialogs.iter() {
if dialog.open {
commands.entity(entity).insert(DialogJustOpened);
}
}
}
fn dialog_clear_just_opened_system(
mut commands: Commands,
dialogs: Query<Entity, With<DialogJustOpened>>,
) {
for entity in dialogs.iter() {
commands.entity(entity).remove::<DialogJustOpened>();
}
}
fn dialog_dismiss_on_escape_system(
keys: Res<ButtonInput<KeyCode>>,
open_stack: Res<DialogOpenStack>,
mut close_events: MessageWriter<DialogCloseEvent>,
mut dialogs: Query<&mut MaterialDialog>,
) {
if !keys.just_pressed(KeyCode::Escape) {
return;
}
let Some(&topmost) = open_stack.ordered.last() else {
return;
};
let Ok(mut dialog) = dialogs.get_mut(topmost) else {
return;
};
if !dialog.open || !dialog.dismiss_on_escape {
return;
}
dialog.open = false;
close_events.write(DialogCloseEvent {
entity: topmost,
dismissed: true,
});
}
fn dialog_pickable_system(
mut commands: Commands,
changed_dialogs: Query<(Entity, &MaterialDialog), Changed<MaterialDialog>>,
mut pickables: Query<&mut Pickable>,
) {
if changed_dialogs.is_empty() {
return;
}
for (entity, _dialog) in changed_dialogs.iter() {
let pickable = Pickable {
should_block_lower: true,
is_hoverable: false,
};
if let Ok(mut existing) = pickables.get_mut(entity) {
*existing = pickable;
} else {
commands.entity(entity).insert(pickable);
}
}
}
fn dialog_telemetry_system(
mut commands: Commands,
telemetry: Option<Res<TelemetryConfig>>,
dialogs: Query<(&TestId, &Children), With<MaterialDialog>>,
children_query: Query<&Children>,
headlines: Query<(), With<DialogHeadline>>,
contents: Query<(), With<DialogContent>>,
actions: Query<(), With<DialogActions>>,
) {
let Some(telemetry) = telemetry else {
return;
};
if !telemetry.enabled {
return;
}
for (test_id, children) in dialogs.iter() {
let base = test_id.id();
let mut found_headline = false;
let mut found_content = false;
let mut found_actions = false;
let mut stack: Vec<Entity> = children.iter().collect();
while let Some(entity) = stack.pop() {
if !found_headline && headlines.get(entity).is_ok() {
found_headline = true;
commands.queue(InsertTestIdIfExists {
entity,
id: format!("{base}/headline"),
});
}
if !found_content && contents.get(entity).is_ok() {
found_content = true;
commands.queue(InsertTestIdIfExists {
entity,
id: format!("{base}/content"),
});
}
if !found_actions && actions.get(entity).is_ok() {
found_actions = true;
commands.queue(InsertTestIdIfExists {
entity,
id: format!("{base}/actions"),
});
}
if found_headline && found_content && found_actions {
break;
}
if let Ok(children) = children_query.get(entity) {
stack.extend(children.iter());
}
}
}
}
fn dialog_scrim_telemetry_system(
mut commands: Commands,
telemetry: Option<Res<TelemetryConfig>>,
scrims: Query<(Entity, &DialogScrimFor), With<DialogScrim>>,
dialogs: Query<&TestId, With<MaterialDialog>>,
) {
let Some(telemetry) = telemetry else {
return;
};
if !telemetry.enabled {
return;
}
for (scrim_entity, for_dialog) in scrims.iter() {
let Ok(dialog_id) = dialogs.get(for_dialog.0) else {
continue;
};
let base = dialog_id.id();
commands.queue(InsertTestIdIfExists {
entity: scrim_entity,
id: format!("{base}/scrim"),
});
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum DialogType {
#[default]
Basic,
FullScreen,
}
#[derive(Component)]
pub struct MaterialDialog {
pub dialog_type: DialogType,
pub open: bool,
pub title: Option<String>,
pub icon: Option<String>,
pub dismiss_on_scrim_click: bool,
pub dismiss_on_escape: bool,
pub modal: bool,
}
impl MaterialDialog {
pub fn new() -> Self {
Self {
dialog_type: DialogType::default(),
open: false,
title: None,
icon: None,
dismiss_on_scrim_click: true,
dismiss_on_escape: true,
modal: true,
}
}
pub fn with_type(mut self, dialog_type: DialogType) -> Self {
self.dialog_type = dialog_type;
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn open(mut self, open: bool) -> Self {
self.open = open;
self
}
pub fn no_scrim_dismiss(mut self) -> Self {
self.dismiss_on_scrim_click = false;
self
}
pub fn no_escape_dismiss(mut self) -> Self {
self.dismiss_on_escape = false;
self
}
pub fn modal(mut self, modal: bool) -> Self {
self.modal = modal;
self
}
pub fn surface_color(&self, theme: &MaterialTheme) -> Color {
theme.surface_container_high
}
pub fn scrim_color(&self, theme: &MaterialTheme) -> Color {
theme.scrim.with_alpha(0.32)
}
pub fn title_color(&self, theme: &MaterialTheme) -> Color {
theme.on_surface
}
pub fn content_color(&self, theme: &MaterialTheme) -> Color {
theme.on_surface_variant
}
pub fn icon_color(&self, theme: &MaterialTheme) -> Color {
theme.secondary
}
pub fn elevation(&self) -> Elevation {
Elevation::Level3
}
}
impl Default for MaterialDialog {
fn default() -> Self {
Self::new()
}
}
#[derive(Event, bevy::prelude::Message)]
pub struct DialogOpenEvent {
pub entity: Entity,
}
#[derive(Event, bevy::prelude::Message)]
pub struct DialogCloseEvent {
pub entity: Entity,
pub dismissed: bool,
}
#[derive(Event, bevy::prelude::Message)]
pub struct DialogConfirmEvent {
pub entity: Entity,
}
pub const DIALOG_MIN_WIDTH: f32 = 280.0;
pub const DIALOG_MAX_WIDTH: f32 = 560.0;
fn dialog_visibility_system(
mut dialogs: Query<(&MaterialDialog, &mut Node), Changed<MaterialDialog>>,
) {
for (dialog, mut node) in dialogs.iter_mut() {
node.display = if dialog.open {
Display::Flex
} else {
Display::None
};
}
}
fn dialog_shadow_system(
mut dialogs: Query<(&MaterialDialog, &mut BoxShadow), Changed<MaterialDialog>>,
) {
for (dialog, mut shadow) in dialogs.iter_mut() {
if dialog.open {
*shadow = dialog.elevation().to_box_shadow();
} else {
*shadow = BoxShadow::default();
}
}
}
fn dialog_scrim_visibility_system(
dialogs: Query<&MaterialDialog>,
mut scrims: Query<(&DialogScrimFor, &mut Node), With<DialogScrim>>,
) {
for (for_dialog, mut node) in scrims.iter_mut() {
let Ok(dialog) = dialogs.get(for_dialog.0) else {
node.display = Display::None;
continue;
};
node.display = if dialog.open && dialog.modal {
Display::Flex
} else {
Display::None
};
}
}
fn dialog_scrim_pickable_system(
dialogs: Query<&MaterialDialog>,
mut scrims: Query<(&DialogScrimFor, &mut Pickable), With<DialogScrim>>,
) {
for (for_dialog, mut pickable) in scrims.iter_mut() {
let Ok(dialog) = dialogs.get(for_dialog.0) else {
*pickable = Pickable::IGNORE;
continue;
};
*pickable = if dialog.open && dialog.modal {
Pickable {
should_block_lower: true,
is_hoverable: true,
}
} else {
Pickable::IGNORE
};
}
}
pub struct DialogBuilder {
dialog: MaterialDialog,
title_key: Option<String>,
}
impl DialogBuilder {
pub fn new() -> Self {
Self {
dialog: MaterialDialog::new(),
title_key: None,
}
}
pub fn dialog_type(mut self, dialog_type: DialogType) -> Self {
self.dialog.dialog_type = dialog_type;
self
}
pub fn full_screen(self) -> Self {
self.dialog_type(DialogType::FullScreen)
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.dialog.title = Some(title.into());
self
}
pub fn title_key(mut self, key: impl Into<String>) -> Self {
self.dialog.title = Some(String::new());
self.title_key = Some(key.into());
self
}
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.dialog.icon = Some(icon.into());
self
}
pub fn open(mut self) -> Self {
self.dialog.open = true;
self
}
pub fn no_scrim_dismiss(mut self) -> Self {
self.dialog.dismiss_on_scrim_click = false;
self
}
pub fn no_escape_dismiss(mut self) -> Self {
self.dialog.dismiss_on_escape = false;
self
}
pub fn modal(mut self, modal: bool) -> Self {
self.dialog.modal = modal;
self
}
pub fn build(self, theme: &MaterialTheme) -> impl Bundle {
let bg_color = self.dialog.surface_color(theme);
let is_full_screen = self.dialog.dialog_type == DialogType::FullScreen;
let modal = self.dialog.modal;
(
self.dialog,
Node {
display: Display::None, position_type: PositionType::Absolute,
width: if is_full_screen {
Val::Percent(100.0)
} else {
Val::Auto
},
height: if is_full_screen {
Val::Percent(100.0)
} else {
Val::Auto
},
min_width: if is_full_screen {
Val::Auto
} else {
Val::Px(DIALOG_MIN_WIDTH)
},
max_width: if is_full_screen {
Val::Auto
} else {
Val::Px(DIALOG_MAX_WIDTH)
},
padding: UiRect::all(Val::Px(Spacing::EXTRA_LARGE)),
flex_direction: FlexDirection::Column,
border_radius: BorderRadius::all(Val::Px(if is_full_screen {
0.0
} else {
CornerRadius::EXTRA_LARGE
})),
..default()
},
BackgroundColor(bg_color),
BoxShadow::default(),
if modal {
Pickable {
should_block_lower: true,
is_hoverable: false,
}
} else {
Pickable {
should_block_lower: false,
is_hoverable: false,
}
},
)
}
}
impl Default for DialogBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Component)]
pub struct DialogScrim;
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
pub struct DialogScrimFor(pub Entity);
#[derive(Component)]
pub struct DialogHeadline;
#[derive(Component)]
pub struct DialogContent;
#[derive(Component)]
pub struct DialogActions;
pub fn create_dialog_scrim(theme: &MaterialTheme) -> impl Bundle {
(
DialogScrim,
Node {
position_type: PositionType::Absolute,
left: Val::Px(0.0),
top: Val::Px(0.0),
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(theme.scrim.with_alpha(0.32)),
Pickable {
should_block_lower: true,
is_hoverable: false,
},
)
}
pub fn create_dialog_scrim_for(
theme: &MaterialTheme,
dialog_entity: Entity,
modal: bool,
) -> impl Bundle {
(
DialogScrim,
DialogScrimFor(dialog_entity),
Node {
display: Display::None, position_type: PositionType::Absolute,
left: Val::Px(0.0),
top: Val::Px(0.0),
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(theme.scrim.with_alpha(0.32)),
if modal {
Pickable {
should_block_lower: true,
is_hoverable: false,
}
} else {
Pickable::IGNORE
},
)
}
pub trait SpawnDialogChild {
fn spawn_dialog(
&mut self,
theme: &MaterialTheme,
headline: impl Into<String>,
with_content: impl FnOnce(&mut ChildSpawnerCommands),
);
fn spawn_dialog_with(
&mut self,
theme: &MaterialTheme,
builder: DialogBuilder,
with_content: impl FnOnce(&mut ChildSpawnerCommands),
);
fn spawn_dialog_scrim(&mut self, theme: &MaterialTheme);
}
impl SpawnDialogChild for ChildSpawnerCommands<'_> {
fn spawn_dialog(
&mut self,
theme: &MaterialTheme,
headline: impl Into<String>,
with_content: impl FnOnce(&mut ChildSpawnerCommands),
) {
self.spawn_dialog_with(theme, DialogBuilder::new().title(headline), with_content);
}
fn spawn_dialog_with(
&mut self,
theme: &MaterialTheme,
builder: DialogBuilder,
with_content: impl FnOnce(&mut ChildSpawnerCommands),
) {
let title_text: Option<String> = builder.dialog.title.clone();
let title_key: Option<String> = builder.title_key.clone();
let headline_color = theme.on_surface;
self.spawn(builder.build(theme)).with_children(|dialog| {
if let Some(ref title) = title_text {
if let Some(key) = title_key.as_deref() {
dialog.spawn((
DialogHeadline,
Text::new(""),
LocalizedText::new(key),
TextFont {
font_size: 24.0,
..default()
},
TextColor(headline_color),
Node {
margin: UiRect::bottom(Val::Px(16.0)),
..default()
},
));
} else {
dialog.spawn((
DialogHeadline,
Text::new(title.as_str()),
TextFont {
font_size: 24.0,
..default()
},
TextColor(headline_color),
Node {
margin: UiRect::bottom(Val::Px(16.0)),
..default()
},
));
}
}
dialog
.spawn((
DialogContent,
Node {
flex_direction: FlexDirection::Column,
flex_grow: 1.0,
..default()
},
))
.with_children(with_content);
});
}
fn spawn_dialog_scrim(&mut self, theme: &MaterialTheme) {
self.spawn(create_dialog_scrim(theme));
}
}