diff --git a/README.md b/README.md
index a4429b7..42b7093 100644
--- a/README.md
+++ b/README.md
@@ -45,12 +45,12 @@ See `examples` for more usage patterns!
### Supported Tags
-| Tag | Usage |
-| ------------------------ | ----------------------------------------------------------------------------------------- |
-| `[b]bold[/b]` | Bold text |
-| `[i]italic[/i]` | Italic text |
-| `[c=#ff00ff]colored[/c]` | Colored text |
-| `[m=foo]test[/m]` | Add a marker component to the `Text` "test", registered via `BbcodeSettings::with_marker` |
+- `b`: \[b]**bold**\[/b] text
+- `i`: \[i]*italic*\[/i] text
+- `c`: \[c=\#ff0000]colored \[/c] text
+ - Register named colors via `ResMut` and use the names instead of hex values
+- `m`: \[m=foo]text with marker component\[/m]
+ - Register marker components via `BbcodeSettings::with_marker` and use them to update text dynamically
## License
diff --git a/examples/dynamic.rs b/examples/dynamic.rs
index 492496f..6326847 100644
--- a/examples/dynamic.rs
+++ b/examples/dynamic.rs
@@ -1,9 +1,13 @@
//! This example demonstrates how parts of the text can be efficiently updated dynamically.
-//! To do this, we use the special `[m]` tag, which allows us to assign a marker component to the contained text.
-//! We can then query for the marker component as usual and apply our edits.
+//!
+//! - To update the text content, we use the `[m]` tag.
+//! It allows us to assign a marker component to the contained text,
+//! which we can then update using queries as usual.
+//! - To update the text color, we use the `[c]` tag with named colors.
+//! We simply update the color for the given name and it updates everywhere.
use bevy::prelude::*;
-use bevy_mod_bbcode::{BbcodeBundle, BbcodePlugin, BbcodeSettings};
+use bevy_mod_bbcode::{BbcodeBundle, BbcodePlugin, BbcodeSettings, ColorMap};
#[derive(Component, Clone)]
struct TimeMarker;
@@ -12,7 +16,7 @@ fn main() {
App::new()
.add_plugins((DefaultPlugins, BbcodePlugin::new().with_fonts("fonts")))
.add_systems(Startup, setup)
- .add_systems(Update, update)
+ .add_systems(Update, (update_text, update_color))
.run();
}
@@ -20,16 +24,22 @@ fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
commands.spawn(BbcodeBundle::from_content(
- "Time passed: [m=time]0.0[/m] s",
+ "Time passed: [m=time]0.0[/m] s with [c=rainbow]rainbow[/c]",
BbcodeSettings::new("Fira Sans", 40., Color::WHITE)
// Register the marker component for the `m=time` tag
.with_marker("time", TimeMarker),
));
}
-fn update(time: Res, mut query: Query<&mut Text, With>) {
+fn update_text(time: Res, mut query: Query<&mut Text, With>) {
for mut text in query.iter_mut() {
// We can directly query for the `Text` component and update it, without the BBCode being parsed again
text.sections[0].value = format!("{:.0}", time.elapsed_seconds());
}
}
+
+fn update_color(time: Res, mut color_map: ResMut) {
+ let hue = (time.elapsed_seconds() * 20.) % 360.;
+ // Updating a value in the color map will update that color wherever the same name is used!
+ color_map.insert("rainbow", Hsva::hsv(hue, 1., 1.));
+}
diff --git a/src/bevy/bbcode.rs b/src/bevy/bbcode.rs
index 27b6f26..bdd7977 100644
--- a/src/bevy/bbcode.rs
+++ b/src/bevy/bbcode.rs
@@ -2,6 +2,8 @@ use std::sync::Arc;
use bevy::{ecs::system::EntityCommands, prelude::*, ui::FocusPolicy, utils::HashMap};
+use super::color::BbCodeColor;
+
#[derive(Debug, Clone, Component, Default)]
pub struct Bbcode {
@@ -20,17 +22,21 @@ pub(crate) struct Modifiers {
pub struct BbcodeSettings {
pub font_family: String,
pub font_size: f32,
- pub color: Color,
+ pub color: BbCodeColor,
pub(crate) modifiers: Modifiers,
}
impl BbcodeSettings {
- pub fn new>(font_family: F, font_size: f32, color: Color) -> Self {
+ pub fn new, C: Into>(
+ font_family: F,
+ font_size: f32,
+ color: C,
+ ) -> Self {
Self {
font_family: font_family.into(),
font_size,
- color,
+ color: color.into(),
modifiers: Default::default(),
}
}
diff --git a/src/bevy/color.rs b/src/bevy/color.rs
new file mode 100644
index 0000000..661614b
--- /dev/null
+++ b/src/bevy/color.rs
@@ -0,0 +1,119 @@
+use bevy::{
+ prelude::*,
+ utils::{HashMap, HashSet},
+};
+
+pub struct ColorPlugin;
+
+impl Plugin for ColorPlugin {
+ fn build(&self, app: &mut App) {
+ app.init_resource::()
+ .add_systems(Update, update_colors);
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum BbCodeColor {
+ Named(String),
+ Static(Color),
+}
+
+impl BbCodeColor {
+ pub fn to_color(&self, color_map: &ColorMap) -> Option {
+ match self {
+ Self::Static(color) => Some(*color),
+ Self::Named(name) => color_map.get(name),
+ }
+ }
+}
+
+impl From for BbCodeColor {
+ fn from(value: Color) -> Self {
+ Self::Static(value)
+ }
+}
+
+impl From for BbCodeColor {
+ fn from(value: String) -> Self {
+ Self::Named(value)
+ }
+}
+
+#[derive(Debug, Resource, Default)]
+pub struct ColorMap {
+ /// The map from name to color.
+ map: HashMap,
+
+ /// Internal tracker for names where the corresponding color has been updated.
+ ///
+ /// Used to only update what's needed.
+ was_updated: HashSet,
+}
+
+impl ColorMap {
+ /// Insert (add or update) a new named color.
+ ///
+ /// Returns `&mut self` for chaining.
+ pub fn insert(&mut self, name: N, color: C) -> &mut Self
+ where
+ N: Into,
+ C: Into,
+ {
+ let name = name.into();
+ self.map.insert(name.clone(), color.into());
+ self.was_updated.insert(name);
+ self
+ }
+
+ /// Get the color for the given name.
+ pub fn get(&self, name: &str) -> Option {
+ self.map.get(name).copied()
+ }
+
+ /// Determine if any color has been updated.
+ pub(crate) fn has_update(&self) -> bool {
+ !self.was_updated.is_empty()
+ }
+
+ /// Determine if the color with the given name has been updated, and if yes to which value.
+ ///
+ /// You should probably call [`ColorMap::clear_was_updated`] at some point afterwards.
+ pub(crate) fn get_update(&self, name: &str) -> Option {
+ if self.was_updated.contains(name) {
+ self.map.get(name).copied()
+ } else {
+ None
+ }
+ }
+
+ /// Clear the tracker for the color names which had their values updated.
+ pub(crate) fn clear_was_updated(&mut self) {
+ self.was_updated.clear();
+ }
+}
+
+/// Tracker for text that's colored via named BBCode components.
+#[derive(Debug, Component)]
+pub struct BbCodeColored {
+ pub name: String,
+}
+
+/// Update all colors whose name has changed.
+fn update_colors(
+ mut color_map: ResMut,
+ mut colored_text_query: Query<(&BbCodeColored, &mut Text)>,
+) {
+ if !color_map.is_changed() || !color_map.has_update() {
+ return;
+ }
+
+ for (colored, mut text) in colored_text_query.iter_mut() {
+ if let Some(color) = color_map.get_update(&colored.name) {
+ for section in &mut text.sections {
+ section.style.color = color;
+ }
+ }
+ }
+
+ color_map.clear_was_updated();
+}
diff --git a/src/bevy/conversion.rs b/src/bevy/conversion.rs
index 4741ae8..eaba0f7 100644
--- a/src/bevy/conversion.rs
+++ b/src/bevy/conversion.rs
@@ -6,7 +6,9 @@ use crate::bbcode::{parser::parse_bbcode, BbcodeNode, BbcodeTag};
use super::{
bbcode::{Bbcode, BbcodeSettings},
+ color::{BbCodeColor, BbCodeColored},
font::FontRegistry,
+ ColorMap,
};
#[derive(Debug, Clone)]
@@ -16,7 +18,7 @@ struct BbcodeContext {
/// Whether the text should be written *italic*.
is_italic: bool,
/// The color of the text.
- color: Color,
+ color: BbCodeColor,
/// Marker components to apply to the spawned `Text`s.
markers: Vec,
@@ -37,13 +39,16 @@ impl BbcodeContext {
"c" | "color" => {
if let Some(color) = tag.simple_param() {
if let Ok(color) = Srgba::hex(color.trim()) {
+ let color: Color = color.into();
Self {
color: color.into(),
..self.clone()
}
} else {
- warn!("Invalid bbcode color {color}");
- self.clone()
+ Self {
+ color: color.clone().into(),
+ ..self.clone()
+ }
}
} else {
warn!("Missing bbcode color on [{}] tag", tag.name());
@@ -73,6 +78,7 @@ pub fn convert_bbcode(
mut commands: Commands,
bbcode_query: Query<(Entity, Ref, Ref)>,
font_registry: Res,
+ color_map: Res,
) {
for (entity, bbcode, settings) in bbcode_query.iter() {
if !bbcode.is_changed() && !settings.is_changed() && !font_registry.is_changed() {
@@ -99,12 +105,13 @@ pub fn convert_bbcode(
BbcodeContext {
is_bold: false,
is_italic: false,
- color: settings.color,
+ color: settings.color.clone(),
markers: Vec::new(),
},
&settings,
&nodes,
font_registry.as_ref(),
+ color_map.as_ref(),
)
}
}
@@ -115,6 +122,7 @@ fn construct_recursively(
settings: &BbcodeSettings,
nodes: &Vec>,
font_registry: &FontRegistry,
+ color_map: &ColorMap,
) {
for node in nodes {
match **node {
@@ -141,10 +149,15 @@ fn construct_recursively(
TextStyle {
font,
font_size: settings.font_size,
- color: context.color,
+ color: context.color.to_color(color_map).unwrap_or(Color::WHITE),
},
));
+ // Track named colors for efficient update
+ if let BbCodeColor::Named(name) = &context.color {
+ text_commands.insert(BbCodeColored { name: name.clone() });
+ }
+
// Apply marker components
for marker in &context.markers {
if let Some(modifier) = settings.modifiers.modifier_map.get(marker) {
@@ -160,6 +173,7 @@ fn construct_recursively(
settings,
tag.children(),
font_registry,
+ color_map,
),
}
}
diff --git a/src/bevy/mod.rs b/src/bevy/mod.rs
index 99fcfbc..9ef0fa6 100644
--- a/src/bevy/mod.rs
+++ b/src/bevy/mod.rs
@@ -1,8 +1,10 @@
pub(crate) mod bbcode;
+pub(crate) mod color;
pub(crate) mod conversion;
pub(crate) mod font;
pub(crate) mod plugin;
pub use bbcode::{Bbcode, BbcodeBundle, BbcodeSettings};
+pub use color::ColorMap;
pub use font::*;
pub use plugin::BbcodePlugin;
diff --git a/src/bevy/plugin.rs b/src/bevy/plugin.rs
index e7da9f1..45c3f05 100644
--- a/src/bevy/plugin.rs
+++ b/src/bevy/plugin.rs
@@ -3,7 +3,7 @@ use bevy::{
prelude::*,
};
-use super::{conversion::convert_bbcode, font::FontPlugin};
+use super::{color::ColorPlugin, conversion::convert_bbcode, font::FontPlugin};
#[derive(Debug, Default)]
pub struct BbcodePlugin {
@@ -31,7 +31,7 @@ impl BbcodePlugin {
impl Plugin for BbcodePlugin {
fn build(&self, app: &mut App) {
- app.add_plugins(FontPlugin)
+ app.add_plugins((FontPlugin, ColorPlugin))
.add_systems(Update, convert_bbcode);
let asset_server = app.world().resource::();