#![doc = include_str!("../docs/pretty.txt")]
#![doc = include_str!("../docs/effect.txt")]
use std::borrow::Cow;
use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use crate::material::{DynMaterialRegistry, ErasedPrettyTextMaterial};
use crate::parser::{Modifier, Modifiers};
pub trait PrettyTextEffectAppExt {
fn register_pretty_effect<T: Default + DynamicEffect>(
&mut self,
tag: &'static str,
) -> &mut Self;
}
impl PrettyTextEffectAppExt for App {
fn register_pretty_effect<T: Default + DynamicEffect>(
&mut self,
tag: &'static str,
) -> &mut Self {
self.add_systems(
PreStartup,
move |mut registry: ResMut<DynEffectRegistry>| {
registry.register(tag, T::default());
},
)
}
}
pub trait DynamicEffect: Send + Sync + 'static {
fn insert_from_args(
&self,
args: &[Cow<'static, str>],
entity: &mut EntityCommands,
) -> Result<()>;
}
#[derive(Debug, Clone, Reflect)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
pub struct PrettyTextEffect {
pub tag: Cow<'static, str>,
pub args: Vec<Cow<'static, str>>,
}
#[derive(Default, Resource)]
pub struct DynEffectRegistry(HashMap<&'static str, Box<dyn DynamicEffect>>);
impl std::fmt::Debug for DynEffectRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("DynEffectRegistry")
.field(&self.0.keys())
.finish()
}
}
impl DynEffectRegistry {
#[inline]
pub fn register(&mut self, tag: &'static str, effect: impl DynamicEffect) {
if self.0.get(tag).is_some() {
error!("effect `{tag}` is already registered");
}
self.0.insert(tag, Box::new(effect));
}
#[inline]
pub fn unregister(&mut self, tag: &'static str) {
self.0.remove(tag);
}
#[inline]
pub fn get(&self, tag: &str) -> Option<&dyn DynamicEffect> {
self.0.get(tag).map(|mat| mat.as_ref())
}
}
#[cfg(feature = "proc-macro")]
impl quote::ToTokens for PrettyTextEffect {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
use quote::TokenStreamExt;
let tag = &self.tag;
let args = &self.args;
tokens.append_all(quote::quote! {
bevy_pretty_text::dynamic_effects::PrettyTextEffect {
tag: std::borrow::Cow::Borrowed(#tag),
args: vec![#(std::borrow::Cow::Borrowed(#args),)*]
}
});
}
}
pub(crate) fn text_effect(
trigger: Trigger<OnAdd, Modifiers>,
mut commands: Commands,
material_registry: Res<DynMaterialRegistry>,
effects_registry: Res<DynEffectRegistry>,
mods: Query<&Modifiers>,
) -> Result {
let mods = mods.get(trigger.target())?;
let mut material = None;
for effect in mods.0.iter().filter_map(|m| match m {
Modifier::Effect(effect) => Some(effect),
_ => None,
}) {
if material_registry.get(effect.tag.as_ref()).is_some() {
if let Some(other) = material {
return Err(format!(
"registered multiple materials on a single span: `{}` and `{}`",
other, effect.tag
)
.into());
}
material = Some(effect.tag.as_ref());
commands
.entity(trigger.target())
.insert(ErasedPrettyTextMaterial {
tag: effect.tag.clone(),
args: effect.args.clone(),
});
} else if let Some(handler) = effects_registry.0.get(effect.tag.as_ref()) {
handler.insert_from_args(&effect.args, &mut commands.entity(trigger.target()))?;
} else {
error!("effect `{}` is not registered", effect.tag);
}
}
Ok(())
}
#[cfg(test)]
mod test {
use bevy::prelude::*;
use crate::dynamic_effects::PrettyTextEffect;
use crate::glyph::Glyph;
use crate::parser::{Modifier, Modifiers};
use crate::test::{prepare_app, run, run_tests};
use super::{DynamicEffect, PrettyTextEffectAppExt};
#[derive(Default, Component)]
struct Effect;
impl DynamicEffect for Effect {
fn insert_from_args(
&self,
args: &[std::borrow::Cow<'static, str>],
entity: &mut EntityCommands,
) -> Result<()> {
assert_eq!(args.len(), 2);
entity.insert(Effect);
Ok(())
}
}
#[test]
fn insert_effect() {
run_tests(
|| {
let mut app = prepare_app();
app.register_pretty_effect::<Effect>("effect");
app.world_mut().run_schedule(PreStartup);
app.world_mut().flush();
app
},
|app, entity, str| {
app.world_mut()
.entity_mut(entity)
.insert(Modifiers(vec![Modifier::Effect(PrettyTextEffect {
tag: "effect".into(),
args: vec!["1".into(), "2".into()],
})]));
app.world_mut().run_schedule(PostUpdate);
app.world_mut().flush();
run(app, move |effect: Query<&Effect>, glyphs: Query<&Glyph>| {
assert!(
effect.single().is_ok(),
"expected 1, got {}",
effect.iter().len()
);
assert_eq!(
glyphs.iter().len(),
str.chars().count(),
"expected {}, got {}",
str.chars().count(),
glyphs.iter().len()
);
});
app.world_mut().entity_mut(entity).despawn();
run(app, |effect: Query<&Effect>, glyphs: Query<&Glyph>| {
assert!(effect.is_empty(), "expected 0, got {}", effect.iter().len());
assert!(glyphs.is_empty(), "expected 0, got {}", glyphs.iter().len());
});
},
)
}
}