bevy_prank/three/
hud.rs

1//! Provides three-dimensional camera HUD overlay.
2
3use super::{Prank3d, Prank3dActive};
4use crate::PrankConfig;
5use bevy::prelude::*;
6
7pub(super) struct Prank3dHudPlugin;
8
9impl Plugin for Prank3dHudPlugin {
10    fn build(&self, app: &mut App) {
11        app.add_systems(
12            Update,
13            (
14                spawn
15                    .run_if(|active: Res<Prank3dActive>| active.is_changed() && active.0.is_some()),
16                despawn
17                    .run_if(|active: Res<Prank3dActive>| active.is_changed() && active.0.is_none()),
18                sync_translation,
19                sync_fps,
20                sync_fov,
21                sync_speed,
22            ),
23        );
24    }
25}
26
27/// Three-dimensional camera HUD overlay configuration.
28#[derive(Clone)]
29pub struct Prank3dHudConfig {
30    /// Overlay height.
31    pub height: Val,
32
33    /// Overlay background color.
34    pub background_color: BackgroundColor,
35
36    /// Overlay text style.
37    pub text_style: TextStyle,
38}
39
40impl Default for Prank3dHudConfig {
41    fn default() -> Self {
42        Self {
43            height: Val::Px(25.0),
44            background_color: Color::BLACK.with_a(0.5).into(),
45            text_style: TextStyle {
46                font_size: 14.0,
47                color: Color::WHITE,
48                ..default()
49            },
50        }
51    }
52}
53
54#[derive(Component)]
55struct Hud;
56
57#[derive(Component)]
58struct HudTranslation;
59
60#[derive(Component)]
61struct HudFps;
62
63#[derive(Component)]
64struct HudFov;
65
66#[derive(Component)]
67struct HudSpeed;
68
69fn spawn(mut commands: Commands, hud: Query<(), With<Hud>>, config: Res<PrankConfig>) {
70    if !hud.is_empty() {
71        return;
72    }
73    let Some(config) = config.hud3d.clone() else {
74        return;
75    };
76
77    commands
78        .spawn((
79            Name::new("Hud"),
80            Hud,
81            NodeBundle {
82                background_color: config.background_color,
83                style: Style {
84                    position_type: PositionType::Absolute,
85                    bottom: Val::Px(0.0),
86                    width: Val::Vw(100.0),
87                    height: config.height,
88                    padding: UiRect::horizontal(Val::Px(5.0)),
89                    column_gap: Val::Px(20.0),
90                    align_items: AlignItems::Center,
91                    ..default()
92                },
93                ..default()
94            },
95        ))
96        .with_children(|parent| {
97            parent.spawn((
98                Name::new("HudTranslation"),
99                HudTranslation,
100                TextBundle::from_section("", config.text_style.clone()),
101            ));
102
103            parent.spawn((
104                Name::new("HudFps"),
105                HudFps,
106                TextBundle::from_section("", config.text_style.clone()),
107            ));
108
109            parent.spawn((
110                Name::new("HudFov"),
111                HudFov,
112                TextBundle::from_section("", config.text_style.clone()),
113            ));
114
115            parent.spawn((
116                Name::new("HudSpeed"),
117                HudSpeed,
118                TextBundle::from_section("", config.text_style.clone()),
119            ));
120        });
121}
122
123fn despawn(mut commands: Commands, hud: Query<Entity, With<Hud>>) {
124    let Ok(entity) = hud.get_single() else {
125        return;
126    };
127
128    commands.entity(entity).despawn_recursive();
129}
130
131fn sync_translation(
132    mut hud_translation: Query<&mut Text, With<HudTranslation>>,
133    active: Res<Prank3dActive>,
134    pranks: Query<&Prank3d>,
135) {
136    let Ok(mut text) = hud_translation.get_single_mut() else {
137        return;
138    };
139    let Some(entity) = active.0 else {
140        return;
141    };
142    let Ok(prank) = pranks.get(entity) else {
143        return;
144    };
145
146    let Vec3 { x, y, z } = prank.translation;
147    text.sections[0].value = format!("Translation: [{:.2}, {:.2}, {:.2}]", x, y, z);
148}
149
150fn sync_fps(mut hud_fps: Query<&mut Text, With<HudFps>>, time: Res<Time>) {
151    let Ok(mut text) = hud_fps.get_single_mut() else {
152        return;
153    };
154
155    text.sections[0].value = format!("FPS: {:.0}", time.delta_seconds().recip());
156}
157
158fn sync_fov(
159    mut hud_fov: Query<&mut Text, With<HudFov>>,
160    active: Res<Prank3dActive>,
161    pranks: Query<&Projection, With<Prank3d>>,
162) {
163    let Ok(mut text) = hud_fov.get_single_mut() else {
164        return;
165    };
166    let Some(entity) = active.0 else {
167        return;
168    };
169    let Ok(projection) = pranks.get(entity) else {
170        return;
171    };
172
173    text.sections[0].value = match projection {
174        Projection::Perspective(projection) => format!("FOV: {:.0}", projection.fov.to_degrees()),
175        Projection::Orthographic(projection) => format!("SCALE: {:.2}", projection.scale),
176    };
177}
178
179fn sync_speed(
180    mut hud_speed: Query<&mut Text, With<HudSpeed>>,
181    active: Res<Prank3dActive>,
182    pranks: Query<&Prank3d>,
183) {
184    let Ok(mut text) = hud_speed.get_single_mut() else {
185        return;
186    };
187    let Some(entity) = active.0 else {
188        return;
189    };
190    let Ok(prank) = pranks.get(entity) else {
191        return;
192    };
193
194    text.sections[0].value = format!("Speed Scalar: {:.1}", prank.speed_scalar);
195}