use super::codepoints::*;
use super::style::IconStyle;
use super::MaterialIconFont;
use super::EMBEDDED_MATERIAL_SYMBOLS_FONT;
use bevy::prelude::*;
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
pub struct MaterialIcon {
pub codepoint: char,
}
impl Default for MaterialIcon {
fn default() -> Self {
Self::home()
}
}
impl MaterialIcon {
pub fn new(codepoint: char) -> Self {
Self { codepoint }
}
pub fn from_name(name: &str) -> Option<Self> {
icon_by_name(name).map(Self::new)
}
pub fn as_str(&self) -> String {
self.codepoint.to_string()
}
pub fn home() -> Self {
Self::new(ICON_HOME)
}
pub fn menu() -> Self {
Self::new(ICON_MENU)
}
pub fn more_vert() -> Self {
Self::new(ICON_MORE_VERT)
}
pub fn more_horiz() -> Self {
Self::new(ICON_MORE_HORIZ)
}
pub fn arrow_back() -> Self {
Self::new(ICON_ARROW_BACK)
}
pub fn arrow_forward() -> Self {
Self::new(ICON_ARROW_FORWARD)
}
pub fn arrow_upward() -> Self {
Self::new(ICON_ARROW_UPWARD)
}
pub fn arrow_downward() -> Self {
Self::new(ICON_ARROW_DOWNWARD)
}
pub fn close() -> Self {
Self::new(ICON_CLOSE)
}
pub fn check() -> Self {
Self::new(ICON_CHECK)
}
pub fn expand_more() -> Self {
Self::new(ICON_EXPAND_MORE)
}
pub fn expand_less() -> Self {
Self::new(ICON_EXPAND_LESS)
}
pub fn chevron_left() -> Self {
Self::new(ICON_CHEVRON_LEFT)
}
pub fn chevron_right() -> Self {
Self::new(ICON_CHEVRON_RIGHT)
}
pub fn add() -> Self {
Self::new(ICON_ADD)
}
pub fn remove() -> Self {
Self::new(ICON_REMOVE)
}
pub fn delete() -> Self {
Self::new(ICON_DELETE)
}
pub fn edit() -> Self {
Self::new(ICON_EDIT)
}
pub fn save() -> Self {
Self::new(ICON_SAVE)
}
pub fn search() -> Self {
Self::new(ICON_SEARCH)
}
pub fn refresh() -> Self {
Self::new(ICON_REFRESH)
}
pub fn settings() -> Self {
Self::new(ICON_SETTINGS)
}
pub fn help() -> Self {
Self::new(ICON_HELP)
}
pub fn info() -> Self {
Self::new(ICON_INFO)
}
pub fn share() -> Self {
Self::new(ICON_SHARE)
}
pub fn download() -> Self {
Self::new(ICON_DOWNLOAD)
}
pub fn upload() -> Self {
Self::new(ICON_UPLOAD)
}
pub fn print() -> Self {
Self::new(ICON_PRINT)
}
pub fn copy() -> Self {
Self::new(ICON_CONTENT_COPY)
}
pub fn paste() -> Self {
Self::new(ICON_CONTENT_PASTE)
}
pub fn cut() -> Self {
Self::new(ICON_CONTENT_CUT)
}
pub fn undo() -> Self {
Self::new(ICON_UNDO)
}
pub fn redo() -> Self {
Self::new(ICON_REDO)
}
pub fn checkbox_checked() -> Self {
Self::new(ICON_CHECK_BOX)
}
pub fn checkbox_unchecked() -> Self {
Self::new(ICON_CHECK_BOX_OUTLINE_BLANK)
}
pub fn radio_checked() -> Self {
Self::new(ICON_RADIO_BUTTON_CHECKED)
}
pub fn radio_unchecked() -> Self {
Self::new(ICON_RADIO_BUTTON_UNCHECKED)
}
pub fn toggle_on() -> Self {
Self::new(ICON_TOGGLE_ON)
}
pub fn toggle_off() -> Self {
Self::new(ICON_TOGGLE_OFF)
}
pub fn star() -> Self {
Self::new(ICON_STAR)
}
pub fn star_outline() -> Self {
Self::new(ICON_STAR_BORDER)
}
pub fn favorite() -> Self {
Self::new(ICON_FAVORITE)
}
pub fn favorite_outline() -> Self {
Self::new(ICON_FAVORITE_BORDER)
}
pub fn visibility() -> Self {
Self::new(ICON_VISIBILITY)
}
pub fn visibility_off() -> Self {
Self::new(ICON_VISIBILITY_OFF)
}
pub fn error() -> Self {
Self::new(ICON_ERROR)
}
pub fn warning() -> Self {
Self::new(ICON_WARNING)
}
pub fn check_circle() -> Self {
Self::new(ICON_CHECK_CIRCLE)
}
pub fn cancel() -> Self {
Self::new(ICON_CANCEL)
}
pub fn block() -> Self {
Self::new(ICON_BLOCK)
}
pub fn notifications() -> Self {
Self::new(ICON_NOTIFICATIONS)
}
pub fn notifications_off() -> Self {
Self::new(ICON_NOTIFICATIONS_OFF)
}
pub fn folder() -> Self {
Self::new(ICON_FOLDER)
}
pub fn folder_open() -> Self {
Self::new(ICON_FOLDER_OPEN)
}
pub fn document() -> Self {
Self::new(ICON_DESCRIPTION)
}
pub fn image() -> Self {
Self::new(ICON_IMAGE)
}
pub fn video() -> Self {
Self::new(ICON_VIDEOCAM)
}
pub fn music() -> Self {
Self::new(ICON_MUSIC_NOTE)
}
pub fn link() -> Self {
Self::new(ICON_LINK)
}
pub fn attachment() -> Self {
Self::new(ICON_ATTACH_FILE)
}
pub fn person() -> Self {
Self::new(ICON_PERSON)
}
pub fn group() -> Self {
Self::new(ICON_GROUP)
}
pub fn account_circle() -> Self {
Self::new(ICON_ACCOUNT_CIRCLE)
}
pub fn person_add() -> Self {
Self::new(ICON_PERSON_ADD)
}
pub fn login() -> Self {
Self::new(ICON_LOGIN)
}
pub fn logout() -> Self {
Self::new(ICON_LOGOUT)
}
pub fn email() -> Self {
Self::new(ICON_EMAIL)
}
pub fn chat() -> Self {
Self::new(ICON_CHAT)
}
pub fn message() -> Self {
Self::new(ICON_MESSAGE)
}
pub fn phone() -> Self {
Self::new(ICON_PHONE)
}
pub fn send() -> Self {
Self::new(ICON_SEND)
}
pub fn play() -> Self {
Self::new(ICON_PLAY_ARROW)
}
pub fn pause() -> Self {
Self::new(ICON_PAUSE)
}
pub fn stop() -> Self {
Self::new(ICON_STOP)
}
pub fn skip_next() -> Self {
Self::new(ICON_SKIP_NEXT)
}
pub fn skip_previous() -> Self {
Self::new(ICON_SKIP_PREVIOUS)
}
pub fn fast_forward() -> Self {
Self::new(ICON_FAST_FORWARD)
}
pub fn fast_rewind() -> Self {
Self::new(ICON_FAST_REWIND)
}
pub fn replay() -> Self {
Self::new(ICON_REPLAY)
}
pub fn shuffle() -> Self {
Self::new(ICON_SHUFFLE)
}
pub fn repeat_icon() -> Self {
Self::new(ICON_REPEAT)
}
pub fn volume_up() -> Self {
Self::new(ICON_VOLUME_UP)
}
pub fn volume_down() -> Self {
Self::new(ICON_VOLUME_DOWN)
}
pub fn volume_mute() -> Self {
Self::new(ICON_VOLUME_MUTE)
}
pub fn volume_off() -> Self {
Self::new(ICON_VOLUME_OFF)
}
pub fn smartphone() -> Self {
Self::new(ICON_SMARTPHONE)
}
pub fn tablet() -> Self {
Self::new(ICON_TABLET)
}
pub fn laptop() -> Self {
Self::new(ICON_LAPTOP)
}
pub fn desktop() -> Self {
Self::new(ICON_DESKTOP_WINDOWS)
}
pub fn keyboard() -> Self {
Self::new(ICON_KEYBOARD)
}
pub fn mouse() -> Self {
Self::new(ICON_MOUSE)
}
pub fn gamepad() -> Self {
Self::new(ICON_GAMEPAD)
}
pub fn wifi() -> Self {
Self::new(ICON_WIFI)
}
pub fn bluetooth() -> Self {
Self::new(ICON_BLUETOOTH)
}
pub fn battery_full() -> Self {
Self::new(ICON_BATTERY_FULL)
}
pub fn battery_alert() -> Self {
Self::new(ICON_BATTERY_ALERT)
}
pub fn dice() -> Self {
Self::new(ICON_CASINO)
}
pub fn puzzle() -> Self {
Self::new(ICON_EXTENSION)
}
pub fn shield() -> Self {
Self::new(ICON_SHIELD)
}
pub fn combat() -> Self {
Self::new(ICON_SPORTS_MARTIAL_ARTS)
}
pub fn magic() -> Self {
Self::new(ICON_AUTO_FIX_HIGH)
}
pub fn lightbulb() -> Self {
Self::new(ICON_LIGHTBULB)
}
pub fn inventory() -> Self {
Self::new(ICON_INVENTORY_2)
}
pub fn book() -> Self {
Self::new(ICON_BOOK)
}
pub fn mind() -> Self {
Self::new(ICON_PSYCHOLOGY)
}
pub fn strength() -> Self {
Self::new(ICON_FITNESS_CENTER)
}
pub fn speed() -> Self {
Self::new(ICON_SPEED)
}
pub fn health() -> Self {
Self::new(ICON_HEALING)
}
pub fn language() -> Self {
Self::new(ICON_LANGUAGE)
}
pub fn dark_mode() -> Self {
Self::new(ICON_DARK_MODE)
}
pub fn light_mode() -> Self {
Self::new(ICON_LIGHT_MODE)
}
pub fn fullscreen() -> Self {
Self::new(ICON_FULLSCREEN)
}
pub fn fullscreen_exit() -> Self {
Self::new(ICON_FULLSCREEN_EXIT)
}
pub fn zoom_in() -> Self {
Self::new(ICON_ZOOM_IN)
}
pub fn zoom_out() -> Self {
Self::new(ICON_ZOOM_OUT)
}
pub fn lock() -> Self {
Self::new(ICON_LOCK)
}
pub fn lock_open() -> Self {
Self::new(ICON_LOCK_OPEN)
}
pub fn tune() -> Self {
Self::new(ICON_TUNE)
}
pub fn filter() -> Self {
Self::new(ICON_FILTER_LIST)
}
pub fn sort() -> Self {
Self::new(ICON_SORT)
}
pub fn drag_handle() -> Self {
Self::new(ICON_DRAG_HANDLE)
}
pub fn apps() -> Self {
Self::new(ICON_APPS)
}
pub fn list_view() -> Self {
Self::new(ICON_VIEW_LIST)
}
pub fn grid_view() -> Self {
Self::new(ICON_VIEW_MODULE)
}
pub fn clock() -> Self {
Self::new(ICON_SCHEDULE)
}
pub fn calendar() -> Self {
Self::new(ICON_EVENT)
}
pub fn today() -> Self {
Self::new(ICON_TODAY)
}
}
#[derive(Bundle, Default)]
pub struct IconBundle {
pub icon: MaterialIcon,
pub style: IconStyle,
pub text: Text,
pub node: Node,
}
impl IconBundle {
pub fn new(icon: MaterialIcon) -> Self {
Self {
icon,
text: Text::new(icon.as_str()),
..default()
}
}
pub fn with_style(mut self, style: IconStyle) -> Self {
self.style = style;
self
}
pub fn with_size(mut self, size: f32) -> Self {
self.style = self.style.with_size(size);
self.node.width = Val::Px(size);
self.node.height = Val::Px(size);
self
}
pub fn with_color(mut self, color: Color) -> Self {
self.style = self.style.with_color(color);
self
}
}
pub struct IconPlugin;
impl Plugin for IconPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PostUpdate, sync_icon_render_components);
}
}
fn sync_icon_render_components(
icon_font: Option<Res<MaterialIconFont>>,
mut fonts: ResMut<Assets<Font>>,
mut commands: Commands,
mut query: Query<(
Entity,
&MaterialIcon,
&IconStyle,
Option<&Node>,
Option<&mut Text>,
Option<&mut TextFont>,
Option<&mut TextColor>,
)>,
) {
let mut created_icon_font_this_frame = false;
let ensured_font_handle: Option<Handle<Font>> = match icon_font.as_ref() {
Some(font) => Some(font.0.clone()),
None => {
let font = Font::try_from_bytes(EMBEDDED_MATERIAL_SYMBOLS_FONT.to_vec())
.expect("Failed to load embedded icon font (legacy compatibility)");
let font_handle = fonts.add(font);
commands.insert_resource(MaterialIconFont(font_handle.clone()));
created_icon_font_this_frame = true;
Some(font_handle)
}
};
let icon_font_changed =
created_icon_font_this_frame || icon_font.as_ref().is_some_and(|font| font.is_changed());
for (entity, icon, style, node, text, text_font, text_color) in query.iter_mut() {
let desired_text = Text::new(icon.as_str());
let desired_size = style.effective_size();
if !icon_font_changed && node.is_some() && text.is_some() {
let has_text_font = text_font.is_some();
let font_matches = match (&ensured_font_handle, &text_font) {
(Some(expected), Some(current)) => current.font == *expected,
(None, Some(_)) => true,
_ => false,
};
if has_text_font && font_matches {
if style.color.is_none() || text_color.is_some() {
continue;
}
}
}
if node.is_none() {
commands.entity(entity).insert(Node {
width: Val::Px(desired_size),
height: Val::Px(desired_size),
..default()
});
}
match text {
Some(mut text) => {
*text = desired_text;
}
None => {
commands.entity(entity).insert(desired_text);
}
}
match text_font {
Some(mut text_font) => {
if let Some(icon_font) = &ensured_font_handle {
text_font.font = icon_font.clone();
}
text_font.font_size = desired_size;
}
None => {
if let Some(icon_font) = &ensured_font_handle {
commands.entity(entity).insert(TextFont {
font: icon_font.clone(),
font_size: desired_size,
..default()
});
}
}
}
if let Some(color) = style.color {
match text_color {
Some(mut text_color) => text_color.0 = color,
None => {
commands.entity(entity).insert(TextColor(color));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_icon_creation() {
let icon = MaterialIcon::home();
assert_eq!(icon.codepoint, ICON_HOME);
assert_eq!(icon.as_str(), ICON_HOME.to_string());
}
#[test]
fn test_icon_from_name() {
let icon = MaterialIcon::from_name("settings").unwrap();
assert_eq!(icon.codepoint, ICON_SETTINGS);
let none = MaterialIcon::from_name("nonexistent");
assert!(none.is_none());
}
#[test]
fn test_icon_bundle() {
let bundle = IconBundle::new(MaterialIcon::search())
.with_size(24.0)
.with_color(Color::WHITE);
assert_eq!(bundle.icon.codepoint, ICON_SEARCH);
assert_eq!(bundle.style.effective_size(), 24.0);
assert_eq!(bundle.style.color, Some(Color::WHITE));
}
#[test]
fn test_all_icon_constructors() {
let icons = [
MaterialIcon::home(),
MaterialIcon::menu(),
MaterialIcon::settings(),
MaterialIcon::search(),
MaterialIcon::delete(),
MaterialIcon::add(),
MaterialIcon::close(),
MaterialIcon::check(),
MaterialIcon::error(),
MaterialIcon::warning(),
MaterialIcon::play(),
MaterialIcon::pause(),
MaterialIcon::dice(),
MaterialIcon::shield(),
];
for icon in icons {
assert!(icon.codepoint as u32 > 0);
assert!(!icon.as_str().is_empty());
}
}
}