Path: blob/main/crates/bevy_dev_tools/src/picking_debug.rs
6595 views
//! Text and on-screen debugging tools12use bevy_app::prelude::*;3use bevy_camera::visibility::Visibility;4use bevy_camera::Camera;5use bevy_color::prelude::*;6use bevy_ecs::prelude::*;7use bevy_picking::backend::HitData;8use bevy_picking::hover::HoverMap;9use bevy_picking::pointer::{Location, PointerId, PointerInput, PointerLocation, PointerPress};10use bevy_picking::prelude::*;11use bevy_picking::PickingSystems;12use bevy_reflect::prelude::*;13use bevy_text::prelude::*;14use bevy_ui::prelude::*;15use core::cmp::Ordering;16use core::fmt::{Debug, Display, Formatter, Result};17use tracing::{debug, trace};1819/// This resource determines the runtime behavior of the debug plugin.20#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Resource)]21pub enum DebugPickingMode {22/// Only log non-noisy events, show the debug overlay.23Normal,24/// Log all events, including noisy events like `Move` and `Drag`, show the debug overlay.25Noisy,26/// Do not show the debug overlay or log any messages.27#[default]28Disabled,29}3031impl DebugPickingMode {32/// A condition indicating the plugin is enabled33pub fn is_enabled(this: Res<Self>) -> bool {34matches!(*this, Self::Normal | Self::Noisy)35}36/// A condition indicating the plugin is disabled37pub fn is_disabled(this: Res<Self>) -> bool {38matches!(*this, Self::Disabled)39}40/// A condition indicating the plugin is enabled and in noisy mode41pub fn is_noisy(this: Res<Self>) -> bool {42matches!(*this, Self::Noisy)43}44}4546/// Logs events for debugging47///48/// "Normal" events are logged at the `debug` level. "Noisy" events are logged at the `trace` level.49/// See [Bevy's LogPlugin](https://docs.rs/bevy/latest/bevy/log/struct.LogPlugin.html) and [Bevy50/// Cheatbook: Logging, Console Messages](https://bevy-cheatbook.github.io/features/log.html) for51/// details.52///53/// Usually, the default level printed is `info`, so debug and trace messages will not be displayed54/// even when this plugin is active. You can set `RUST_LOG` to change this.55///56/// You can also change the log filter at runtime in your code. The [LogPlugin57/// docs](https://docs.rs/bevy/latest/bevy/log/struct.LogPlugin.html) give an example.58///59/// Use the [`DebugPickingMode`] state resource to control this plugin. Example:60///61/// ```ignore62/// use DebugPickingMode::{Normal, Disabled};63/// app.insert_resource(DebugPickingMode::Normal)64/// .add_systems(65/// PreUpdate,66/// (|mut mode: ResMut<DebugPickingMode>| {67/// *mode = match *mode {68/// DebugPickingMode::Disabled => DebugPickingMode::Normal,69/// _ => DebugPickingMode::Disabled,70/// };71/// })72/// .distributive_run_if(bevy::input::common_conditions::input_just_pressed(73/// KeyCode::F3,74/// )),75/// )76/// ```77/// This sets the starting mode of the plugin to [`DebugPickingMode::Disabled`] and binds the F3 key78/// to toggle it.79#[derive(Debug, Default, Clone)]80pub struct DebugPickingPlugin;8182impl Plugin for DebugPickingPlugin {83fn build(&self, app: &mut App) {84app.init_resource::<DebugPickingMode>()85.add_systems(86PreUpdate,87pointer_debug_visibility.in_set(PickingSystems::PostHover),88)89.add_systems(90PreUpdate,91(92// This leaves room to easily change the log-level associated93// with different events, should that be desired.94log_event_debug::<PointerInput>.run_if(DebugPickingMode::is_noisy),95log_pointer_event_debug::<Over>,96log_pointer_event_debug::<Out>,97log_pointer_event_debug::<Press>,98log_pointer_event_debug::<Release>,99log_pointer_event_debug::<Click>,100log_pointer_event_trace::<Move>.run_if(DebugPickingMode::is_noisy),101log_pointer_event_debug::<DragStart>,102log_pointer_event_trace::<Drag>.run_if(DebugPickingMode::is_noisy),103log_pointer_event_debug::<DragEnd>,104log_pointer_event_debug::<DragEnter>,105log_pointer_event_trace::<DragOver>.run_if(DebugPickingMode::is_noisy),106log_pointer_event_debug::<DragLeave>,107log_pointer_event_debug::<DragDrop>,108)109.distributive_run_if(DebugPickingMode::is_enabled)110.in_set(PickingSystems::Last),111);112113app.add_systems(114PreUpdate,115(add_pointer_debug, update_debug_data, debug_draw)116.chain()117.distributive_run_if(DebugPickingMode::is_enabled)118.in_set(PickingSystems::Last),119);120}121}122123/// Listen for any event and logs it at the debug level124pub fn log_event_debug<E: BufferedEvent + Debug>(mut events: EventReader<PointerInput>) {125for event in events.read() {126debug!("{event:?}");127}128}129130/// Listens for pointer events of type `E` and logs them at "debug" level131pub fn log_pointer_event_debug<E: Debug + Clone + Reflect>(132mut pointer_events: EventReader<Pointer<E>>,133) {134for event in pointer_events.read() {135debug!("{event}");136}137}138139/// Listens for pointer events of type `E` and logs them at "trace" level140pub fn log_pointer_event_trace<E: Debug + Clone + Reflect>(141mut pointer_events: EventReader<Pointer<E>>,142) {143for event in pointer_events.read() {144trace!("{event}");145}146}147148/// Adds [`PointerDebug`] to pointers automatically.149pub fn add_pointer_debug(150mut commands: Commands,151pointers: Query<Entity, (With<PointerId>, Without<PointerDebug>)>,152) {153for entity in &pointers {154commands.entity(entity).insert(PointerDebug::default());155}156}157158/// Hide text from pointers.159pub fn pointer_debug_visibility(160debug: Res<DebugPickingMode>,161mut pointers: Query<&mut Visibility, With<PointerId>>,162) {163let visible = match *debug {164DebugPickingMode::Disabled => Visibility::Hidden,165_ => Visibility::Visible,166};167for mut vis in &mut pointers {168*vis = visible;169}170}171172/// Storage for per-pointer debug information.173#[derive(Debug, Component, Clone, Default)]174pub struct PointerDebug {175/// The pointer location.176pub location: Option<Location>,177178/// Representation of the different pointer button states.179pub press: PointerPress,180181/// List of hit elements to be displayed.182pub hits: Vec<(String, HitData)>,183}184185fn bool_to_icon(f: &mut Formatter, prefix: &str, input: bool) -> Result {186write!(f, "{prefix}{}", if input { "[X]" } else { "[ ]" })187}188189impl Display for PointerDebug {190fn fmt(&self, f: &mut Formatter<'_>) -> Result {191if let Some(location) = &self.location {192writeln!(f, "Location: {:.2?}", location.position)?;193}194bool_to_icon(f, "Pressed: ", self.press.is_primary_pressed())?;195bool_to_icon(f, " ", self.press.is_middle_pressed())?;196bool_to_icon(f, " ", self.press.is_secondary_pressed())?;197let mut sorted_hits = self.hits.clone();198sorted_hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));199for (entity, hit) in sorted_hits.iter() {200write!(f, "\nEntity: {entity:?}")?;201if let Some((position, normal)) = hit.position.zip(hit.normal) {202write!(f, ", Position: {position:.2?}, Normal: {normal:.2?}")?;203}204write!(f, ", Depth: {:.2?}", hit.depth)?;205}206207Ok(())208}209}210211/// Update typed debug data used to draw overlays212pub fn update_debug_data(213hover_map: Res<HoverMap>,214entity_names: Query<NameOrEntity>,215mut pointers: Query<(216&PointerId,217&PointerLocation,218&PointerPress,219&mut PointerDebug,220)>,221) {222for (id, location, press, mut debug) in &mut pointers {223*debug = PointerDebug {224location: location.location().cloned(),225press: press.to_owned(),226hits: hover_map227.get(id)228.iter()229.flat_map(|h| h.iter())230.filter_map(|(e, h)| {231if let Ok(entity_name) = entity_names.get(*e) {232Some((entity_name.to_string(), h.to_owned()))233} else {234None235}236})237.collect(),238};239}240}241242/// Draw text on each cursor with debug info243pub fn debug_draw(244mut commands: Commands,245camera_query: Query<(Entity, &Camera)>,246primary_window: Query<Entity, With<bevy_window::PrimaryWindow>>,247pointers: Query<(Entity, &PointerId, &PointerDebug)>,248scale: Res<UiScale>,249) {250for (entity, id, debug) in &pointers {251let Some(pointer_location) = &debug.location else {252continue;253};254let text = format!("{id:?}\n{debug}");255256for (camera, _) in camera_query.iter().filter(|(_, camera)| {257camera258.target259.normalize(primary_window.single().ok())260.is_some_and(|target| target == pointer_location.target)261}) {262let mut pointer_pos = pointer_location.position;263if let Some(viewport) = camera_query264.get(camera)265.ok()266.and_then(|(_, camera)| camera.logical_viewport_rect())267{268pointer_pos -= viewport.min;269}270271commands272.entity(entity)273.despawn_related::<Children>()274.insert((275Node {276position_type: PositionType::Absolute,277left: Val::Px(pointer_pos.x + 5.0) / scale.0,278top: Val::Px(pointer_pos.y + 5.0) / scale.0,279padding: UiRect::px(10.0, 10.0, 8.0, 6.0),280..Default::default()281},282BackgroundColor(Color::BLACK.with_alpha(0.75)),283GlobalZIndex(i32::MAX),284Pickable::IGNORE,285UiTargetCamera(camera),286children![(Text::new(text.clone()), TextFont::from_font_size(12.0))],287));288}289}290}291292293