From b119019e305b4fdedaba26c5c90c35c62f7b63cc Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Sat, 19 Aug 2023 08:50:52 -0700 Subject: [PATCH] Audio vertical slice done --- bin/editor.rs | 524 +++++++++++++++++++---------------------------- src/ui.rs | 553 ++++++++++++-------------------------------------- 2 files changed, 340 insertions(+), 737 deletions(-) diff --git a/bin/editor.rs b/bin/editor.rs index 2173af1..e96e3e4 100644 --- a/bin/editor.rs +++ b/bin/editor.rs @@ -22,7 +22,7 @@ use bevy::{ prelude::*, utils::HashSet, }; -use monologue_trees::debug::*; +use monologue_trees::{debug::*, ui}; fn main() { App::new() @@ -36,361 +36,249 @@ fn main() { ..default() }), DebugInfoPlugin, + ui::GameUiPlugin, )) .init_resource::() - .add_systems(Startup, (initialize_ui,)) - .add_systems( - Update, - ( - // GLTF Systems - load_gltf, - unload_gltf, - manage_gltf_ui, - // Scene Systems - manage_scene_ui, - spawn_scene, - // Animation systems - manage_animation_ui, - // Camera systems - manage_camera_ui, - // Audio Systems - load_audio, - unload_audio, - manage_audio_ui, - play_audio, - // Level Import/Export systems - export_level, - import_level, - // Misc/Debug Systems - ), - ) + .add_systems(Startup, initialize_ui) + .add_systems(Update, (import_files, import_audio, play_audio)) .run(); } -/// A generic referenece used for UI elements to point to assets or entities -#[derive(Debug, Component)] -enum UiRef { - Handle(T), - // Entity(T), - // Event(T), +#[derive(Resource, Default)] +struct AssetRegistry(Vec); + +#[derive(Resource)] +struct Styles { + button: Style, + button_hovered: Style, + container: Style, } -/// UI: -/// * GLTFs -/// * Scenes -/// * Cameras -/// * Animations -/// * Audios fn initialize_ui(mut commands: Commands) { commands.spawn(( Camera2dBundle { ..default() }, UiCameraConfig { show_ui: true }, )); + let base_style = Style { + border: UiRect::all(Val::Px(1.0)), + padding: UiRect::all(Val::Px(5.0)), + flex_direction: FlexDirection::Column, + overflow: Overflow::clip(), + ..default() + }; + commands .spawn(NodeBundle { style: Style { - width: Val::Percent(100.0), - height: Val::Percent(100.0), - ..default() + width: Val::Percent(50.0), + height: Val::Percent(50.0), + ..base_style.clone() }, + background_color: Color::WHITE.into(), + border_color: Color::BLACK.into(), + z_index: ZIndex::Local(100), ..default() }) .with_children(|parent| { - parent - .spawn(( - Name::new("GLTFs"), - GltfsUi, - NodeBundle { + { + parent + .spawn(NodeBundle { style: Style { - flex_direction: FlexDirection::Column, - ..default() + ..base_style.clone() }, + background_color: Color::WHITE.into(), + border_color: Color::BLACK.into(), + z_index: ZIndex::Local(20), ..default() - }, - )) - .with_children(|parent| { - parent.spawn((Name::new("Scenes"), ScenesUi, NodeBundle { ..default() })); - parent.spawn((Name::new("Cameras"), CamerasUi, NodeBundle { ..default() })); - parent.spawn(( - Name::new("Animations"), - AnimationsUi, - NodeBundle { ..default() }, - )); - }); - parent.spawn(( - Name::new("Audio Clips"), - AudioClipsUi, - NodeBundle { - style: Style { - flex_direction: FlexDirection::Column, - ..default() - }, - ..default() - }, - )); - }); -} - -#[derive(Resource, Default, Debug)] -struct AssetRegistry(HashSet); - -/// Component marking UI for loaded Gltf assets -#[derive(Component)] -struct GltfsUi; + }) + .with_children(|parent| { + // Spawn button container + let container = parent + .spawn(( + NodeBundle { + style: Style { + ..base_style.clone() + }, + background_color: Color::WHITE.into(), + border_color: Color::BLACK.into(), + z_index: ZIndex::Local(10), + ..default() + }, + AudioWidget, + ui::Scroll, + ui::Sorting(2), + )) + .id(); -/// Drag+Drop import GLTF to editor -fn load_gltf( - mut events: EventReader, - server: Res, - mut assets: ResMut, -) { - events - .iter() - .filter_map(|event| match event { - FileDragAndDrop::DroppedFile { path_buf, .. } => Some(path_buf), - _ => None, - }) - .for_each(|path_buf| { - let path = path_buf.as_path(); - let handle = server.load_untyped(path); - assets.0.insert(handle); + // Spawn widget + parent.spawn(( + ButtonBundle { + style: Style { + ..base_style.clone() + }, + background_color: Color::WHITE.into(), + border_color: Color::BLACK.into(), + z_index: ZIndex::Local(15), + ..default() + }, + ui::Title { + name: "Audio".into(), + }, + ui::Collapse { target: container }, + ui::Sorting(1), + )); + }); + } }); -} -/// Helper method to extract the stripped filename given a full asset path -fn get_fname(asset_path: AssetPath, suffixes: &[&str]) -> String { - let path = asset_path.path().file_name().expect("Filename"); - let path_str = path.to_str().expect("Asset Path to Str"); - let name_str = suffixes - .iter() - .rfold(path_str, |acc, &suffix| acc.trim_end_matches(suffix)); - String::from(name_str) + commands.insert_resource(Styles { + button: Style { ..default() }, + button_hovered: Style { ..default() }, + container: Style { ..default() }, + }) } -/// Sync GLTF assets with UI -/// -/// TODO: Handle failed load events -/// Options: -/// * Show Error message, do not add to UI -/// * Add to UI with visual indicator -/// This should be a separate async system -fn manage_gltf_ui( - mut events: EventReader>, - root: Query, Without>>)>, - mut commands: Commands, +fn import_files( + mut events: EventReader, server: Res, + mut registry: ResMut, ) { - events - .iter() - .filter_map(|event| match event { - AssetEvent::Created { handle } => { - let asset_path = server - .get_handle_path(handle.clone()) - .expect("Fetch Asset Path"); - let name = get_fname(asset_path, &[".gltf", ".glb"]); - Some((handle.clone(), String::from(name))) - } - _ => None, - }) - .for_each(|(handle, name)| { - root.iter().for_each(|entity| { - commands.entity(entity).log_components(); - }); - commands - .spawn(( - Name::new(name), - GltfsUi, - ButtonBundle { ..default() }, - UiRef::Handle(handle.clone()), - )) - .set_parent(root.single()); - }); -} - -/// Remove gltf from editor -fn unload_gltf() {} - -/// Component marking UI for Scene assets -#[derive(Component)] -struct ScenesUi; - -/// Sync scene assets with UI -fn manage_scene_ui( - mut events: EventReader>, - root: Query, Without>>)>, - mut commands: Commands, - gltfs: Res>, - registry: Res, -) { - events - .iter() - .filter_map(|event| match event { - AssetEvent::Created { handle } => { - let name = registry - .0 - .iter() - .find_map( - |gltf_handle| match gltfs.get(&gltf_handle.clone().typed::()) { - Some(gltf) => { - gltf.named_scenes.iter().find_map(|(name, scene_handle)| { - (scene_handle == handle).then_some(name) - }) - } - None => None, - }, - ) - .expect("Find scene name"); - Some((handle.clone(), String::from(name))) - } - _ => None, - }) - .for_each(|(handle, name)| { - commands - .spawn(( - Name::new(name), - ScenesUi, - ButtonBundle { ..default() }, - UiRef::Handle(handle.clone()), - )) - .set_parent(root.single()); - }); + 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"), + ), + ); + } + _ => (), + }) } -/// Component marking UI for Camera assets -#[derive(Component)] -struct CamerasUi; +use audio::*; +mod audio { + use bevy::audio::PlaybackMode; -fn manage_camera_ui() {} + use super::*; -/// Component marking UI for Animation assets -#[derive(Component)] -struct AnimationsUi; + #[derive(Debug, Component)] + pub struct AudioWidget; -fn manage_animation_ui( - mut events: EventReader>, - root: Query, Without>>)>, - mut commands: Commands, - gltfs: Res>, - registry: Res, -) { - events - .iter() - .filter_map(|event| match event { - AssetEvent::Created { handle } => { - let name = - registry - .0 - .iter() - .find_map(|gltf_handle| { - match gltfs.get(&gltf_handle.clone().typed::()) { - Some(gltf) => gltf.named_animations.iter().find_map( - |(name, animation_handle)| { - (animation_handle == handle).then_some(name) - }, - ), - None => None, + pub fn import_audio( + mut events: EventReader>, + mut commands: Commands, + root: Query>, + current: Query<(Entity, &Handle)>, + server: Res, + ) { + // TODO: Filter to just audio files + events + .iter() + .filter(|event| match event { + AssetEvent::Created { handle } + | AssetEvent::Removed { handle } + | AssetEvent::Modified { handle } => { + if let Some(asset_path) = server.get_handle_path(handle.clone()) { + if let Some(extension) = asset_path.path().extension() { + extension == "ogg" + } else { + false + } + } else { + false + } + } + }) + .for_each(|event| { + let create = |commands: &mut Commands, handle: Handle| { + commands.entity(root.single()).with_children(|parent| { + let name = { + 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("???") } - }) - .expect("Find animation name"); - Some((handle.clone(), String::from(name))) - } - _ => None, - }) - .for_each(|(handle, name)| { - commands - .spawn(( - Name::new(name), - AnimationsUi, - ButtonBundle { ..default() }, - UiRef::Handle(handle.clone()), - )) - .set_parent(root.single()); - }); -} - -/// Component marking UI for Audio Clip assets -#[derive(Component)] -struct AudioClipsUi; - -/// Drag+Drop import Audio to editor -fn load_audio( - mut events: EventReader, - server: Res, - mut assets: ResMut, -) { - events - .iter() - .filter_map(|event| match event { - FileDragAndDrop::DroppedFile { path_buf, .. } => Some(path_buf), - _ => None, - }) - .for_each(|path_buf| { - let path = path_buf.as_path(); - let handle = server.load_untyped(path); - assets.0.insert(handle); - }); -} - -fn manage_audio_ui( - mut events: EventReader>, - root: Query, Without)>, - mut commands: Commands, - server: Res, -) { - events - .iter() - .filter_map(|event| match event { - AssetEvent::Created { handle } => { - let asset_path = server - .get_handle_path(handle.clone()) - .expect("Fetch Asset Path"); - let name = get_fname(asset_path, &[".ogg"]); - Some((handle.clone(), String::from(name))) - } - _ => None, - }) - .for_each(|(handle, name)| { - commands - .spawn(( - Name::new(name), - ButtonBundle { ..default() }, - AudioClipsUi, - AudioBundle { - source: handle.clone(), - settings: PlaybackSettings { - mode: bevy::audio::PlaybackMode::Loop, + }; + let settings = PlaybackSettings { + mode: PlaybackMode::Loop, paused: true, ..default() - }, - }, - )) - .set_parent(root.single()); - }); -} + }; + parent.spawn(( + AudioSourceBundle { + source: handle, + settings, + }, + ButtonBundle { + style: Style { + border: UiRect::all(Val::Px(1.0)), + margin: UiRect::all(Val::Px(5.0)), + padding: UiRect::all(Val::Px(5.0)), + ..default() + }, + border_color: Color::BLACK.into(), + ..default() + }, + ui::Title { name }, + )); + }); + }; + let destroy = |commands: &mut Commands, handle: Handle| { + if let Some(entity) = current.iter().find_map(|(entity, current)| { + if *current == handle { + Some(entity) + } else { + None + } + }) { + commands.entity(entity).despawn_recursive(); + } + }; + match event { + AssetEvent::Created { handle } => { + info!("Asset created! {:?}", event); + create(&mut commands, handle.clone()); + } + AssetEvent::Removed { handle } => { + info!("Asset removed! {:?}", event); + destroy(&mut commands, handle.clone()); + } + AssetEvent::Modified { handle } => { + info!("Asset modified! {:?}", event); + destroy(&mut commands, handle.clone()); + create(&mut commands, handle.clone()); + } + } + }); + } -/// Play/Loop Audio -fn play_audio( - mut events: Query<(&Interaction, &AudioSink), (Changed, With)>, -) { - events - .iter_mut() - .for_each(|(interaction, sink)| match interaction { - Interaction::Pressed => { + pub fn play_audio( + events: Query<(Entity, &Interaction, &AudioSink), (With