use bevy::prelude::*;
use bevy::ui::BoxShadow;
use crate::{
elevation::Elevation,
theme::{blend_state_layer, MaterialTheme},
tokens::{CornerRadius, Spacing},
};
pub struct CardPlugin;
impl Plugin for CardPlugin {
fn build(&self, app: &mut App) {
if !app.is_plugin_added::<crate::MaterialUiCorePlugin>() {
app.add_plugins(crate::MaterialUiCorePlugin);
}
app.add_message::<CardClickEvent>().add_systems(
Update,
(
card_interaction_system,
card_style_system,
card_theme_refresh_system,
card_shadow_system,
),
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum CardVariant {
#[default]
Elevated,
Filled,
Outlined,
}
#[derive(Component)]
pub struct MaterialCard {
pub variant: CardVariant,
pub clickable: bool,
pub draggable: bool,
pub pressed: bool,
pub hovered: bool,
}
impl MaterialCard {
pub fn new() -> Self {
Self {
variant: CardVariant::default(),
clickable: false,
draggable: false,
pressed: false,
hovered: false,
}
}
pub fn with_variant(mut self, variant: CardVariant) -> Self {
self.variant = variant;
self
}
pub fn clickable(mut self) -> Self {
self.clickable = true;
self
}
pub fn draggable(mut self) -> Self {
self.draggable = true;
self
}
pub fn background_color(&self, theme: &MaterialTheme) -> Color {
let base = match self.variant {
CardVariant::Elevated => theme.surface_container_low,
CardVariant::Filled => theme.surface_container_highest,
CardVariant::Outlined => theme.surface,
};
let state_opacity = self.state_layer_opacity();
if state_opacity > 0.0 {
blend_state_layer(base, theme.on_surface, state_opacity)
} else {
base
}
}
pub fn border_color(&self, theme: &MaterialTheme) -> Color {
match self.variant {
CardVariant::Outlined => theme.outline_variant,
_ => Color::NONE,
}
}
pub fn elevation(&self) -> Elevation {
match self.variant {
CardVariant::Elevated => {
if self.pressed || self.hovered {
Elevation::Level2
} else {
Elevation::Level1
}
}
CardVariant::Filled | CardVariant::Outlined => {
if self.clickable && (self.pressed || self.hovered) {
Elevation::Level1
} else {
Elevation::Level0
}
}
}
}
pub fn state_layer_opacity(&self) -> f32 {
if !self.clickable {
return 0.0;
}
if self.pressed {
0.12
} else if self.hovered {
0.08
} else {
0.0
}
}
}
impl Default for MaterialCard {
fn default() -> Self {
Self::new()
}
}
#[derive(Event, bevy::prelude::Message)]
pub struct CardClickEvent {
pub entity: Entity,
}
fn card_interaction_system(
mut interaction_query: Query<
(Entity, &Interaction, &mut MaterialCard),
(Changed<Interaction>, With<MaterialCard>),
>,
mut click_events: MessageWriter<CardClickEvent>,
) {
for (entity, interaction, mut card) in interaction_query.iter_mut() {
if !card.clickable {
continue;
}
match *interaction {
Interaction::Pressed => {
card.pressed = true;
card.hovered = false;
click_events.write(CardClickEvent { entity });
}
Interaction::Hovered => {
card.pressed = false;
card.hovered = true;
}
Interaction::None => {
card.pressed = false;
card.hovered = false;
}
}
}
}
fn card_style_system(
theme: Option<Res<MaterialTheme>>,
mut cards: Query<
(&MaterialCard, &mut BackgroundColor, &mut BorderColor),
Changed<MaterialCard>,
>,
) {
let Some(theme) = theme else { return };
for (card, mut bg_color, mut border_color) in cards.iter_mut() {
*bg_color = BackgroundColor(card.background_color(&theme));
*border_color = BorderColor::all(card.border_color(&theme));
}
}
fn card_theme_refresh_system(
theme: Option<Res<MaterialTheme>>,
mut cards: Query<(&MaterialCard, &mut BackgroundColor, &mut BorderColor)>,
) {
let Some(theme) = theme else { return };
if !theme.is_changed() {
return;
}
for (card, mut bg_color, mut border_color) in cards.iter_mut() {
*bg_color = BackgroundColor(card.background_color(&theme));
*border_color = BorderColor::all(card.border_color(&theme));
}
}
fn card_shadow_system(mut cards: Query<(&MaterialCard, &mut BoxShadow), Changed<MaterialCard>>) {
for (card, mut box_shadow) in cards.iter_mut() {
let elevation = card.elevation();
*box_shadow = elevation.to_box_shadow();
}
}
pub struct CardBuilder {
card: MaterialCard,
width: Option<Val>,
height: Option<Val>,
padding: f32,
}
impl CardBuilder {
pub fn new() -> Self {
Self {
card: MaterialCard::new(),
width: None,
height: None,
padding: Spacing::LARGE,
}
}
pub fn variant(mut self, variant: CardVariant) -> Self {
self.card.variant = variant;
self
}
pub fn elevated(self) -> Self {
self.variant(CardVariant::Elevated)
}
pub fn filled(self) -> Self {
self.variant(CardVariant::Filled)
}
pub fn outlined(self) -> Self {
self.variant(CardVariant::Outlined)
}
pub fn clickable(mut self) -> Self {
self.card.clickable = true;
self
}
pub fn draggable(mut self) -> Self {
self.card.draggable = true;
self
}
pub fn width(mut self, width: Val) -> Self {
self.width = Some(width);
self
}
pub fn height(mut self, height: Val) -> Self {
self.height = Some(height);
self
}
pub fn padding(mut self, padding: f32) -> Self {
self.padding = padding;
self
}
pub fn build(self, theme: &MaterialTheme) -> impl Bundle {
let bg_color = self.card.background_color(theme);
let border_color = self.card.border_color(theme);
let border_width = if self.card.variant == CardVariant::Outlined {
1.0
} else {
0.0
};
let elevation = self.card.elevation();
let mut node = Node {
padding: UiRect::all(Val::Px(self.padding)),
border: UiRect::all(Val::Px(border_width)),
flex_direction: FlexDirection::Column,
border_radius: BorderRadius::all(Val::Px(CornerRadius::MEDIUM)),
..default()
};
if let Some(w) = self.width {
node.width = w;
}
if let Some(h) = self.height {
node.height = h;
}
(
self.card,
node,
BackgroundColor(bg_color),
BorderColor::all(border_color),
elevation.to_box_shadow(),
)
}
pub fn build_without_shadow(self, theme: &MaterialTheme) -> impl Bundle {
let bg_color = self.card.background_color(theme);
let border_color = self.card.border_color(theme);
let border_width = if self.card.variant == CardVariant::Outlined {
1.0
} else {
0.0
};
let mut node = Node {
padding: UiRect::all(Val::Px(self.padding)),
border: UiRect::all(Val::Px(border_width)),
flex_direction: FlexDirection::Column,
border_radius: BorderRadius::all(Val::Px(CornerRadius::MEDIUM)),
..default()
};
if let Some(w) = self.width {
node.width = w;
}
if let Some(h) = self.height {
node.height = h;
}
(
self.card,
node,
BackgroundColor(bg_color),
BorderColor::all(border_color),
)
}
}
impl Default for CardBuilder {
fn default() -> Self {
Self::new()
}
}
pub trait SpawnCardChild {
fn spawn_elevated_card(
&mut self,
theme: &MaterialTheme,
with_children: impl FnOnce(&mut ChildSpawnerCommands),
);
fn spawn_filled_card(
&mut self,
theme: &MaterialTheme,
with_children: impl FnOnce(&mut ChildSpawnerCommands),
);
fn spawn_outlined_card(
&mut self,
theme: &MaterialTheme,
with_children: impl FnOnce(&mut ChildSpawnerCommands),
);
fn spawn_card_with(
&mut self,
theme: &MaterialTheme,
builder: CardBuilder,
with_children: impl FnOnce(&mut ChildSpawnerCommands),
);
}
impl SpawnCardChild for ChildSpawnerCommands<'_> {
fn spawn_elevated_card(
&mut self,
theme: &MaterialTheme,
with_children: impl FnOnce(&mut ChildSpawnerCommands),
) {
self.spawn_card_with(theme, CardBuilder::new().elevated(), with_children);
}
fn spawn_filled_card(
&mut self,
theme: &MaterialTheme,
with_children: impl FnOnce(&mut ChildSpawnerCommands),
) {
self.spawn_card_with(theme, CardBuilder::new().filled(), with_children);
}
fn spawn_outlined_card(
&mut self,
theme: &MaterialTheme,
with_children: impl FnOnce(&mut ChildSpawnerCommands),
) {
self.spawn_card_with(theme, CardBuilder::new().outlined(), with_children);
}
fn spawn_card_with(
&mut self,
theme: &MaterialTheme,
builder: CardBuilder,
with_children: impl FnOnce(&mut ChildSpawnerCommands),
) {
self.spawn(builder.build(theme))
.with_children(with_children);
}
}