use bevy::prelude::*;
use crate::{
icons::MaterialIcon,
ripple::RippleHost,
theme::{blend_state_layer, MaterialTheme},
tokens::CornerRadius,
};
pub struct IconButtonPlugin;
impl Plugin for IconButtonPlugin {
fn build(&self, app: &mut App) {
app.add_message::<IconButtonClickEvent>().add_systems(
Update,
(
icon_button_interaction_system,
icon_button_style_system,
icon_button_content_style_system,
icon_button_theme_refresh_system,
),
);
if !app.is_plugin_added::<crate::MaterialUiCorePlugin>() {
app.add_plugins(crate::MaterialUiCorePlugin);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum IconButtonVariant {
#[default]
Standard,
Filled,
FilledTonal,
Outlined,
}
#[derive(Component)]
pub struct MaterialIconButton {
pub variant: IconButtonVariant,
pub disabled: bool,
pub selected: bool,
pub toggle: bool,
pub icon: String,
pub pressed: bool,
pub hovered: bool,
}
impl MaterialIconButton {
pub fn new(icon: impl Into<String>) -> Self {
Self {
variant: IconButtonVariant::default(),
disabled: false,
selected: false,
toggle: false,
icon: icon.into(),
pressed: false,
hovered: false,
}
}
pub fn with_variant(mut self, variant: IconButtonVariant) -> Self {
self.variant = variant;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn toggleable(mut self) -> Self {
self.toggle = true;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn background_color(&self, theme: &MaterialTheme) -> Color {
if self.disabled {
return match self.variant {
IconButtonVariant::Standard => Color::NONE,
IconButtonVariant::Filled | IconButtonVariant::FilledTonal => {
theme.on_surface.with_alpha(0.12)
}
IconButtonVariant::Outlined => Color::NONE,
};
}
let base = match self.variant {
IconButtonVariant::Standard => Color::NONE,
IconButtonVariant::Filled => {
if self.selected {
theme.primary
} else {
theme.surface_container_highest
}
}
IconButtonVariant::FilledTonal => {
if self.selected {
theme.secondary_container
} else {
theme.surface_container_highest
}
}
IconButtonVariant::Outlined => {
if self.selected {
theme.inverse_surface
} else {
Color::NONE
}
}
};
let state_opacity = self.state_layer_opacity();
if state_opacity > 0.0 {
let state_color = self.icon_color(theme);
if base == Color::NONE {
state_color.with_alpha(state_opacity)
} else {
blend_state_layer(base, state_color, state_opacity)
}
} else {
base
}
}
fn state_layer_opacity(&self) -> f32 {
if self.disabled {
0.0
} else if self.pressed {
0.12
} else if self.hovered {
0.08
} else {
0.0
}
}
pub fn icon_color(&self, theme: &MaterialTheme) -> Color {
if self.disabled {
return theme.on_surface.with_alpha(0.38);
}
match self.variant {
IconButtonVariant::Standard => {
if self.selected {
theme.primary
} else {
theme.on_surface_variant
}
}
IconButtonVariant::Filled => {
if self.selected {
theme.on_primary
} else {
theme.primary
}
}
IconButtonVariant::FilledTonal => {
if self.selected {
theme.on_secondary_container
} else {
theme.on_surface_variant
}
}
IconButtonVariant::Outlined => {
if self.selected {
theme.inverse_on_surface
} else {
theme.on_surface_variant
}
}
}
}
pub fn border_color(&self, theme: &MaterialTheme) -> Color {
if self.variant != IconButtonVariant::Outlined {
return Color::NONE;
}
if self.disabled {
theme.on_surface.with_alpha(0.12)
} else if self.selected {
Color::NONE
} else {
theme.outline
}
}
}
#[derive(Event, bevy::prelude::Message)]
pub struct IconButtonClickEvent {
pub entity: Entity,
pub selected: bool,
}
fn icon_button_interaction_system(
mut interaction_query: Query<
(Entity, &Interaction, &mut MaterialIconButton),
(Changed<Interaction>, With<MaterialIconButton>),
>,
mut click_events: MessageWriter<IconButtonClickEvent>,
) {
for (entity, interaction, mut button) in interaction_query.iter_mut() {
if button.disabled {
continue;
}
match *interaction {
Interaction::Pressed => {
button.pressed = true;
button.hovered = false;
if button.toggle {
button.selected = !button.selected;
}
click_events.write(IconButtonClickEvent {
entity,
selected: button.selected,
});
}
Interaction::Hovered => {
button.pressed = false;
button.hovered = true;
}
Interaction::None => {
button.pressed = false;
button.hovered = false;
}
}
}
}
fn icon_button_style_system(
theme: Option<Res<MaterialTheme>>,
mut buttons: Query<
(&MaterialIconButton, &mut BackgroundColor, &mut BorderColor),
Changed<MaterialIconButton>,
>,
) {
let Some(theme) = theme else { return };
for (button, mut bg_color, mut border_color) in buttons.iter_mut() {
*bg_color = BackgroundColor(button.background_color(&theme));
*border_color = BorderColor::all(button.border_color(&theme));
}
}
fn icon_button_content_style_system(
theme: Option<Res<MaterialTheme>>,
buttons: Query<(Entity, &MaterialIconButton), Changed<MaterialIconButton>>,
children_q: Query<&Children>,
mut icons: Query<&mut MaterialIcon>,
) {
let Some(theme) = theme else { return };
for (entity, button) in buttons.iter() {
let Ok(children) = children_q.get(entity) else {
continue;
};
let icon_color = button.icon_color(&theme);
for child in children.iter() {
if let Ok(mut icon) = icons.get_mut(child) {
icon.color = icon_color;
}
}
}
}
fn icon_button_theme_refresh_system(
theme: Option<Res<MaterialTheme>>,
mut buttons: Query<(
Entity,
&MaterialIconButton,
&mut BackgroundColor,
&mut BorderColor,
)>,
children_q: Query<&Children>,
mut icons: Query<&mut MaterialIcon>,
) {
let Some(theme) = theme else { return };
if !theme.is_changed() {
return;
}
for (entity, button, mut bg_color, mut border_color) in buttons.iter_mut() {
*bg_color = BackgroundColor(button.background_color(&theme));
*border_color = BorderColor::all(button.border_color(&theme));
let Ok(children) = children_q.get(entity) else {
continue;
};
let icon_color = button.icon_color(&theme);
for child in children.iter() {
if let Ok(mut icon) = icons.get_mut(child) {
icon.color = icon_color;
}
}
}
}
pub const ICON_BUTTON_SIZE: f32 = 40.0;
pub const ICON_SIZE: f32 = 24.0;
pub struct IconButtonBuilder {
button: MaterialIconButton,
}
impl IconButtonBuilder {
pub fn new(icon: impl Into<String>) -> Self {
Self {
button: MaterialIconButton::new(icon),
}
}
pub fn variant(mut self, variant: IconButtonVariant) -> Self {
self.button.variant = variant;
self
}
pub fn standard(self) -> Self {
self.variant(IconButtonVariant::Standard)
}
pub fn filled(self) -> Self {
self.variant(IconButtonVariant::Filled)
}
pub fn filled_tonal(self) -> Self {
self.variant(IconButtonVariant::FilledTonal)
}
pub fn outlined(self) -> Self {
self.variant(IconButtonVariant::Outlined)
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.button.disabled = disabled;
self
}
pub fn toggle(mut self) -> Self {
self.button.toggle = true;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.button.selected = selected;
self
}
pub fn build(self, theme: &MaterialTheme) -> impl Bundle {
let bg_color = self.button.background_color(theme);
let border_color = self.button.border_color(theme);
let border_width = if self.button.variant == IconButtonVariant::Outlined {
1.0
} else {
0.0
};
(
self.button,
Button,
RippleHost::new(),
Node {
width: Val::Px(ICON_BUTTON_SIZE),
height: Val::Px(ICON_BUTTON_SIZE),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(border_width)),
border_radius: BorderRadius::all(Val::Px(CornerRadius::FULL)),
..default()
},
BackgroundColor(bg_color),
BorderColor::all(border_color),
)
}
}
pub trait SpawnIconButtonChild {
fn spawn_icon_button(
&mut self,
theme: &MaterialTheme,
icon: impl Into<String>,
variant: IconButtonVariant,
);
fn spawn_standard_icon_button(&mut self, theme: &MaterialTheme, icon: impl Into<String>);
fn spawn_filled_icon_button(&mut self, theme: &MaterialTheme, icon: impl Into<String>);
fn spawn_outlined_icon_button(&mut self, theme: &MaterialTheme, icon: impl Into<String>);
fn spawn_icon_button_with(&mut self, theme: &MaterialTheme, button: MaterialIconButton);
}
impl SpawnIconButtonChild for ChildSpawnerCommands<'_> {
fn spawn_icon_button(
&mut self,
theme: &MaterialTheme,
icon: impl Into<String>,
variant: IconButtonVariant,
) {
let icon_name = icon.into();
let builder = IconButtonBuilder::new(icon_name.clone()).variant(variant);
let icon_color = builder.button.icon_color(theme);
self.spawn(builder.build(theme)).with_children(|button| {
if let Some(icon) = MaterialIcon::from_name(&icon_name) {
button.spawn(icon.with_color(icon_color).with_size(ICON_SIZE));
}
});
}
fn spawn_standard_icon_button(&mut self, theme: &MaterialTheme, icon: impl Into<String>) {
self.spawn_icon_button(theme, icon, IconButtonVariant::Standard);
}
fn spawn_filled_icon_button(&mut self, theme: &MaterialTheme, icon: impl Into<String>) {
self.spawn_icon_button(theme, icon, IconButtonVariant::Filled);
}
fn spawn_outlined_icon_button(&mut self, theme: &MaterialTheme, icon: impl Into<String>) {
self.spawn_icon_button(theme, icon, IconButtonVariant::Outlined);
}
fn spawn_icon_button_with(&mut self, theme: &MaterialTheme, button: MaterialIconButton) {
let icon_color = button.icon_color(theme);
let icon_name = button.icon.clone();
let builder = IconButtonBuilder { button };
self.spawn(builder.build(theme)).with_children(|btn| {
if let Some(icon) = MaterialIcon::from_name(&icon_name) {
btn.spawn(icon.with_color(icon_color).with_size(ICON_SIZE));
}
});
}
}