Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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