use std::iter;
use bevy::{
app::{Plugin, Update},
ecs::{
component::Component,
entity::Entity,
hierarchy::{ChildOf, Children},
query::Changed,
world::World,
},
platform::collections::HashMap,
prelude::{
Deref, DerefMut, DetectChanges, DetectChangesMut, FromWorld, IntoScheduleConfigs, Mut, Or,
Query, RemovedComponents, Res, ResMut, Resource, SystemSet, Text, Text2d, With,
},
text::TextSpan,
};
use parser::parse_richtext;
pub mod prelude {
pub use crate::{RichText, RichText2d, RichTextPlugin, StyleTag, StyleTags};
}
mod parser;
#[derive(Component)]
#[require(Text)]
pub struct RichText(pub String);
impl RichText {
pub fn new(markup: impl Into<String>) -> Self {
Self(markup.into())
}
}
#[derive(Component)]
#[require(Text2d)]
pub struct RichText2d(pub String);
impl RichText2d {
pub fn new(markup: impl Into<String>) -> Self {
Self(markup.into())
}
}
#[derive(Component)]
pub struct StyleTag(pub String);
impl StyleTag {
pub fn new(tag: impl Into<String>) -> Self {
Self(tag.into())
}
}
impl Default for StyleTag {
fn default() -> Self {
Self("".into())
}
}
#[derive(Resource, Deref, DerefMut)]
pub struct StyleTags(pub HashMap<String, Entity>);
impl StyleTags {
pub fn get_default(&self) -> &Entity {
&self.0[""]
}
pub fn get_or_default(&self, tag: &str) -> &Entity {
self.0.get(tag).unwrap_or_else(|| self.get_default())
}
}
impl FromWorld for StyleTags {
fn from_world(world: &mut World) -> Self {
Self(HashMap::from_iter([(
"".to_string(),
world.spawn((DefaultStyle, StyleTag::new(""))).id(),
)]))
}
}
#[derive(Component)]
pub struct DefaultStyle;
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct RichTextSystems;
pub struct RichTextPlugin;
impl Plugin for RichTextPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.init_resource::<StyleTags>();
app.add_systems(
Update,
(registry_changed, sync_registry, richtext_changed)
.chain()
.in_set(RichTextSystems),
);
}
}
fn sync_registry(
changed: Query<(Entity, &StyleTag), Changed<StyleTag>>,
all: Query<(), With<StyleTag>>,
mut removed: RemovedComponents<StyleTag>,
mut registry: ResMut<StyleTags>,
) {
for ent in removed.read() {
registry.0.retain(|_, v| *v != ent);
}
if changed.is_empty() {
return;
}
for (ent, style) in &changed {
registry.0.insert(style.0.clone(), ent);
}
registry.0.retain(|_, v| all.get(*v).is_ok());
}
fn registry_changed(registry: Res<StyleTags>, mut rt_query: Query<Mut<RichText>>) {
if !registry.is_changed() {
return;
}
for mut rt in &mut rt_query {
rt.set_changed();
}
}
fn richtext_changed(world: &mut World) {
let mut ents_query =
world.query_filtered::<Entity, Or<(Changed<RichText>, Changed<RichText2d>)>>();
let ents = ents_query.iter(world).collect::<Vec<_>>();
if ents.is_empty() {
return;
}
let mut rt_query = world.query::<&RichText>();
let mut rt_2d_query = world.query::<&RichText2d>();
world.resource_scope(|world, registry: Mut<StyleTags>| {
for ent in ents {
world.commands().entity(ent).despawn_related::<Children>();
world.flush();
let Ok(rt) = rt_query
.get(world, ent)
.map(|rt| &rt.0)
.or_else(|_| rt_2d_query.get(world, ent).map(|rt| &rt.0))
else {
continue;
};
let parsed = parse_richtext(rt);
for section in parsed {
let mut tags = vec!["".to_string()];
tags.extend(section.tags);
let span_ent = world.spawn(TextSpan::new(section.value.clone())).id();
world.entity_mut(ent).add_child(span_ent);
let empty_tags = iter::once("");
for tag in empty_tags.chain(tags.iter().map(|t| t.as_str())) {
let style_ent = registry.get_or_default(tag);
world
.commands()
.entity(*style_ent)
.clone_with_opt_out(span_ent, |builder| {
builder.deny::<(StyleTag, DefaultStyle, ChildOf)>();
});
}
}
}
});
}