use std::fmt;
use std::sync::{
mpsc::{self, Receiver, SyncSender},
Mutex,
};
use bevy::{prelude::*, utils::HashMap};
use lazy_static::lazy_static;
use crate::block::Blocks;
const MAX_LINES: usize = 4096;
lazy_static! {
#[doc(hidden)]
pub static ref COMMAND_CHANNELS: CommandChannels = {
let (sender, receiver) = mpsc::sync_channel(MAX_LINES);
CommandChannels {
sender,
receiver: Mutex::new(receiver),
}
};
}
#[macro_export]
macro_rules! screen_print {
(col: $color:expr, $text:expr $(, $fmt_args:expr)*) => {
screen_print!(@impl sec: 7.0, col: Some($color), $text $(, $fmt_args)*);
};
(sec: $timeout:expr, col: $color:expr, $text:expr $(, $fmt_args:expr)*) => {
screen_print!(@impl sec: $timeout, col: Some($color), $text $(, $fmt_args)*);
};
(sec: $timeout:expr, $text:expr $(, $fmt_args:expr)*) => {
screen_print!(@impl sec: $timeout, col: None, $text $(, $fmt_args)*);
};
($text:expr $(, $fmt_args:expr)*) => {
screen_print!(@impl sec: 7.0, col: None, $text $(, $fmt_args)*);
};
(@impl sec: $timeout:expr, col: $color:expr, $text:expr $(, $fmt_args:expr)*) => {{
use $crate::{InvocationSiteKey, COMMAND_CHANNELS};
let key = InvocationSiteKey { file: file!(), line: line!(), column: column!() };
COMMAND_CHANNELS.add_text(key, || format!($text $(, $fmt_args)*), $timeout as f64, $color);
}};
}
#[derive(Hash, PartialEq, Eq)]
#[doc(hidden)]
pub struct InvocationSiteKey {
pub file: &'static str,
pub line: u32,
pub column: u32,
}
impl fmt::Display for InvocationSiteKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}:{}:{}]", self.file, self.line, self.column)
}
}
enum Command {
Refresh {
key: InvocationSiteKey,
color: Option<Color>,
text: String,
timeout: f64,
},
}
#[doc(hidden)]
pub struct CommandChannels {
sender: SyncSender<Command>,
receiver: Mutex<Receiver<Command>>,
}
impl CommandChannels {
pub fn add_text(
&self,
key: InvocationSiteKey,
text: impl FnOnce() -> String,
timeout: f64,
color: Option<Color>,
) {
let text = format!("{key} {}\n", text());
let cmd = Command::Refresh { text, key, color, timeout };
self.sender
.try_send(cmd)
.expect("Number of debug messages exceeds limit!");
}
}
#[derive(Component)]
struct Message {
expiration: f64,
}
impl Message {
fn new(expiration: f64) -> Self {
Self { expiration }
}
}
struct Options {
font: Option<&'static str>,
font_size: f32,
color: Color,
}
impl<'a> From<&'a OverlayPlugin> for Options {
fn from(plugin: &'a OverlayPlugin) -> Self {
Self {
font: plugin.font,
color: plugin.fallback_color,
font_size: plugin.font_size,
}
}
}
struct OverlayFont(Handle<Font>);
impl FromWorld for OverlayFont {
fn from_world(world: &mut World) -> Self {
let options = world.get_resource::<Options>().unwrap();
let assets = world.get_resource::<AssetServer>().unwrap();
let font = match options.font {
Some(font) => assets.load(font),
#[cfg(not(feature = "builtin-font"))]
None => panic!(
"No default font supplied, please either set the `builtin-font` \
flag or provide your own font file by setting the `font` field of \
`OverlayPlugin` to `Some(thing)`"
),
#[cfg(feature = "builtin-font")]
None => world.get_resource_mut::<Assets<Font>>().unwrap().add(
Font::try_from_bytes(include_bytes!("screen_debug_text.ttf").to_vec())
.expect("The hardcoded builtin font is valid, this should never fail."),
),
};
Self(font)
}
}
fn update_messages_as_per_commands(
mut messages: Query<(&mut Text, &mut Message)>,
mut key_entities: Local<HashMap<InvocationSiteKey, Entity>>,
mut cmds: Commands,
time: Res<Time>,
options: Res<Options>,
font: Res<OverlayFont>,
) {
let channels = &COMMAND_CHANNELS;
let text_style = |color| TextStyle {
color,
font: font.0.clone(),
font_size: options.font_size,
};
let text_align = TextAlignment {
horizontal: HorizontalAlign::Left,
..Default::default()
};
let current_time = time.seconds_since_startup();
let iterator = channels.receiver.lock().unwrap();
for Command::Refresh { key, color, text, timeout } in iterator.try_iter() {
let color = color.unwrap_or(options.color);
if let Some(&entity) = key_entities.get(&key) {
if let Ok((mut ui_text, mut message)) = messages.get_mut(entity) {
message.expiration = timeout + current_time;
if ui_text.sections[0].style.color != color {
ui_text.sections[0].style.color = color;
}
if ui_text.sections[0].value != text {
ui_text.sections[0].value = text;
}
}
} else {
let entity = cmds
.spawn_bundle(TextBundle {
text: Text::with_section(text, text_style(color), text_align),
style: Style {
position_type: PositionType::Absolute,
..Default::default()
},
visibility: Visibility { is_visible: false },
..Default::default()
})
.insert(Message::new(timeout + current_time))
.id();
key_entities.insert(key, entity);
}
}
}
fn layout_messages(
mut messages: Query<(Entity, &mut Style, &mut Visibility, &Node, &Message)>,
mut line_sizes: Local<Blocks<Entity, f32>>,
time: Res<Time>,
) {
for (entity, mut style, mut vis, Node { size }, text) in messages.iter_mut() {
let is_expired = text.expiration < time.seconds_since_startup();
if vis.is_visible == is_expired {
vis.is_visible = !is_expired;
if !is_expired {
style.position.left = Val::Px(0.0);
let offset = line_sizes.insert_size(entity, size.y);
style.position.top = Val::Px(offset);
} else {
line_sizes.remove(entity);
}
}
}
}
pub struct OverlayPlugin {
pub font: Option<&'static str>,
pub fallback_color: Color,
pub font_size: f32,
}
impl Default for OverlayPlugin {
fn default() -> Self {
Self {
font: None,
fallback_color: Color::YELLOW,
font_size: 13.0,
}
}
}
impl Plugin for OverlayPlugin {
fn build(&self, app: &mut App) {
app.insert_resource::<Options>(self.into())
.init_resource::<OverlayFont>()
.add_system(layout_messages.after("update_line"))
.add_system(update_messages_as_per_commands.label("update_line"));
}
}