You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1031 lines
34 KiB
Rust

// Monologue Trees Editor
//
// Editor for creating Monologue Trees levels
//
// BUGS:
// * Cannot view scene when selected, WTF
// * Scene and Animation tabs are whacked
//
// TODO:
// * (medium) Spawn gltf scene
// * (medium) Load default scene when gltf selected
// * (medium) Set gltf to active/inactive
// * (medium) Play individual animation(s)
// * Only select one at a time.
// * (hard) Better Colorscheme
// * (medium) Visual errors for bad GLTFs
// * (medium) Spawn clicked scene
// * (medium) Play clicked animation
// * (easy) Play all animations
// * (medium) Add fonts similar to Audios based on inspect-fonts
// * (hard) Add Dialogs (requires text box UI, saving, loading).
//
// Asset types:
// * Audios (done)
// * Loop individual
// * Stop all
// * Gltfs (doing)
// * Scenes
// * Animations
// * Play/Pause all
// * Fonts
// * Monologues
use bevy::{
asset::{Asset, Assets},
asset::{AssetLoader, LoadContext, LoadedAsset},
audio::PlaybackMode,
gltf::Gltf,
input::{keyboard::KeyboardInput, ButtonState},
prelude::*,
utils::BoxedFuture,
};
use monologue_trees::{debug::*, ui};
fn main() {
App::new()
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Monologue Trees Editor".into(),
resolution: (640., 480.).into(),
..default()
}),
..default()
}),
DebugInfoPlugin,
ui::GameUiPlugin,
))
.init_resource::<AssetRegistry>()
.add_asset::<Monologue>()
.init_asset_loader::<MonologueLoader>()
.add_event::<CustomAssetEvent<Scene>>()
.add_event::<CustomAssetEvent<AnimationClip>>()
.add_systems(Startup, (initialize_ui, welcome_message))
.add_systems(
Update,
(
import_files,
audio_ui,
gltf_ui,
fonts_ui,
texts_ui,
cameras_ui,
manage_active_gltf,
manage_gltf_scene_ui,
manage_gltf_animation_ui,
scenes_ui,
animations_ui,
spawn_scenes,
manage_camera,
play_animation,
play_audio,
),
)
.run();
}
#[derive(Resource, Default)]
pub struct AssetRegistry(Vec<HandleUntyped>);
#[derive(Event)]
pub enum CustomAssetEvent<T: Asset> {
Add { handle: Handle<T>, name: String },
Remove { handle: Handle<T> },
Clear,
}
#[derive(Debug, Component)]
pub struct TabRoot;
#[derive(Debug, Component)]
pub struct LevelRoot;
#[derive(Debug, Component)]
pub struct EditorCamera;
fn initialize_ui(mut commands: Commands) {
// Empty entity for populating the level being edited
commands.spawn((TransformBundle { ..default() }, LevelRoot));
commands.spawn((
Camera3dBundle { ..default() },
UiCameraConfig { show_ui: true },
Name::new("Editor Camera"),
EditorCamera,
));
let base_style = Style {
border: UiRect::all(Val::Px(1.0)),
padding: UiRect::all(Val::Px(1.0)),
overflow: Overflow::clip(),
flex_direction: FlexDirection::Column,
..default()
};
commands
.spawn(NodeBundle {
style: Style {
top: Val::Px(0.0),
left: Val::Px(0.0),
position_type: PositionType::Absolute,
border: UiRect::all(Val::Px(1.0)),
margin: UiRect::all(Val::Px(1.0)),
padding: UiRect::all(Val::Px(1.0)),
flex_direction: FlexDirection::Column,
overflow: Overflow::clip(),
..default()
},
background_color: Color::WHITE.into(),
border_color: Color::BLACK.into(),
..default()
})
.with_children(|parent| {
let container = parent
.spawn((NodeBundle {
style: Style {
border: UiRect::all(Val::Px(1.0)),
margin: UiRect::all(Val::Px(1.0)),
padding: UiRect::all(Val::Px(1.0)),
flex_direction: FlexDirection::Row,
overflow: Overflow::clip(),
..default()
},
background_color: Color::WHITE.into(),
border_color: Color::BLACK.into(),
..default()
},))
.with_children(|parent| {
// HACK: This is super janky but I think we need it like this for UI layout rules
let mut content_containers: Vec<(String, Entity)> = Vec::new();
// Containers with asset content
parent
.spawn((
NodeBundle {
style: Style {
border: UiRect::all(Val::Px(1.0)),
margin: UiRect::all(Val::Px(1.0)),
padding: UiRect::all(Val::Px(1.0)),
flex_direction: FlexDirection::Column,
overflow: Overflow::clip(),
justify_content: JustifyContent::FlexStart,
..default()
},
background_color: Color::WHITE.into(),
border_color: Color::BLACK.into(),
..default()
},
ui::Sorting(2),
))
.with_children(|parent| {
content_containers.push(spawn_tab_container::<FontWidget>(
"Font",
parent,
ui::Select::Single,
));
content_containers.push(spawn_tab_container::<AudioWidget>(
"Audio",
parent,
ui::Select::Multi,
));
content_containers.push(spawn_tab_container::<GltfWidget>(
"Gltf",
parent,
ui::Select::Single,
));
content_containers.push(spawn_tab_container::<SceneWidget>(
"Scene",
parent,
ui::Select::Single,
));
content_containers.push(spawn_tab_container::<AnimationWidget>(
"Animation",
parent,
ui::Select::Multi,
));
content_containers.push(spawn_tab_container::<CameraWidget>(
"Camera",
parent,
ui::Select::Single,
));
});
// Container for tabs that open/close containers
parent
.spawn((
NodeBundle {
style: Style {
border: UiRect::all(Val::Px(1.0)),
margin: UiRect::all(Val::Px(1.0)),
padding: UiRect::all(Val::Px(1.0)),
flex_direction: FlexDirection::Column,
overflow: Overflow::clip(),
..default()
},
background_color: Color::WHITE.into(),
border_color: Color::BLACK.into(),
..default()
},
ui::Sorting(1),
ui::Select::Single,
))
.with_children(|parent| {
let b = ButtonBundle {
style: Style {
..base_style.clone()
},
background_color: Color::WHITE.into(),
border_color: Color::BLACK.into(),
..default()
};
content_containers.iter().for_each(|(name, target)| {
parent.spawn((
b.clone(),
ui::Title { text: name.clone() },
ui::Collapse { target: *target },
));
});
});
})
.id();
parent.spawn((
NodeBundle {
style: Style {
border: UiRect::all(Val::Px(1.0)),
margin: UiRect::all(Val::Px(1.0)),
padding: UiRect::all(Val::Px(1.0)),
flex_direction: FlexDirection::Row,
overflow: Overflow::clip(),
align_items: AlignItems::Center,
align_content: AlignContent::Center,
justify_content: JustifyContent::SpaceBetween,
..default()
},
background_color: Color::ALICE_BLUE.into(),
border_color: Color::BLACK.into(),
..default()
},
ui::Title {
text: "Assets".into(),
},
ui::Minimize { target: container },
ui::Sorting(0),
));
});
}
fn welcome_message(mut writer: EventWriter<ui::Alert>) {
writer.send(ui::Alert::Info(
"Welcome to the Monologue Trees editor!".into(),
));
writer.send(ui::Alert::Info(
[
"Import assets by dragging and dropping files into the editor",
"",
"Supported file types (for now):",
"* 3D: .gltf, .glb",
"* Audio: .ogg",
"* Font: .ttf, .otf",
]
.join("\n")
.into(),
));
}
fn spawn_tab_container<T: Default + Component>(
title: &'static str,
parent: &mut ChildBuilder,
select: ui::Select,
) -> (String, Entity) {
(
title.into(),
// Content node
parent
.spawn((
NodeBundle {
style: Style {
display: Display::None,
border: UiRect::all(Val::Px(1.0)),
margin: UiRect::all(Val::Px(1.0)),
padding: UiRect::all(Val::Px(1.0)),
flex_direction: FlexDirection::Column,
..default()
},
background_color: Color::WHITE.into(),
border_color: Color::BLACK.into(),
..default()
},
T::default(),
ui::Scroll,
Interaction::default(),
select,
))
.id(),
)
}
fn import_files(
mut events: EventReader<FileDragAndDrop>,
server: Res<AssetServer>,
mut registry: ResMut<AssetRegistry>,
) {
events.iter().for_each(|event| match event {
FileDragAndDrop::DroppedFile { path_buf, .. } => {
registry.0.push(
server.load_untyped(
path_buf
.clone()
.into_os_string()
.into_string()
.expect("Path converts to string"),
),
);
}
_ => (),
})
}
use audio::*;
mod audio {
use super::*;
#[derive(Debug, Component, Default)]
pub struct AudioWidget;
pub fn audio_ui(
mut events: EventReader<AssetEvent<AudioSource>>,
mut commands: Commands,
widget: Query<Entity, With<AudioWidget>>,
current: Query<(Entity, &ui::TargetAsset<AudioSource>)>,
server: Res<AssetServer>,
) {
events
.iter()
.filter(|&event| match event {
AssetEvent::Created { handle }
| AssetEvent::Removed { handle }
| AssetEvent::Modified { handle } => {
has_extensions(&server, handle.clone(), &["ogg"])
}
})
.for_each(|event| match event {
AssetEvent::Created { handle } => {
info!("Asset created! {:?}", event);
let id = create_asset_button(
&widget,
&mut commands,
ui::TargetAsset {
handle: handle.clone(),
},
get_asset_name(&server, handle.clone()),
);
commands.entity(id).insert(AudioSourceBundle {
source: handle.clone(),
settings: PlaybackSettings {
mode: PlaybackMode::Loop,
paused: true,
..default()
},
});
}
AssetEvent::Removed { handle } => {
info!("Asset removed! {:?}", event);
destroy_asset_button(
&current,
&mut commands,
&ui::TargetAsset {
handle: handle.clone(),
},
);
}
AssetEvent::Modified { handle } => {
info!("Asset modified! {:?}", event);
destroy_asset_button(
&current,
&mut commands,
&ui::TargetAsset {
handle: handle.clone(),
},
);
let id = create_asset_button(
&widget,
&mut commands,
ui::TargetAsset {
handle: handle.clone(),
},
get_asset_name(&server, handle.clone()),
);
commands.entity(id).insert(AudioSourceBundle {
source: handle.clone(),
settings: PlaybackSettings {
mode: PlaybackMode::Loop,
paused: true,
..default()
},
});
}
});
}
pub fn play_audio(
events: Query<(Entity, &Interaction, &AudioSink), (With<Button>, Changed<Interaction>)>,
mut commands: Commands,
) {
events
.iter()
.filter(|(_, &interaction, _)| interaction == Interaction::Pressed)
.for_each(|(entity, _, sink)| {
sink.toggle();
if sink.is_paused() {
commands.entity(entity).remove::<ui::Active>();
} else {
commands.entity(entity).insert(ui::Active);
}
});
}
}
use assets::*;
mod assets {
use super::*;
pub fn get_asset_name<T: Asset>(server: &AssetServer, handle: Handle<T>) -> String {
if let Some(asset_path) = server.get_handle_path(handle.clone()) {
if let Some(stem) = asset_path.path().file_stem() {
if let Some(val) = stem.to_str() {
String::from(val)
} else {
String::from("???")
}
} else {
String::from("???")
}
} else {
String::from("???")
}
}
pub fn create_asset_button<A: Asset, C: Component>(
root: &Query<Entity, With<C>>,
commands: &mut Commands,
target: ui::TargetAsset<A>,
name: String,
) -> Entity {
commands
.spawn((
target,
ButtonBundle {
style: Style {
border: UiRect::all(Val::Px(1.0)),
margin: UiRect::all(Val::Px(1.0)),
padding: UiRect::all(Val::Px(1.0)),
..default()
},
border_color: Color::BLACK.into(),
..default()
},
ui::Title { text: name },
))
.set_parent(root.single())
.id()
}
pub fn create_entity_button<C: Component>(
root: &Query<Entity, With<C>>,
commands: &mut Commands,
target: ui::TargetEntity,
name: String,
) -> Entity {
commands
.spawn((
target,
ButtonBundle {
style: Style {
border: UiRect::all(Val::Px(1.0)),
margin: UiRect::all(Val::Px(1.0)),
padding: UiRect::all(Val::Px(1.0)),
..default()
},
border_color: Color::BLACK.into(),
..default()
},
ui::Title { text: name },
))
.set_parent(root.single())
.id()
}
pub fn destroy_asset_button<A: Asset>(
current: &Query<(Entity, &ui::TargetAsset<A>)>,
commands: &mut Commands,
target: &ui::TargetAsset<A>,
) {
if let Some(entity) = current.iter().find_map(|(entity, this)| {
if this.handle == target.handle {
Some(entity)
} else {
None
}
}) {
commands.entity(entity).despawn_recursive();
}
}
pub fn has_extensions<T: Asset>(
server: &AssetServer,
handle: Handle<T>,
extensions: &[&'static str],
) -> bool {
if let Some(asset_path) = server.get_handle_path(handle.clone()) {
if let Some(extension) = asset_path.path().extension() {
extensions.iter().any(|&check| check == extension)
} else {
false
}
} else {
false
}
}
}
use gltf::*;
mod gltf {
use super::*;
#[derive(Debug, Component, Default)]
pub struct GltfWidget;
// TODO: Mark selected gltf as active ~single exclusive~
pub fn gltf_ui(
mut events: EventReader<AssetEvent<Gltf>>,
mut commands: Commands,
widget: Query<Entity, With<GltfWidget>>,
current: Query<(Entity, &ui::TargetAsset<Gltf>)>,
server: Res<AssetServer>,
) {
events
.iter()
.filter(|&event| match event {
AssetEvent::Created { handle }
| AssetEvent::Removed { handle }
| AssetEvent::Modified { handle } => {
has_extensions(&server, handle.clone(), &["gltf", "glb"])
}
})
.for_each(|event| match event {
AssetEvent::Created { handle } => {
info!("Asset created! {:?}", event);
create_asset_button(
&widget,
&mut commands,
ui::TargetAsset {
handle: handle.clone(),
},
get_asset_name(&server, handle.clone()),
);
}
AssetEvent::Removed { handle } => {
info!("Asset removed! {:?}", event);
destroy_asset_button(
&current,
&mut commands,
&ui::TargetAsset {
handle: handle.clone(),
},
);
}
AssetEvent::Modified { handle } => {
info!("Asset modified! {:?}", event);
destroy_asset_button(
&current,
&mut commands,
&ui::TargetAsset {
handle: handle.clone(),
},
);
create_asset_button(
&widget,
&mut commands,
ui::TargetAsset {
handle: handle.clone(),
},
get_asset_name(&server, handle.clone()),
);
}
});
}
pub fn manage_active_gltf(
events: Query<
(Entity, &Interaction, Option<&ui::Active>),
(With<Button>, Changed<Interaction>),
>,
mut commands: Commands,
) {
events
.iter()
.filter(|(_, &interaction, _)| interaction == Interaction::Pressed)
.for_each(|(entity, _, active_ish)| match active_ish {
Some(_) => {
commands.entity(entity).remove::<ui::Active>();
}
None => {
commands.entity(entity).insert(ui::Active);
}
});
}
pub fn manage_gltf_animation_ui(
added: Query<Entity, (With<Button>, Added<ui::Active>)>,
mut removed: RemovedComponents<ui::Active>,
targets_gltf: Query<&ui::TargetAsset<Gltf>>,
gltfs: Res<Assets<Gltf>>,
mut animation_clip_events: EventWriter<CustomAssetEvent<AnimationClip>>,
) {
removed
.iter()
.filter_map(|entity| {
if let Ok(ui::TargetAsset { handle }) = targets_gltf.get(entity) {
gltfs.get(handle)
} else {
None
}
})
.for_each(|gltf| {
gltf.named_animations
.iter()
.for_each(|(animation_name, animation_handle)| {
info!("Named animation: {:?}", animation_name);
animation_clip_events.send(CustomAssetEvent::Remove {
handle: animation_handle.clone(),
});
});
});
added
.iter()
.filter_map(|entity| {
if let Ok(ui::TargetAsset { handle }) = targets_gltf.get(entity) {
gltfs.get(handle)
} else {
None
}
})
.for_each(|gltf| {
// Populate animations tab
gltf.named_animations
.iter()
.for_each(|(animation_name, animation_handle)| {
animation_clip_events.send(CustomAssetEvent::Add {
name: animation_name.clone(),
handle: animation_handle.clone(),
})
});
});
}
pub fn manage_gltf_scene_ui(
added: Query<Entity, (With<Button>, Added<ui::Active>)>,
mut removed: RemovedComponents<ui::Active>,
targets_gltf: Query<&ui::TargetAsset<Gltf>>,
gltfs: Res<Assets<Gltf>>,
mut scene_events: EventWriter<CustomAssetEvent<Scene>>,
) {
removed
.iter()
.filter_map(|entity| {
if let Ok(ui::TargetAsset { handle }) = targets_gltf.get(entity) {
gltfs.get(handle)
} else {
None
}
})
.for_each(|gltf| {
gltf.named_scenes
.iter()
.for_each(|(scene_name, scene_handle)| {
info!("Named scene: {:?}", scene_name);
scene_events.send(CustomAssetEvent::Remove {
handle: scene_handle.clone(),
});
});
});
added
.iter()
.filter_map(|entity| {
if let Ok(ui::TargetAsset { handle }) = targets_gltf.get(entity) {
gltfs.get(handle)
} else {
None
}
})
.for_each(|gltf| {
// Populate scenes tab
gltf.named_scenes
.iter()
.for_each(|(scene_name, scene_handle)| {
info!("Named scene: {:?}", scene_name);
scene_events.send(CustomAssetEvent::Add {
name: scene_name.clone(),
handle: scene_handle.clone(),
})
})
});
}
}
// TODO: Mark loaded animation as active
use scenes::*;
mod scenes {
use super::*;
#[derive(Debug, Component, Default)]
pub struct SceneWidget;
pub fn scenes_ui(
mut events: EventReader<CustomAssetEvent<Scene>>,
mut commands: Commands,
widget: Query<Entity, With<SceneWidget>>,
current: Query<(Entity, &ui::TargetAsset<Scene>)>,
) {
events.iter().for_each(|event| {
match event {
CustomAssetEvent::Add { name, handle } => {
info!("Asset loading! {:?}({:?})", name, handle);
// Spawn new tree
create_asset_button(
&widget,
&mut commands,
ui::TargetAsset {
handle: handle.clone(),
},
name.clone(),
);
}
CustomAssetEvent::Remove { handle } => {
destroy_asset_button(
&current,
&mut commands,
&ui::TargetAsset {
handle: handle.clone(),
},
);
}
CustomAssetEvent::Clear => {
commands.entity(widget.single()).despawn_descendants();
}
}
});
}
pub fn spawn_scenes(
events: Query<
(&Interaction, &ui::TargetAsset<Scene>),
(With<Button>, Changed<Interaction>),
>,
level_root: Query<Entity, With<LevelRoot>>,
mut commands: Commands,
) {
events
.iter()
.filter(|(&interaction, _)| interaction == Interaction::Pressed)
.for_each(|(_, ui::TargetAsset { handle })| {
info!("Spawning {:?}", handle);
commands
.entity(level_root.single())
.with_children(|parent| {
parent.spawn((handle.clone(), TransformBundle { ..default() }));
});
})
}
}
// TODO: Play all animations
// TODO: Mark playing animation as active
use animations::*;
mod animations {
use super::*;
#[derive(Debug, Component, Default)]
pub struct AnimationWidget;
pub fn animations_ui(
mut events: EventReader<CustomAssetEvent<AnimationClip>>,
mut commands: Commands,
widget: Query<Entity, With<AnimationWidget>>,
current: Query<(Entity, &ui::TargetAsset<AnimationClip>)>,
) {
events.iter().for_each(|event| {
match event {
CustomAssetEvent::Add { name, handle } => {
info!("Asset loading! {:?}({:?})", name, handle);
// Spawn new tree
create_asset_button(
&widget,
&mut commands,
ui::TargetAsset {
handle: handle.clone(),
},
name.clone(),
);
}
CustomAssetEvent::Remove { handle } => {
destroy_asset_button(
&current,
&mut commands,
&ui::TargetAsset {
handle: handle.clone(),
},
);
}
CustomAssetEvent::Clear => {
commands.entity(widget.single()).despawn_descendants();
}
}
});
}
pub fn play_animation(
events: Query<
(&Interaction, &ui::TargetAsset<AnimationClip>),
(With<Button>, Changed<Interaction>),
>,
mut targets: Query<(&mut AnimationPlayer, &Name), With<Transform>>,
clips: Res<Assets<AnimationClip>>,
) {
events
.iter()
.filter(|(&interaction, _)| interaction == Interaction::Pressed)
.for_each(|(_, ui::TargetAsset { handle })| {
let clip = clips.get(handle).expect("Load animation clip");
targets
.iter_mut()
.filter(|(_, name)| clip.compatible_with(name))
.for_each(|(mut player, _)| {
player.play(handle.clone()).repeat();
})
});
}
}
use fonts::*;
mod fonts {
use super::*;
#[derive(Debug, Component, Default)]
pub struct FontWidget;
// TODO: Make each button have the font
pub fn fonts_ui(
mut events: EventReader<AssetEvent<Font>>,
mut commands: Commands,
widget: Query<Entity, With<FontWidget>>,
current: Query<(Entity, &ui::TargetAsset<Font>)>,
server: Res<AssetServer>,
) {
events
.iter()
.filter(|&event| match event {
AssetEvent::Created { handle }
| AssetEvent::Removed { handle }
| AssetEvent::Modified { handle } => {
has_extensions(&server, handle.clone(), &["ttf", "otf"])
}
})
.for_each(|event| match event {
AssetEvent::Created { handle } => {
info!("Asset created! {:?}", event);
create_asset_button(
&widget,
&mut commands,
ui::TargetAsset {
handle: handle.clone(),
},
get_asset_name(&server, handle.clone()),
);
}
AssetEvent::Removed { handle } => {
info!("Asset removed! {:?}", event);
destroy_asset_button(
&current,
&mut commands,
&ui::TargetAsset {
handle: handle.clone(),
},
);
}
AssetEvent::Modified { handle } => {
info!("Asset modified! {:?}", event);
destroy_asset_button(
&current,
&mut commands,
&ui::TargetAsset {
handle: handle.clone(),
},
);
create_asset_button(
&widget,
&mut commands,
ui::TargetAsset {
handle: handle.clone(),
},
get_asset_name(&server, handle.clone()),
);
}
});
}
}
use monologues::*;
mod monologues {
use super::*;
use bevy::reflect::{TypePath, TypeUuid};
use serde::Deserialize;
#[derive(Debug, Component, Default)]
pub struct MonologueWidget;
#[derive(Debug, Deserialize, TypeUuid, TypePath, PartialEq)]
#[uuid = "216a570b-d142-4026-baed-d7feb0250458"]
pub struct Monologue {
text: String,
}
#[derive(Default)]
pub struct MonologueLoader;
impl AssetLoader for MonologueLoader {
fn load<'a>(
&'a self,
bytes: &'a [u8],
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<(), bevy::asset::Error>> {
Box::pin(async move {
load_context.set_default_asset(LoadedAsset::new(
String::from_utf8(bytes.to_vec()).expect("Convert bytes to String"),
));
Ok(())
})
}
fn extensions(&self) -> &[&str] {
&[".monologue.txt"]
}
}
// TODO: Load .txt files for monologues
pub fn texts_ui(
mut events: EventReader<AssetEvent<Monologue>>,
mut _commands: Commands,
_widget: Query<Entity, With<MonologueWidget>>,
_current: Query<(Entity, &ui::TargetAsset<Monologue>)>,
_server: Res<AssetServer>,
) {
events.iter().for_each(|_event| {
info!("Loading monologue");
})
}
}
use cameras::*;
mod cameras {
use super::*;
#[derive(Debug, Component, Default)]
pub struct CameraWidget;
// TODO: Despawn camera button when camera removed
pub fn cameras_ui(
mut events: Query<(Entity, &mut Camera, &Name), (Added<Camera>, Without<EditorCamera>)>,
widget: Query<Entity, With<CameraWidget>>,
mut commands: Commands,
) {
events.iter_mut().for_each(|(entity, mut camera, name)| {
info!("Camera added {:?} {:?}", entity, name);
create_entity_button(
&widget,
&mut commands,
ui::TargetEntity { entity },
name.as_str().into(),
);
camera.is_active = false;
})
}
pub fn manage_camera(
events: Query<(&Interaction, &ui::TargetEntity), Changed<Interaction>>,
mut cameras: Query<(Entity, &mut Camera)>,
) {
events
.iter()
.filter(|(&interaction, _)| interaction == Interaction::Pressed)
.for_each(|(_, ui::TargetEntity { entity })| {
cameras.iter_mut().for_each(|(this_entity, mut camera)| {
camera.is_active = this_entity == *entity;
});
});
}
}