//! Overlay display for debugging //! //! # Architecture Overview //! //! The implementation is as follow: //! * We have a static variable [`static@COMMAND_CHANNELS`] of type [`CommandChannels`] //! that contains channels for syncing [`Command`]s. //! * [`screen_print!`] secretly expands to a call of to that global variable, //! it simply pushes messages to the sender channel using //! [`CommandChannels::refresh_text`] method. This is why, `COMMAND_CHANNELS` is //! public. The end user code needs to be able to access it. But it is kept //! hidden thanks to the `#[doc(hidden)]` attribute. //! * The [`update_messages_as_per_commands`] system reads from the `receiver` //! channel of `COMMAND_CHANNELS` and updates or adds new debug message entities. //! For each [`Command::Refresh`], a line is updated or added, a refresh can change //! the text or the color, and will always update the [`Message::expiration`]. //! * The [`layout_messages`] system takes care of the layout (making sure to //! **NOT** move visible text, filling empty spaces, and hidding expirated //! messages). It uses the dumb 1D allocation algorithm specified in //! [`crate::block`]. //! //! ## Notes //! //! The channels use the `try_{send,iter}` methods to avoid blocking //! operations. Since we are already running parallel thanks to the bevy //! scheduler, there is no need for control-flow timing operations. //! //! Each individual invocation of [`screen_print!`] gets a unique //! [`InvocationSiteKey`], and a corresponding `Entity`. use std::fmt; use std::sync::mpsc::{self, Receiver, SyncSender}; use std::sync::{Mutex, OnceLock}; use bevy::{prelude::*, utils::HashMap}; use crate::block::Blocks; const MAX_LINES: usize = 4096; static COMMAND_CHANNELS: OnceLock = OnceLock::new(); #[doc(hidden)] pub fn command_channels() -> &'static CommandChannels { let (sender, receiver) = mpsc::sync_channel(MAX_LINES); COMMAND_CHANNELS.get_or_init(|| CommandChannels { sender, receiver: Mutex::new(receiver) }) } // TODO: better API? /// Display text on top left corner of the screen. /// /// The same `screen_print!` invocation can only have a single text displayed /// on screen at the same time, unless specified otherwise. /// /// # Limitations /// /// * Entity count: Entities used for displaying text are never despawned, /// so if at one point you have very many messages displayed at the same time, /// it might slow down afterward your game. Note that aready spawned entities /// are reused, so you need not fear leaks. /// * Max call per frame: at most 4096 messages can be printed per frame, /// exceeding that amount will panic. /// /// # Usage /// /// Call `screen_print!` like you would call any `format!`-style macros from /// the standard lib. /// /// You can also customize color and timeout, by adding prefix optional arguments /// (only supported in this order): /// /// 1. `push`: Do not overwrite previous text value. This allows /// printing multiple messages from the same macro call, you can use this /// in loops, or for messages that makes sense to duplicate on screen. /// Be advised! Using a `push` message once per frame will spam the log. /// 2. `sec: `: specify in seconds for how long the text shows up /// (default is 7 seconds) /// 3. `col: `: specify the color of the text. Default is /// `fallback_color` provided in `OverlayPlugin`, which itself defaults /// to yellow. /// /// ```rust,no_run /// use bevy_debug_text_overlay::{screen_print, OverlayPlugin}; /// use bevy::prelude::Color; /// /// let x = (13, 3.4, vec![1,2,3,4,5,6,7,8]); /// screen_print!("multiline: {x:#?}"); /// screen_print!(push, "This shows multiple times"); /// screen_print!(sec: 6.0, "first and second fields: {}, {}", x.0, x.1); /// screen_print!(col: Color::BLUE, "single line: {x:?}"); /// screen_print!(sec: 10.0, col: Color::BLUE, "last field: {:?}", x.2); /// ``` #[macro_export] macro_rules! screen_print { (push, col: $color:expr, $text:expr $(, $fmt_args:expr)*) => { $crate::screen_print!(@impl push, sec: 7.0, col: Some($color), $text $(, $fmt_args)*); }; (col: $color:expr, $text:expr $(, $fmt_args:expr)*) => { $crate::screen_print!(@impl sec: 7.0, col: Some($color), $text $(, $fmt_args)*); }; (push, sec: $timeout:expr, col: $color:expr, $text:expr $(, $fmt_args:expr)*) => { $crate::screen_print!(@impl push, sec: $timeout, col: Some($color), $text $(, $fmt_args)*); }; (sec: $timeout:expr, col: $color:expr, $text:expr $(, $fmt_args:expr)*) => { $crate::screen_print!(@impl sec: $timeout, col: Some($color), $text $(, $fmt_args)*); }; (push, sec: $timeout:expr, $text:expr $(, $fmt_args:expr)*) => { $crate::screen_print!(@impl push, sec: $timeout, col: None, $text $(, $fmt_args)*); }; (sec: $timeout:expr, $text:expr $(, $fmt_args:expr)*) => { $crate::screen_print!(@impl sec: $timeout, col: None, $text $(, $fmt_args)*); }; (push, $text:expr $(, $fmt_args:expr)*) => { $crate::screen_print!(@impl push, sec: 7.0, col: None, $text $(, $fmt_args)*); }; ($text:expr $(, $fmt_args:expr)*) => { $crate::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().refresh_text(key, || format!($text $(, $fmt_args)*), $timeout as f64, $color); }}; (@impl push, 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().push_text(key, || format!($text $(, $fmt_args)*), $timeout as f64, $color); }}; } /// Specific call site of [`screen_print!`]. /// /// Used to identify where a message is coming from and replacing it on screen /// when updated. #[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 { /// Update in place or add new message already printed at given site. Refresh { key: InvocationSiteKey, color: Option, text: String, timeout: f64, }, /// Always add the message to the screen. Push { color: Option, text: String, timeout: f64, }, } /// Queue text to display on the screen #[doc(hidden)] pub struct CommandChannels { sender: SyncSender, receiver: Mutex>, } impl CommandChannels { // POSSIBLE LEAD: consider providing an API so that at_interval (from demo.rs) can // be used without too much hassle pub fn refresh_text( &self, key: InvocationSiteKey, text: impl FnOnce() -> String, timeout: f64, color: Option, ) { let text = format!("{key} {}\n", text()); let cmd = Command::Refresh { text, key, color, timeout }; let sent = self.sender.try_send(cmd).is_ok(); if !sent { error!("Number of debug messages sent in one frame exceeds limit of {MAX_LINES}"); } } pub fn push_text( &self, key: InvocationSiteKey, text: impl FnOnce() -> String, timeout: f64, color: Option, ) { let text = format!("{key} {}\n", text()); let cmd = Command::Push { text, color, timeout }; let sent = self.sender.try_send(cmd).is_ok(); if !sent { error!("Number of debug messages sent in one frame exceeds limit of {MAX_LINES}"); } } } #[derive(Component)] struct Message { expiration: f64, } impl Message { fn new(expiration: f64) -> Self { Self { expiration } } } #[derive(Resource)] struct Options { font_size: f32, color: Color, } impl<'a> From<&'a OverlayPlugin> for Options { fn from(plugin: &'a OverlayPlugin) -> Self { Self { color: plugin.fallback_color, font_size: plugin.font_size, } } } #[derive(Copy, Clone)] struct PushEntry { entity: Entity, expired: f64, } #[derive(Default)] struct PushList(Vec); impl PushList { fn new_or_allocate( &mut self, spawn_new: impl FnOnce() -> Entity, current: f64, timeout: f64, ) -> Option { let free_existing = self.0.iter_mut().find(|entry| entry.expired < current); let ret = free_existing.as_ref().map(|entry| entry.entity); let new_entry = || PushEntry { entity: spawn_new(), expired: current + timeout }; match free_existing { Some(to_update) => to_update.expired = current + timeout, None => self.0.push(new_entry()), } ret } } fn update_messages_as_per_commands( mut messages: Query<(&mut Text, &mut Message)>, mut key_entities: Local>, mut push_entities: Local, mut cmds: Commands, time: Res