Skip to content

Commit fcec8aa

Browse files
authored
Enable the use of named colors (#10)
* Add infrastructure for named colors * Apply named colors on text * Fix color updates not being tracked * Update dynamic example with animated colors * Make tag reference more readable * Remove unused variable
1 parent 994518c commit fcec8aa

File tree

7 files changed

+173
-22
lines changed

7 files changed

+173
-22
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ See `examples` for more usage patterns!
4545

4646
### Supported Tags
4747

48-
| Tag | Usage |
49-
| ------------------------ | ----------------------------------------------------------------------------------------- |
50-
| `[b]bold[/b]` | Bold text |
51-
| `[i]italic[/i]` | Italic text |
52-
| `[c=#ff00ff]colored[/c]` | Colored text |
53-
| `[m=foo]test[/m]` | Add a marker component to the `Text` "test", registered via `BbcodeSettings::with_marker` |
48+
- `b`: \[b]**bold**\[/b] text
49+
- `i`: \[i]*italic*\[/i] text
50+
- `c`: \[c=\#ff0000]<span style="color: red">colored</span>\[/c] text
51+
- Register named colors via `ResMut<ColorMap>` and use the names instead of hex values
52+
- `m`: \[m=foo]text with marker component\[/m]
53+
- Register marker components via `BbcodeSettings::with_marker` and use them to update text dynamically
5454

5555
## License
5656

examples/dynamic.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
//! This example demonstrates how parts of the text can be efficiently updated dynamically.
2-
//! To do this, we use the special `[m]` tag, which allows us to assign a marker component to the contained text.
3-
//! We can then query for the marker component as usual and apply our edits.
2+
//!
3+
//! - To update the text content, we use the `[m]` tag.
4+
//! It allows us to assign a marker component to the contained text,
5+
//! which we can then update using queries as usual.
6+
//! - To update the text color, we use the `[c]` tag with named colors.
7+
//! We simply update the color for the given name and it updates everywhere.
48
59
use bevy::prelude::*;
6-
use bevy_mod_bbcode::{BbcodeBundle, BbcodePlugin, BbcodeSettings};
10+
use bevy_mod_bbcode::{BbcodeBundle, BbcodePlugin, BbcodeSettings, ColorMap};
711

812
#[derive(Component, Clone)]
913
struct TimeMarker;
@@ -12,24 +16,30 @@ fn main() {
1216
App::new()
1317
.add_plugins((DefaultPlugins, BbcodePlugin::new().with_fonts("fonts")))
1418
.add_systems(Startup, setup)
15-
.add_systems(Update, update)
19+
.add_systems(Update, (update_text, update_color))
1620
.run();
1721
}
1822

1923
fn setup(mut commands: Commands) {
2024
commands.spawn(Camera2dBundle::default());
2125

2226
commands.spawn(BbcodeBundle::from_content(
23-
"Time passed: [m=time]0.0[/m] s",
27+
"Time passed: [m=time]0.0[/m] s with [c=rainbow]rainbow[/c]",
2428
BbcodeSettings::new("Fira Sans", 40., Color::WHITE)
2529
// Register the marker component for the `m=time` tag
2630
.with_marker("time", TimeMarker),
2731
));
2832
}
2933

30-
fn update(time: Res<Time>, mut query: Query<&mut Text, With<TimeMarker>>) {
34+
fn update_text(time: Res<Time>, mut query: Query<&mut Text, With<TimeMarker>>) {
3135
for mut text in query.iter_mut() {
3236
// We can directly query for the `Text` component and update it, without the BBCode being parsed again
3337
text.sections[0].value = format!("{:.0}", time.elapsed_seconds());
3438
}
3539
}
40+
41+
fn update_color(time: Res<Time>, mut color_map: ResMut<ColorMap>) {
42+
let hue = (time.elapsed_seconds() * 20.) % 360.;
43+
// Updating a value in the color map will update that color wherever the same name is used!
44+
color_map.insert("rainbow", Hsva::hsv(hue, 1., 1.));
45+
}

src/bevy/bbcode.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::sync::Arc;
22

33
use bevy::{ecs::system::EntityCommands, prelude::*, ui::FocusPolicy, utils::HashMap};
44

5+
use super::color::BbCodeColor;
6+
57
#[derive(Debug, Clone, Component, Default)]
68

79
pub struct Bbcode {
@@ -20,17 +22,21 @@ pub(crate) struct Modifiers {
2022
pub struct BbcodeSettings {
2123
pub font_family: String,
2224
pub font_size: f32,
23-
pub color: Color,
25+
pub color: BbCodeColor,
2426

2527
pub(crate) modifiers: Modifiers,
2628
}
2729

2830
impl BbcodeSettings {
29-
pub fn new<F: Into<String>>(font_family: F, font_size: f32, color: Color) -> Self {
31+
pub fn new<F: Into<String>, C: Into<BbCodeColor>>(
32+
font_family: F,
33+
font_size: f32,
34+
color: C,
35+
) -> Self {
3036
Self {
3137
font_family: font_family.into(),
3238
font_size,
33-
color,
39+
color: color.into(),
3440
modifiers: Default::default(),
3541
}
3642
}

src/bevy/color.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
use bevy::{
2+
prelude::*,
3+
utils::{HashMap, HashSet},
4+
};
5+
6+
pub struct ColorPlugin;
7+
8+
impl Plugin for ColorPlugin {
9+
fn build(&self, app: &mut App) {
10+
app.init_resource::<ColorMap>()
11+
.add_systems(Update, update_colors);
12+
}
13+
}
14+
15+
#[derive(Debug, Clone)]
16+
pub enum BbCodeColor {
17+
Named(String),
18+
Static(Color),
19+
}
20+
21+
impl BbCodeColor {
22+
pub fn to_color(&self, color_map: &ColorMap) -> Option<Color> {
23+
match self {
24+
Self::Static(color) => Some(*color),
25+
Self::Named(name) => color_map.get(name),
26+
}
27+
}
28+
}
29+
30+
impl From<Color> for BbCodeColor {
31+
fn from(value: Color) -> Self {
32+
Self::Static(value)
33+
}
34+
}
35+
36+
impl From<String> for BbCodeColor {
37+
fn from(value: String) -> Self {
38+
Self::Named(value)
39+
}
40+
}
41+
42+
#[derive(Debug, Resource, Default)]
43+
pub struct ColorMap {
44+
/// The map from name to color.
45+
map: HashMap<String, Color>,
46+
47+
/// Internal tracker for names where the corresponding color has been updated.
48+
///
49+
/// Used to only update what's needed.
50+
was_updated: HashSet<String>,
51+
}
52+
53+
impl ColorMap {
54+
/// Insert (add or update) a new named color.
55+
///
56+
/// Returns `&mut self` for chaining.
57+
pub fn insert<N, C>(&mut self, name: N, color: C) -> &mut Self
58+
where
59+
N: Into<String>,
60+
C: Into<Color>,
61+
{
62+
let name = name.into();
63+
self.map.insert(name.clone(), color.into());
64+
self.was_updated.insert(name);
65+
self
66+
}
67+
68+
/// Get the color for the given name.
69+
pub fn get(&self, name: &str) -> Option<Color> {
70+
self.map.get(name).copied()
71+
}
72+
73+
/// Determine if any color has been updated.
74+
pub(crate) fn has_update(&self) -> bool {
75+
!self.was_updated.is_empty()
76+
}
77+
78+
/// Determine if the color with the given name has been updated, and if yes to which value.
79+
///
80+
/// You should probably call [`ColorMap::clear_was_updated`] at some point afterwards.
81+
pub(crate) fn get_update(&self, name: &str) -> Option<Color> {
82+
if self.was_updated.contains(name) {
83+
self.map.get(name).copied()
84+
} else {
85+
None
86+
}
87+
}
88+
89+
/// Clear the tracker for the color names which had their values updated.
90+
pub(crate) fn clear_was_updated(&mut self) {
91+
self.was_updated.clear();
92+
}
93+
}
94+
95+
/// Tracker for text that's colored via named BBCode components.
96+
#[derive(Debug, Component)]
97+
pub struct BbCodeColored {
98+
pub name: String,
99+
}
100+
101+
/// Update all colors whose name has changed.
102+
fn update_colors(
103+
mut color_map: ResMut<ColorMap>,
104+
mut colored_text_query: Query<(&BbCodeColored, &mut Text)>,
105+
) {
106+
if !color_map.is_changed() || !color_map.has_update() {
107+
return;
108+
}
109+
110+
for (colored, mut text) in colored_text_query.iter_mut() {
111+
if let Some(color) = color_map.get_update(&colored.name) {
112+
for section in &mut text.sections {
113+
section.style.color = color;
114+
}
115+
}
116+
}
117+
118+
color_map.clear_was_updated();
119+
}

src/bevy/conversion.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ use crate::bbcode::{parser::parse_bbcode, BbcodeNode, BbcodeTag};
66

77
use super::{
88
bbcode::{Bbcode, BbcodeSettings},
9+
color::{BbCodeColor, BbCodeColored},
910
font::FontRegistry,
11+
ColorMap,
1012
};
1113

1214
#[derive(Debug, Clone)]
@@ -16,7 +18,7 @@ struct BbcodeContext {
1618
/// Whether the text should be written *italic*.
1719
is_italic: bool,
1820
/// The color of the text.
19-
color: Color,
21+
color: BbCodeColor,
2022

2123
/// Marker components to apply to the spawned `Text`s.
2224
markers: Vec<String>,
@@ -37,13 +39,16 @@ impl BbcodeContext {
3739
"c" | "color" => {
3840
if let Some(color) = tag.simple_param() {
3941
if let Ok(color) = Srgba::hex(color.trim()) {
42+
let color: Color = color.into();
4043
Self {
4144
color: color.into(),
4245
..self.clone()
4346
}
4447
} else {
45-
warn!("Invalid bbcode color {color}");
46-
self.clone()
48+
Self {
49+
color: color.clone().into(),
50+
..self.clone()
51+
}
4752
}
4853
} else {
4954
warn!("Missing bbcode color on [{}] tag", tag.name());
@@ -73,6 +78,7 @@ pub fn convert_bbcode(
7378
mut commands: Commands,
7479
bbcode_query: Query<(Entity, Ref<Bbcode>, Ref<BbcodeSettings>)>,
7580
font_registry: Res<FontRegistry>,
81+
color_map: Res<ColorMap>,
7682
) {
7783
for (entity, bbcode, settings) in bbcode_query.iter() {
7884
if !bbcode.is_changed() && !settings.is_changed() && !font_registry.is_changed() {
@@ -99,12 +105,13 @@ pub fn convert_bbcode(
99105
BbcodeContext {
100106
is_bold: false,
101107
is_italic: false,
102-
color: settings.color,
108+
color: settings.color.clone(),
103109
markers: Vec::new(),
104110
},
105111
&settings,
106112
&nodes,
107113
font_registry.as_ref(),
114+
color_map.as_ref(),
108115
)
109116
}
110117
}
@@ -115,6 +122,7 @@ fn construct_recursively(
115122
settings: &BbcodeSettings,
116123
nodes: &Vec<Arc<BbcodeNode>>,
117124
font_registry: &FontRegistry,
125+
color_map: &ColorMap,
118126
) {
119127
for node in nodes {
120128
match **node {
@@ -141,10 +149,15 @@ fn construct_recursively(
141149
TextStyle {
142150
font,
143151
font_size: settings.font_size,
144-
color: context.color,
152+
color: context.color.to_color(color_map).unwrap_or(Color::WHITE),
145153
},
146154
));
147155

156+
// Track named colors for efficient update
157+
if let BbCodeColor::Named(name) = &context.color {
158+
text_commands.insert(BbCodeColored { name: name.clone() });
159+
}
160+
148161
// Apply marker components
149162
for marker in &context.markers {
150163
if let Some(modifier) = settings.modifiers.modifier_map.get(marker) {
@@ -160,6 +173,7 @@ fn construct_recursively(
160173
settings,
161174
tag.children(),
162175
font_registry,
176+
color_map,
163177
),
164178
}
165179
}

src/bevy/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
pub(crate) mod bbcode;
2+
pub(crate) mod color;
23
pub(crate) mod conversion;
34
pub(crate) mod font;
45
pub(crate) mod plugin;
56

67
pub use bbcode::{Bbcode, BbcodeBundle, BbcodeSettings};
8+
pub use color::ColorMap;
79
pub use font::*;
810
pub use plugin::BbcodePlugin;

src/bevy/plugin.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use bevy::{
33
prelude::*,
44
};
55

6-
use super::{conversion::convert_bbcode, font::FontPlugin};
6+
use super::{color::ColorPlugin, conversion::convert_bbcode, font::FontPlugin};
77

88
#[derive(Debug, Default)]
99
pub struct BbcodePlugin {
@@ -31,7 +31,7 @@ impl BbcodePlugin {
3131

3232
impl Plugin for BbcodePlugin {
3333
fn build(&self, app: &mut App) {
34-
app.add_plugins(FontPlugin)
34+
app.add_plugins((FontPlugin, ColorPlugin))
3535
.add_systems(Update, convert_bbcode);
3636

3737
let asset_server = app.world().resource::<AssetServer>();

0 commit comments

Comments
 (0)