Skip to content

Commit

Permalink
Enable the use of named colors (#10)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
TimJentzsch authored Jul 27, 2024
1 parent 994518c commit fcec8aa
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 22 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]<span style="color: red">colored</span>\[/c] text
- Register named colors via `ResMut<ColorMap>` 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

Expand Down
22 changes: 16 additions & 6 deletions examples/dynamic.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,24 +16,30 @@ 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();
}

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<Time>, mut query: Query<&mut Text, With<TimeMarker>>) {
fn update_text(time: Res<Time>, mut query: Query<&mut Text, With<TimeMarker>>) {
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<Time>, mut color_map: ResMut<ColorMap>) {
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.));
}
12 changes: 9 additions & 3 deletions src/bevy/bbcode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<F: Into<String>>(font_family: F, font_size: f32, color: Color) -> Self {
pub fn new<F: Into<String>, C: Into<BbCodeColor>>(
font_family: F,
font_size: f32,
color: C,
) -> Self {
Self {
font_family: font_family.into(),
font_size,
color,
color: color.into(),
modifiers: Default::default(),
}
}
Expand Down
119 changes: 119 additions & 0 deletions src/bevy/color.rs
Original file line number Diff line number Diff line change
@@ -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::<ColorMap>()
.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<Color> {
match self {
Self::Static(color) => Some(*color),
Self::Named(name) => color_map.get(name),
}
}
}

impl From<Color> for BbCodeColor {
fn from(value: Color) -> Self {
Self::Static(value)
}
}

impl From<String> 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<String, Color>,

/// Internal tracker for names where the corresponding color has been updated.
///
/// Used to only update what's needed.
was_updated: HashSet<String>,
}

impl ColorMap {
/// Insert (add or update) a new named color.
///
/// Returns `&mut self` for chaining.
pub fn insert<N, C>(&mut self, name: N, color: C) -> &mut Self
where
N: Into<String>,
C: Into<Color>,
{
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<Color> {
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<Color> {
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<ColorMap>,
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();
}
24 changes: 19 additions & 5 deletions src/bevy/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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<String>,
Expand All @@ -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());
Expand Down Expand Up @@ -73,6 +78,7 @@ pub fn convert_bbcode(
mut commands: Commands,
bbcode_query: Query<(Entity, Ref<Bbcode>, Ref<BbcodeSettings>)>,
font_registry: Res<FontRegistry>,
color_map: Res<ColorMap>,
) {
for (entity, bbcode, settings) in bbcode_query.iter() {
if !bbcode.is_changed() && !settings.is_changed() && !font_registry.is_changed() {
Expand All @@ -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(),
)
}
}
Expand All @@ -115,6 +122,7 @@ fn construct_recursively(
settings: &BbcodeSettings,
nodes: &Vec<Arc<BbcodeNode>>,
font_registry: &FontRegistry,
color_map: &ColorMap,
) {
for node in nodes {
match **node {
Expand All @@ -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) {
Expand All @@ -160,6 +173,7 @@ fn construct_recursively(
settings,
tag.children(),
font_registry,
color_map,
),
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/bevy/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 2 additions & 2 deletions src/bevy/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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::<AssetServer>();
Expand Down

0 comments on commit fcec8aa

Please sign in to comment.