use bevy::prelude::*;
use bevy::ui::BoxShadow;
use crate::{
elevation::Elevation,
icons::IconStyle,
ripple::RippleHost,
theme::{blend_state_layer, MaterialTheme},
tokens::{CornerRadius, Spacing},
};
pub struct FabPlugin;
impl Plugin for FabPlugin {
fn build(&self, app: &mut App) {
app.add_message::<FabClickEvent>().add_systems(
Update,
(
fab_interaction_system,
fab_style_system,
fab_content_style_system,
fab_theme_refresh_system,
fab_shadow_system,
),
);
if !app.is_plugin_added::<crate::MaterialUiCorePlugin>() {
app.add_plugins(crate::MaterialUiCorePlugin);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum FabSize {
Small,
#[default]
Regular,
Large,
}
impl FabSize {
pub fn size(&self) -> f32 {
match self {
FabSize::Small => 40.0,
FabSize::Regular => 56.0,
FabSize::Large => 96.0,
}
}
pub fn icon_size(&self) -> f32 {
match self {
FabSize::Small => 24.0,
FabSize::Regular => 24.0,
FabSize::Large => 36.0,
}
}
pub fn corner_radius(&self) -> f32 {
match self {
FabSize::Small => CornerRadius::MEDIUM,
FabSize::Regular => CornerRadius::LARGE,
FabSize::Large => CornerRadius::EXTRA_LARGE,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum FabColor {
#[default]
Primary,
Surface,
Secondary,
Tertiary,
}
#[derive(Component)]
pub struct MaterialFab {
pub size: FabSize,
pub color: FabColor,
pub lowered: bool,
pub icon: String,
pub label: Option<String>,
pub pressed: bool,
pub hovered: bool,
}
impl MaterialFab {
pub fn new(icon: impl Into<String>) -> Self {
Self {
size: FabSize::default(),
color: FabColor::default(),
lowered: false,
icon: icon.into(),
label: None,
pressed: false,
hovered: false,
}
}
pub fn with_size(mut self, size: FabSize) -> Self {
self.size = size;
self
}
pub fn with_color(mut self, color: FabColor) -> Self {
self.color = color;
self
}
pub fn lowered(mut self) -> Self {
self.lowered = true;
self
}
pub fn extended(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn background_color(&self, theme: &MaterialTheme) -> Color {
let base = match self.color {
FabColor::Primary => theme.primary_container,
FabColor::Surface => theme.surface_container_high,
FabColor::Secondary => theme.secondary_container,
FabColor::Tertiary => theme.tertiary_container,
};
let state_opacity = self.state_layer_opacity();
if state_opacity > 0.0 {
let state_color = self.content_color(theme);
blend_state_layer(base, state_color, state_opacity)
} else {
base
}
}
fn state_layer_opacity(&self) -> f32 {
if self.pressed {
0.12
} else if self.hovered {
0.08
} else {
0.0
}
}
pub fn content_color(&self, theme: &MaterialTheme) -> Color {
match self.color {
FabColor::Primary => theme.on_primary_container,
FabColor::Surface => theme.primary,
FabColor::Secondary => theme.on_secondary_container,
FabColor::Tertiary => theme.on_tertiary_container,
}
}
pub fn elevation(&self) -> Elevation {
if self.lowered {
if self.pressed {
Elevation::Level1
} else if self.hovered {
Elevation::Level2
} else {
Elevation::Level1
}
} else if self.pressed {
Elevation::Level3
} else if self.hovered {
Elevation::Level4
} else {
Elevation::Level3
}
}
pub fn is_extended(&self) -> bool {
self.label.is_some()
}
}
#[derive(Event, bevy::prelude::Message)]
pub struct FabClickEvent {
pub entity: Entity,
}
fn fab_interaction_system(
mut interaction_query: Query<
(Entity, &Interaction, &mut MaterialFab),
(Changed<Interaction>, With<MaterialFab>),
>,
mut click_events: MessageWriter<FabClickEvent>,
) {
for (entity, interaction, mut fab) in interaction_query.iter_mut() {
match *interaction {
Interaction::Pressed => {
fab.pressed = true;
fab.hovered = false;
click_events.write(FabClickEvent { entity });
}
Interaction::Hovered => {
fab.pressed = false;
fab.hovered = true;
}
Interaction::None => {
fab.pressed = false;
fab.hovered = false;
}
}
}
}
fn fab_style_system(
theme: Option<Res<MaterialTheme>>,
mut fabs: Query<(&MaterialFab, &mut BackgroundColor), Changed<MaterialFab>>,
) {
let Some(theme) = theme else { return };
for (fab, mut bg_color) in fabs.iter_mut() {
*bg_color = BackgroundColor(fab.background_color(&theme));
}
}
fn fab_content_style_system(
theme: Option<Res<MaterialTheme>>,
fabs: Query<(Entity, &MaterialFab), Changed<MaterialFab>>,
children_q: Query<&Children>,
mut icon_styles: Query<&mut IconStyle>,
mut labels: Query<&mut TextColor, With<FabLabel>>,
) {
let Some(theme) = theme else { return };
for (entity, fab) in fabs.iter() {
let Ok(children) = children_q.get(entity) else {
continue;
};
let content_color = fab.content_color(&theme);
for child in children.iter() {
if let Ok(mut style) = icon_styles.get_mut(child) {
style.color = content_color;
}
if let Ok(mut color) = labels.get_mut(child) {
color.0 = content_color;
}
}
}
}
fn fab_theme_refresh_system(
theme: Option<Res<MaterialTheme>>,
mut fabs: Query<(Entity, &MaterialFab, &mut BackgroundColor)>,
children_q: Query<&Children>,
mut icon_styles: Query<&mut IconStyle>,
mut labels: Query<&mut TextColor, With<FabLabel>>,
) {
let Some(theme) = theme else { return };
if !theme.is_changed() {
return;
}
for (entity, fab, mut bg_color) in fabs.iter_mut() {
*bg_color = BackgroundColor(fab.background_color(&theme));
let Ok(children) = children_q.get(entity) else {
continue;
};
let content_color = fab.content_color(&theme);
for child in children.iter() {
if let Ok(mut style) = icon_styles.get_mut(child) {
style.color = content_color;
}
if let Ok(mut color) = labels.get_mut(child) {
color.0 = content_color;
}
}
}
}
fn fab_shadow_system(mut fabs: Query<(&MaterialFab, &mut BoxShadow), Changed<MaterialFab>>) {
for (fab, mut box_shadow) in fabs.iter_mut() {
let elevation = fab.elevation();
*box_shadow = elevation.to_box_shadow();
}
}
pub struct FabBuilder {
fab: MaterialFab,
}
impl FabBuilder {
pub fn new(icon: impl Into<String>) -> Self {
Self {
fab: MaterialFab::new(icon),
}
}
pub fn size(mut self, size: FabSize) -> Self {
self.fab.size = size;
self
}
pub fn small(self) -> Self {
self.size(FabSize::Small)
}
pub fn large(self) -> Self {
self.size(FabSize::Large)
}
pub fn color(mut self, color: FabColor) -> Self {
self.fab.color = color;
self
}
pub fn surface(self) -> Self {
self.color(FabColor::Surface)
}
pub fn secondary(self) -> Self {
self.color(FabColor::Secondary)
}
pub fn tertiary(self) -> Self {
self.color(FabColor::Tertiary)
}
pub fn lowered(mut self) -> Self {
self.fab.lowered = true;
self
}
pub fn extended(mut self, label: impl Into<String>) -> Self {
self.fab.label = Some(label.into());
self
}
pub fn build(self, theme: &MaterialTheme) -> impl Bundle {
let bg_color = self.fab.background_color(theme);
let size = self.fab.size.size();
let corner_radius = self.fab.size.corner_radius();
let is_extended = self.fab.is_extended();
let elevation = self.fab.elevation();
(
self.fab,
Button,
RippleHost::new(),
Node {
width: if is_extended {
Val::Auto
} else {
Val::Px(size)
},
height: Val::Px(size),
min_width: if is_extended {
Val::Px(80.0)
} else {
Val::Auto
},
padding: if is_extended {
UiRect::axes(Val::Px(Spacing::LARGE), Val::Px(Spacing::LARGE))
} else {
UiRect::all(Val::Px(0.0))
},
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
column_gap: if is_extended {
Val::Px(Spacing::SMALL)
} else {
Val::Px(0.0)
},
border_radius: BorderRadius::all(Val::Px(corner_radius)),
..default()
},
BackgroundColor(bg_color),
elevation.to_box_shadow(),
)
}
}
use crate::icons::MaterialIcon;
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct FabLabel;
pub trait SpawnFabChild {
fn spawn_fab(&mut self, theme: &MaterialTheme, icon: impl Into<String>, size: FabSize);
fn spawn_small_fab(&mut self, theme: &MaterialTheme, icon: impl Into<String>);
fn spawn_regular_fab(&mut self, theme: &MaterialTheme, icon: impl Into<String>);
fn spawn_large_fab(&mut self, theme: &MaterialTheme, icon: impl Into<String>);
fn spawn_extended_fab(
&mut self,
theme: &MaterialTheme,
icon: impl Into<String>,
label: impl Into<String>,
);
fn spawn_fab_with(&mut self, theme: &MaterialTheme, fab: MaterialFab);
}
impl SpawnFabChild for ChildSpawnerCommands<'_> {
fn spawn_fab(&mut self, theme: &MaterialTheme, icon: impl Into<String>, size: FabSize) {
let icon_name = icon.into();
let builder = FabBuilder::new(icon_name.clone()).size(size);
let icon_color = builder.fab.content_color(theme);
let icon_size = builder.fab.size.icon_size();
self.spawn(builder.build(theme)).with_children(|fab| {
if let Some(icon) = MaterialIcon::from_name(&icon_name) {
fab.spawn((
icon,
IconStyle::outlined()
.with_color(icon_color)
.with_size(icon_size),
));
}
});
}
fn spawn_small_fab(&mut self, theme: &MaterialTheme, icon: impl Into<String>) {
self.spawn_fab(theme, icon, FabSize::Small);
}
fn spawn_regular_fab(&mut self, theme: &MaterialTheme, icon: impl Into<String>) {
self.spawn_fab(theme, icon, FabSize::Regular);
}
fn spawn_large_fab(&mut self, theme: &MaterialTheme, icon: impl Into<String>) {
self.spawn_fab(theme, icon, FabSize::Large);
}
fn spawn_extended_fab(
&mut self,
theme: &MaterialTheme,
icon: impl Into<String>,
label: impl Into<String>,
) {
let icon_name = icon.into();
let label_str = label.into();
let builder = FabBuilder::new(icon_name.clone()).extended(label_str.clone());
let icon_color = builder.fab.content_color(theme);
let text_color = builder.fab.content_color(theme);
let icon_size = builder.fab.size.icon_size();
self.spawn(builder.build(theme)).with_children(|fab| {
if let Some(icon) = MaterialIcon::from_name(&icon_name) {
fab.spawn((
icon,
IconStyle::outlined()
.with_color(icon_color)
.with_size(icon_size),
));
}
fab.spawn((
FabLabel,
Text::new(label_str),
TextColor(text_color),
TextFont {
font_size: 14.0,
..default()
},
));
});
}
fn spawn_fab_with(&mut self, theme: &MaterialTheme, fab: MaterialFab) {
let icon_color = fab.content_color(theme);
let text_color = fab.content_color(theme);
let icon_name = fab.icon.clone();
let label_text = fab.label.clone();
let icon_size = fab.size.icon_size();
let builder = FabBuilder { fab };
self.spawn(builder.build(theme)).with_children(|fab_inner| {
if let Some(icon) = MaterialIcon::from_name(&icon_name) {
fab_inner.spawn((
icon,
IconStyle::outlined()
.with_color(icon_color)
.with_size(icon_size),
));
}
if let Some(label) = label_text {
fab_inner.spawn((
FabLabel,
Text::new(label),
TextColor(text_color),
TextFont {
font_size: 14.0,
..default()
},
));
}
});
}
}