|
| 1 | +//! Text and on-screen debugging tools |
| 2 | +
|
| 3 | +use bevy_app::prelude::*; |
| 4 | +use bevy_asset::prelude::*; |
| 5 | +use bevy_color::prelude::*; |
| 6 | +use bevy_ecs::prelude::*; |
| 7 | +use bevy_picking::backend::HitData; |
| 8 | +use bevy_picking::hover::HoverMap; |
| 9 | +use bevy_picking::pointer::{Location, PointerId, PointerPress}; |
| 10 | +use bevy_picking::prelude::*; |
| 11 | +use bevy_picking::{pointer, PickSet}; |
| 12 | +use bevy_reflect::prelude::*; |
| 13 | +use bevy_render::prelude::*; |
| 14 | +use bevy_text::prelude::*; |
| 15 | +use bevy_ui::prelude::*; |
| 16 | +use core::cmp::Ordering; |
| 17 | +use core::fmt::{Debug, Display, Formatter, Result}; |
| 18 | +use tracing::{debug, trace}; |
| 19 | + |
| 20 | +/// This resource determines the runtime behavior of the debug plugin. |
| 21 | +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Resource)] |
| 22 | +pub enum DebugPickingMode { |
| 23 | + /// Only log non-noisy events, show the debug overlay. |
| 24 | + Normal, |
| 25 | + /// Log all events, including noisy events like `Move` and `Drag`, show the debug overlay. |
| 26 | + Noisy, |
| 27 | + /// Do not show the debug overlay or log any messages. |
| 28 | + #[default] |
| 29 | + Disabled, |
| 30 | +} |
| 31 | + |
| 32 | +impl DebugPickingMode { |
| 33 | + /// A condition indicating the plugin is enabled |
| 34 | + pub fn is_enabled(this: Res<Self>) -> bool { |
| 35 | + matches!(*this, Self::Normal | Self::Noisy) |
| 36 | + } |
| 37 | + /// A condition indicating the plugin is disabled |
| 38 | + pub fn is_disabled(this: Res<Self>) -> bool { |
| 39 | + matches!(*this, Self::Disabled) |
| 40 | + } |
| 41 | + /// A condition indicating the plugin is enabled and in noisy mode |
| 42 | + pub fn is_noisy(this: Res<Self>) -> bool { |
| 43 | + matches!(*this, Self::Noisy) |
| 44 | + } |
| 45 | +} |
| 46 | + |
| 47 | +/// Logs events for debugging |
| 48 | +/// |
| 49 | +/// "Normal" events are logged at the `debug` level. "Noisy" events are logged at the `trace` level. |
| 50 | +/// See [Bevy's LogPlugin](https://docs.rs/bevy/latest/bevy/log/struct.LogPlugin.html) and [Bevy |
| 51 | +/// Cheatbook: Logging, Console Messages](https://bevy-cheatbook.github.io/features/log.html) for |
| 52 | +/// details. |
| 53 | +/// |
| 54 | +/// Usually, the default level printed is `info`, so debug and trace messages will not be displayed |
| 55 | +/// even when this plugin is active. You can set `RUST_LOG` to change this. |
| 56 | +/// |
| 57 | +/// You can also change the log filter at runtime in your code. The [LogPlugin |
| 58 | +/// docs](https://docs.rs/bevy/latest/bevy/log/struct.LogPlugin.html) give an example. |
| 59 | +/// |
| 60 | +/// Use the [`DebugPickingMode`] state resource to control this plugin. Example: |
| 61 | +/// |
| 62 | +/// ```ignore |
| 63 | +/// use DebugPickingMode::{Normal, Disabled}; |
| 64 | +/// app.insert_resource(DebugPickingMode::Normal) |
| 65 | +/// .add_systems( |
| 66 | +/// PreUpdate, |
| 67 | +/// (|mut mode: ResMut<DebugPickingMode>| { |
| 68 | +/// *mode = match *mode { |
| 69 | +/// DebugPickingMode::Disabled => DebugPickingMode::Normal, |
| 70 | +/// _ => DebugPickingMode::Disabled, |
| 71 | +/// }; |
| 72 | +/// }) |
| 73 | +/// .distributive_run_if(bevy::input::common_conditions::input_just_pressed( |
| 74 | +/// KeyCode::F3, |
| 75 | +/// )), |
| 76 | +/// ) |
| 77 | +/// ``` |
| 78 | +/// This sets the starting mode of the plugin to [`DebugPickingMode::Disabled`] and binds the F3 key |
| 79 | +/// to toggle it. |
| 80 | +#[derive(Debug, Default, Clone)] |
| 81 | +pub struct DebugPickingPlugin; |
| 82 | + |
| 83 | +impl Plugin for DebugPickingPlugin { |
| 84 | + fn build(&self, app: &mut App) { |
| 85 | + app.init_resource::<DebugPickingMode>() |
| 86 | + .add_systems( |
| 87 | + PreUpdate, |
| 88 | + pointer_debug_visibility.in_set(PickSet::PostHover), |
| 89 | + ) |
| 90 | + .add_systems( |
| 91 | + PreUpdate, |
| 92 | + ( |
| 93 | + // This leaves room to easily change the log-level associated |
| 94 | + // with different events, should that be desired. |
| 95 | + log_event_debug::<pointer::PointerInput>.run_if(DebugPickingMode::is_noisy), |
| 96 | + log_pointer_event_debug::<Over>, |
| 97 | + log_pointer_event_debug::<Out>, |
| 98 | + log_pointer_event_debug::<Pressed>, |
| 99 | + log_pointer_event_debug::<Released>, |
| 100 | + log_pointer_event_debug::<Click>, |
| 101 | + log_pointer_event_trace::<Move>.run_if(DebugPickingMode::is_noisy), |
| 102 | + log_pointer_event_debug::<DragStart>, |
| 103 | + log_pointer_event_trace::<Drag>.run_if(DebugPickingMode::is_noisy), |
| 104 | + log_pointer_event_debug::<DragEnd>, |
| 105 | + log_pointer_event_debug::<DragEnter>, |
| 106 | + log_pointer_event_trace::<DragOver>.run_if(DebugPickingMode::is_noisy), |
| 107 | + log_pointer_event_debug::<DragLeave>, |
| 108 | + log_pointer_event_debug::<DragDrop>, |
| 109 | + ) |
| 110 | + .distributive_run_if(DebugPickingMode::is_enabled) |
| 111 | + .in_set(PickSet::Last), |
| 112 | + ); |
| 113 | + |
| 114 | + app.add_systems( |
| 115 | + PreUpdate, |
| 116 | + (add_pointer_debug, update_debug_data, debug_draw) |
| 117 | + .chain() |
| 118 | + .distributive_run_if(DebugPickingMode::is_enabled) |
| 119 | + .in_set(PickSet::Last), |
| 120 | + ); |
| 121 | + } |
| 122 | +} |
| 123 | + |
| 124 | +/// Listen for any event and logs it at the debug level |
| 125 | +pub fn log_event_debug<E: Event + Debug>(mut events: EventReader<pointer::PointerInput>) { |
| 126 | + for event in events.read() { |
| 127 | + debug!("{event:?}"); |
| 128 | + } |
| 129 | +} |
| 130 | + |
| 131 | +/// Listens for pointer events of type `E` and logs them at "debug" level |
| 132 | +pub fn log_pointer_event_debug<E: Debug + Clone + Reflect>( |
| 133 | + mut pointer_events: EventReader<Pointer<E>>, |
| 134 | +) { |
| 135 | + for event in pointer_events.read() { |
| 136 | + debug!("{event}"); |
| 137 | + } |
| 138 | +} |
| 139 | + |
| 140 | +/// Listens for pointer events of type `E` and logs them at "trace" level |
| 141 | +pub fn log_pointer_event_trace<E: Debug + Clone + Reflect>( |
| 142 | + mut pointer_events: EventReader<Pointer<E>>, |
| 143 | +) { |
| 144 | + for event in pointer_events.read() { |
| 145 | + trace!("{event}"); |
| 146 | + } |
| 147 | +} |
| 148 | + |
| 149 | +/// Adds [`PointerDebug`] to pointers automatically. |
| 150 | +pub fn add_pointer_debug( |
| 151 | + mut commands: Commands, |
| 152 | + pointers: Query<Entity, (With<PointerId>, Without<PointerDebug>)>, |
| 153 | +) { |
| 154 | + for entity in &pointers { |
| 155 | + commands.entity(entity).insert(PointerDebug::default()); |
| 156 | + } |
| 157 | +} |
| 158 | + |
| 159 | +/// Hide text from pointers. |
| 160 | +pub fn pointer_debug_visibility( |
| 161 | + debug: Res<DebugPickingMode>, |
| 162 | + mut pointers: Query<&mut Visibility, With<PointerId>>, |
| 163 | +) { |
| 164 | + let visible = match *debug { |
| 165 | + DebugPickingMode::Disabled => Visibility::Hidden, |
| 166 | + _ => Visibility::Visible, |
| 167 | + }; |
| 168 | + for mut vis in &mut pointers { |
| 169 | + *vis = visible; |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +/// Storage for per-pointer debug information. |
| 174 | +#[derive(Debug, Component, Clone, Default)] |
| 175 | +pub struct PointerDebug { |
| 176 | + /// The pointer location. |
| 177 | + pub location: Option<Location>, |
| 178 | + |
| 179 | + /// Representation of the different pointer button states. |
| 180 | + pub press: PointerPress, |
| 181 | + |
| 182 | + /// List of hit elements to be displayed. |
| 183 | + pub hits: Vec<(String, HitData)>, |
| 184 | +} |
| 185 | + |
| 186 | +fn bool_to_icon(f: &mut Formatter, prefix: &str, input: bool) -> Result { |
| 187 | + write!(f, "{prefix}{}", if input { "[X]" } else { "[ ]" }) |
| 188 | +} |
| 189 | + |
| 190 | +impl Display for PointerDebug { |
| 191 | + fn fmt(&self, f: &mut Formatter<'_>) -> Result { |
| 192 | + if let Some(location) = &self.location { |
| 193 | + writeln!(f, "Location: {:.2?}", location.position)?; |
| 194 | + } |
| 195 | + bool_to_icon(f, "Pressed: ", self.press.is_primary_pressed())?; |
| 196 | + bool_to_icon(f, " ", self.press.is_middle_pressed())?; |
| 197 | + bool_to_icon(f, " ", self.press.is_secondary_pressed())?; |
| 198 | + let mut sorted_hits = self.hits.clone(); |
| 199 | + sorted_hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)); |
| 200 | + for (entity, hit) in sorted_hits.iter() { |
| 201 | + write!(f, "\nEntity: {entity:?}")?; |
| 202 | + if let Some((position, normal)) = hit.position.zip(hit.normal) { |
| 203 | + write!(f, ", Position: {position:.2?}, Normal: {normal:.2?}")?; |
| 204 | + } |
| 205 | + write!(f, ", Depth: {:.2?}", hit.depth)?; |
| 206 | + } |
| 207 | + |
| 208 | + Ok(()) |
| 209 | + } |
| 210 | +} |
| 211 | + |
| 212 | +/// Update typed debug data used to draw overlays |
| 213 | +pub fn update_debug_data( |
| 214 | + hover_map: Res<HoverMap>, |
| 215 | + entity_names: Query<NameOrEntity>, |
| 216 | + mut pointers: Query<( |
| 217 | + &PointerId, |
| 218 | + &pointer::PointerLocation, |
| 219 | + &PointerPress, |
| 220 | + &mut PointerDebug, |
| 221 | + )>, |
| 222 | +) { |
| 223 | + for (id, location, press, mut debug) in &mut pointers { |
| 224 | + *debug = PointerDebug { |
| 225 | + location: location.location().cloned(), |
| 226 | + press: press.to_owned(), |
| 227 | + hits: hover_map |
| 228 | + .get(id) |
| 229 | + .iter() |
| 230 | + .flat_map(|h| h.iter()) |
| 231 | + .filter_map(|(e, h)| { |
| 232 | + if let Ok(entity_name) = entity_names.get(*e) { |
| 233 | + Some((entity_name.to_string(), h.to_owned())) |
| 234 | + } else { |
| 235 | + None |
| 236 | + } |
| 237 | + }) |
| 238 | + .collect(), |
| 239 | + }; |
| 240 | + } |
| 241 | +} |
| 242 | + |
| 243 | +/// Draw text on each cursor with debug info |
| 244 | +pub fn debug_draw( |
| 245 | + mut commands: Commands, |
| 246 | + camera_query: Query<(Entity, &Camera)>, |
| 247 | + primary_window: Query<Entity, With<bevy_window::PrimaryWindow>>, |
| 248 | + pointers: Query<(Entity, &PointerId, &PointerDebug)>, |
| 249 | + scale: Res<UiScale>, |
| 250 | +) { |
| 251 | + let font_handle: Handle<Font> = Default::default(); |
| 252 | + for (entity, id, debug) in pointers.iter() { |
| 253 | + let Some(pointer_location) = &debug.location else { |
| 254 | + continue; |
| 255 | + }; |
| 256 | + let text = format!("{id:?}\n{debug}"); |
| 257 | + |
| 258 | + for camera in camera_query |
| 259 | + .iter() |
| 260 | + .map(|(entity, camera)| { |
| 261 | + ( |
| 262 | + entity, |
| 263 | + camera.target.normalize(primary_window.get_single().ok()), |
| 264 | + ) |
| 265 | + }) |
| 266 | + .filter_map(|(entity, target)| Some(entity).zip(target)) |
| 267 | + .filter(|(_entity, target)| target == &pointer_location.target) |
| 268 | + .map(|(cam_entity, _target)| cam_entity) |
| 269 | + { |
| 270 | + let mut pointer_pos = pointer_location.position; |
| 271 | + if let Some(viewport) = camera_query |
| 272 | + .get(camera) |
| 273 | + .ok() |
| 274 | + .and_then(|(_, camera)| camera.logical_viewport_rect()) |
| 275 | + { |
| 276 | + pointer_pos -= viewport.min; |
| 277 | + } |
| 278 | + |
| 279 | + commands |
| 280 | + .entity(entity) |
| 281 | + .insert(( |
| 282 | + Text::new(text.clone()), |
| 283 | + TextFont { |
| 284 | + font: font_handle.clone(), |
| 285 | + font_size: 12.0, |
| 286 | + ..Default::default() |
| 287 | + }, |
| 288 | + TextColor(Color::WHITE), |
| 289 | + Node { |
| 290 | + position_type: PositionType::Absolute, |
| 291 | + left: Val::Px(pointer_pos.x + 5.0) / scale.0, |
| 292 | + top: Val::Px(pointer_pos.y + 5.0) / scale.0, |
| 293 | + ..Default::default() |
| 294 | + }, |
| 295 | + )) |
| 296 | + .insert(PickingBehavior::IGNORE) |
| 297 | + .insert(TargetCamera(camera)); |
| 298 | + } |
| 299 | + } |
| 300 | +} |
0 commit comments