Oh finally figured out growing UI containers; sheesh.

main
Elijah Voigt 2 years ago
parent eef3acdade
commit a2a4b11087

@ -1,26 +0,0 @@
# Exploration
## Inspectors
### Model Inspector
- [ ] Construct Scene from Nodes/Meshes (not auto-scene builder)
- [ ] Show debug info about selected model
- [ ] Wireframe view
- [ ] Automatic tighter bounding box for selection
### Audio Inspector
- [ ] UI for selecting sound
- [ ] Play/Pause/Volume
- [x] Load sounds
- [ ] Scrolling list of sounds
## WASM
- [ ] Build and run using model/text inspector
- https://github.com/bevyengine/bevy/blob/main/examples/README.md#wasm
## Text Inspector
- [ ] Performance improvements?

@ -5,9 +5,10 @@
// TODO: // TODO:
// * Tree Organization: GLTF contains Animations and Scenes // * Tree Organization: GLTF contains Animations and Scenes
// * Camera can only select one at a time. // * Camera can only select one at a time.
// * (easy) Better Colorscheme // * (hard) Better Colorscheme
// * (medium) Visual errors for bad GLTFs // * (medium) Visual errors for bad GLTFs
// * (medium) Collapsable containers (Gltfs, Animations, Scenes, Audio Clips, Cameras) // * (medium) Collapsable containers (Gltfs, Animations, Scenes, Audio Clips, Cameras)
// * (medium) Show/hide entire UI
// * (medium) Spawn clicked scene // * (medium) Spawn clicked scene
// * (medium) Play clicked animation // * (medium) Play clicked animation
// * (idea) Use enum instead of markers for exclusive UI // * (idea) Use enum instead of markers for exclusive UI
@ -104,6 +105,7 @@ fn initialize_ui(mut commands: Commands) {
Name::new("GLTFs"), Name::new("GLTFs"),
NodeBundle { ..default() }, NodeBundle { ..default() },
GltfsUi, GltfsUi,
UiCollapse::Show,
)) ))
.with_children(|parent| { .with_children(|parent| {
parent.spawn(( parent.spawn((
@ -111,18 +113,21 @@ fn initialize_ui(mut commands: Commands) {
Name::new("Scenes"), Name::new("Scenes"),
NodeBundle { ..default() }, NodeBundle { ..default() },
ScenesUi, ScenesUi,
UiCollapse::Show,
)); ));
parent.spawn(( parent.spawn((
GameUiList, GameUiList,
Name::new("Cameras"), Name::new("Cameras"),
NodeBundle { ..default() }, NodeBundle { ..default() },
CamerasUi, CamerasUi,
UiCollapse::Show,
)); ));
parent.spawn(( parent.spawn((
GameUiList, GameUiList,
Name::new("Animations"), Name::new("Animations"),
NodeBundle { ..default() }, NodeBundle { ..default() },
AnimationsUi, AnimationsUi,
UiCollapse::Show,
)); ));
}); });
parent.spawn(( parent.spawn((
@ -130,6 +135,7 @@ fn initialize_ui(mut commands: Commands) {
Name::new("Audio Clips"), Name::new("Audio Clips"),
NodeBundle { ..default() }, NodeBundle { ..default() },
AudioClipsUi, AudioClipsUi,
UiCollapse::Show,
)); ));
}); });
} }
@ -358,7 +364,7 @@ fn load_audio(
fn manage_audio_ui( fn manage_audio_ui(
mut events: EventReader<AssetEvent<AudioSource>>, mut events: EventReader<AssetEvent<AudioSource>>,
root: Query<Entity, (With<AudioClipsUi>, Without<UiRef<Handle<AudioSource>>>)>, root: Query<Entity, (With<AudioClipsUi>, Without<AudioSink>)>,
mut commands: Commands, mut commands: Commands,
server: Res<AssetServer>, server: Res<AssetServer>,
) { ) {

@ -1,4 +1,8 @@
use bevy::{input::mouse::MouseWheel, prelude::*}; use bevy::{
input::{keyboard::KeyboardInput, ButtonState},
prelude::*,
window::PrimaryWindow,
};
fn main() { fn main() {
App::new() App::new()
@ -10,92 +14,209 @@ fn main() {
}), }),
..default() ..default()
})) }))
.add_systems(Startup, (init,)) .init_resource::<Icon>()
.add_systems(Update, (scroll,)) .add_systems(Startup, (init, init_cursors, init_container))
.add_systems(Update, (cursors, container))
.run(); .run();
} }
#[derive(Component)] const CURSORS: [CursorIcon; 35] = [
struct ScrollingList; CursorIcon::Default,
CursorIcon::Crosshair,
CursorIcon::Hand,
CursorIcon::Arrow,
CursorIcon::Move,
CursorIcon::Text,
CursorIcon::Wait,
CursorIcon::Help,
CursorIcon::Progress,
CursorIcon::NotAllowed,
CursorIcon::ContextMenu,
CursorIcon::Cell,
CursorIcon::VerticalText,
CursorIcon::Alias,
CursorIcon::Copy,
CursorIcon::NoDrop,
CursorIcon::Grab,
CursorIcon::Grabbing,
CursorIcon::AllScroll,
CursorIcon::ZoomIn,
CursorIcon::ZoomOut,
CursorIcon::EResize,
CursorIcon::NResize,
CursorIcon::NeResize,
CursorIcon::NwResize,
CursorIcon::SResize,
CursorIcon::SeResize,
CursorIcon::SwResize,
CursorIcon::WResize,
CursorIcon::EwResize,
CursorIcon::NsResize,
CursorIcon::NeswResize,
CursorIcon::NwseResize,
CursorIcon::ColResize,
CursorIcon::RowResize,
];
#[derive(Debug, Component, Resource, Default)]
struct Icon(CursorIcon);
fn init(mut commands: Commands) { fn init(mut commands: Commands) {
info!("Spawning camera"); info!("Spawning camera");
commands.spawn(Camera2dBundle { ..default() }); commands.spawn((
Camera2dBundle { ..default() },
UiCameraConfig { show_ui: true },
));
}
info!("Initializing UI"); fn init_cursors(mut commands: Commands) {
info!("Spawning Cursor Icons");
commands commands
.spawn(NodeBundle { .spawn(NodeBundle {
background_color: BackgroundColor(Color::WHITE),
style: Style { style: Style {
justify_content: JustifyContent::Center, flex_direction: FlexDirection::Column,
width: Val::Percent(90.0),
height: Val::Percent(90.0),
overflow: Overflow::clip(),
..default() ..default()
}, },
background_color: BackgroundColor(Color::BLACK),
..default() ..default()
}) })
.with_children(|parent| { .with_children(|parent| {
parent.spawn(
TextBundle::from_section("Cursor Icons", TextStyle { ..default() })
.with_style(Style {
max_width: Val::Percent(100.0),
justify_self: JustifySelf::Stretch,
..default()
})
.with_background_color(Color::PINK),
);
parent parent
.spawn(( .spawn(NodeBundle {
NodeBundle {
background_color: BackgroundColor(Color::OLIVE),
style: Style { style: Style {
align_items: AlignItems::FlexStart,
flex_direction: FlexDirection::Column,
flex_wrap: FlexWrap::Wrap, flex_wrap: FlexWrap::Wrap,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceAround,
width: Val::Auto,
height: Val::Auto,
max_width: Val::Auto,
max_height: Val::Auto,
..default() ..default()
}, },
..default() ..default()
}, })
ScrollingList,
))
.with_children(|parent| { .with_children(|parent| {
info!("Initializing Child Element"); CURSORS.iter().for_each(|&icon| {
let colors = [ parent
Color::ORANGE, .spawn((
Color::BLUE, ButtonBundle {
Color::GREEN,
Color::SALMON,
Color::SEA_GREEN,
Color::MAROON,
Color::ORANGE,
Color::BLUE,
Color::GREEN,
Color::SALMON,
Color::SEA_GREEN,
Color::MAROON,
];
for i in 0..12 {
parent.spawn(NodeBundle {
background_color: BackgroundColor(colors[i]),
style: Style { style: Style {
width: Val::Px(256.0),
height: Val::Px(256.0),
padding: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)),
margin: UiRect::all(Val::Px(5.0)),
border: UiRect::all(Val::Px(2.0)),
..default() ..default()
}, },
background_color: BackgroundColor(Color::GRAY),
border_color: BorderColor(Color::WHITE),
..default() ..default()
},
Icon(icon),
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
format!("{:?}", icon),
TextStyle { ..default() },
));
});
}); });
}
}); });
}); });
} }
fn scroll( fn cursors(
mut scroll_evr: EventReader<MouseWheel>, events: Query<(&Interaction, &Icon), (Changed<Interaction>, With<Button>)>,
mut query: Query<&mut Style, With<ScrollingList>>, mut primary_window: Query<&mut Window, With<PrimaryWindow>>,
mut curr: ResMut<Icon>,
) { ) {
for ev in scroll_evr.iter() { events.iter().for_each(|(&interaction, &ref icon)| {
for mut s in query.iter_mut() { let mut window = primary_window.single_mut();
s.top = match s.top {
Val::Px(current) => Val::Px(current + (ev.y * 5.0)), match interaction {
_ => Val::Px(0.0), Interaction::Hovered => {
}; (*window).cursor.icon = icon.0.clone();
}
Interaction::Pressed => {
curr.0 = icon.0.clone();
}
Interaction::None => {
(*window).cursor.icon = curr.0.clone();
} }
} }
})
}
#[derive(Debug, Component)]
struct Container;
fn init_container(mut commands: Commands) {
commands
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::Column,
..default()
},
..default()
})
.with_children(|parent| {
parent.spawn(
TextBundle::from_section("Grow/Shrink Container", TextStyle { ..default() })
.with_background_color(Color::PURPLE),
);
parent.spawn((
Container,
NodeBundle {
style: Style {
flex_direction: FlexDirection::Row,
flex_wrap: FlexWrap::Wrap,
max_width: Val::Px(250.0),
..default()
},
background_color: BackgroundColor(Color::PINK),
..default()
},
));
});
}
fn container(
mut events: EventReader<KeyboardInput>,
mut commands: Commands,
root: Query<Entity, With<Container>>,
children: Query<&Children, With<Container>>,
) {
events.iter().for_each(
|KeyboardInput {
key_code, state, ..
}| {
match (key_code, state) {
(Some(KeyCode::Up), ButtonState::Pressed) => {
commands.entity(root.single()).with_children(|parent| {
parent.spawn(NodeBundle {
style: Style {
width: Val::Px(10.0),
height: Val::Px(10.0),
padding: UiRect::all(Val::Px(10.0)),
margin: UiRect::all(Val::Px(10.0)),
..default()
},
background_color: BackgroundColor(Color::PURPLE),
..default()
});
});
}
(Some(KeyCode::Down), ButtonState::Pressed) => {
children.single().iter().last().iter().for_each(|&&child| {
commands.entity(child).despawn_recursive();
});
}
_ => (),
}
},
)
} }

@ -9,16 +9,118 @@ impl Plugin for GameUiPlugin {
app.add_systems( app.add_systems(
Update, Update,
( (
manage_ui_list, // UI lists
manage_ui_set, init_ui_list,
manage_ui_button, // UI Set
manage_cursor, init_ui_set,
// Buttons
init_ui_button,
manage_button_interaction, manage_button_interaction,
manage_button_title,
// Collapse systems
init_ui_collapse,
manage_collapse,
toggle_collapse,
// Cursor
manage_cursor,
), ),
); );
} }
} }
/// Ui Navigation Element
#[derive(Debug, Component)]
struct UiNav;
/// Collapsed UI element
#[derive(Debug, Component)]
pub enum UiCollapse {
Show,
Hide,
}
/// Marker for UiCollapse Button
#[derive(Debug, Component)]
struct UiCollapseButton;
/// When a UiCollapse entity is created, populate the open/close button
fn init_ui_collapse(
events: Query<(Entity, &UiCollapse), Added<UiCollapse>>,
mut commands: Commands,
) {
events.iter().for_each(|(entity, collapse)| {
commands.entity(entity).with_children(|parent| {
let name = match collapse {
UiCollapse::Show => Name::new("Hide"),
UiCollapse::Hide => Name::new("Show"),
};
parent.spawn((
GameUiButton,
name,
UiCollapseButton,
NodeBundle {
style: Style {
top: Val::Px(0.0),
right: Val::Px(0.0),
..default()
},
..default()
},
));
});
});
}
fn manage_collapse(
events: Query<(&UiCollapse, &Children), Changed<UiCollapse>>,
mut visibility: Query<(&mut Visibility, &mut Style), Without<UiCollapseButton>>,
) {
events.iter().for_each(|(collapse, children)| {
children.iter().for_each(|&child| {
if let Ok((mut vis, mut style)) = visibility.get_mut(child) {
match collapse {
UiCollapse::Show => {
*vis = Visibility::Inherited;
style.display = Display::Flex;
}
UiCollapse::Hide => {
*vis = Visibility::Hidden;
style.display = Display::None;
}
};
}
});
});
}
fn toggle_collapse(
mut events: Query<
(&Interaction, &Parent, &mut Name),
(Changed<Interaction>, With<UiCollapseButton>),
>,
mut collapses: Query<&mut UiCollapse>,
) {
events
.iter_mut()
.for_each(|(interaction, parent, mut name)| match interaction {
Interaction::Pressed => {
if let Ok(mut collapse) = collapses.get_mut(parent.get()) {
match *collapse {
UiCollapse::Show => {
*name = Name::new("Show");
*collapse = UiCollapse::Hide;
}
UiCollapse::Hide => {
*name = Name::new("Hide");
*collapse = UiCollapse::Show
}
}
}
}
_ => (),
});
}
/// Describes the state of an element /// Describes the state of an element
#[derive(Debug, Component)] #[derive(Debug, Component)]
pub enum UiElementState { pub enum UiElementState {
@ -33,20 +135,23 @@ pub enum UiElementState {
pub struct GameUiList; pub struct GameUiList;
/// Manage UI Lists: lists of UI entities. /// Manage UI Lists: lists of UI entities.
fn manage_ui_list(events: Query<(Entity, &Name), Added<GameUiList>>, mut commands: Commands) { fn init_ui_list(events: Query<(Entity, &Name), Added<GameUiList>>, mut commands: Commands) {
events.iter().for_each(|(entity, name)| { events.iter().for_each(|(entity, name)| {
commands commands
.entity(entity) .entity(entity)
.insert(NodeBundle { .insert(NodeBundle {
style: Style { style: Style {
// width: Val::Px(100.0), // align_content: AlignContent::FlexStart,
margin: UiRect::all(Val::Px(2.0)), align_items: AlignItems::Stretch,
padding: UiRect::all(Val::Px(2.0)),
border: UiRect::all(Val::Px(2.0)), border: UiRect::all(Val::Px(2.0)),
flex_direction: FlexDirection::Column, flex_direction: FlexDirection::Column,
align_items: AlignItems::Stretch,
justify_items: JustifyItems::Center, justify_items: JustifyItems::Center,
align_content: AlignContent::FlexStart, margin: UiRect::all(Val::Px(2.0)),
padding: UiRect::all(Val::Px(2.0)),
// overflow: Overflow {
// x: OverflowAxis::Clip,
// y: OverflowAxis::Clip,
// },
..default() ..default()
}, },
background_color: BackgroundColor(Color::RED), background_color: BackgroundColor(Color::RED),
@ -54,7 +159,13 @@ fn manage_ui_list(events: Query<(Entity, &Name), Added<GameUiList>>, mut command
..default() ..default()
}) })
.with_children(|parent| { .with_children(|parent| {
parent.spawn(TextBundle::from_section(name, TextStyle { ..default() })); parent.spawn(
TextBundle::from_section(name, TextStyle { ..default() }).with_style(Style {
top: Val::Px(0.0),
left: Val::Px(0.0),
..default()
}),
);
}); });
}); });
} }
@ -64,20 +175,23 @@ fn manage_ui_list(events: Query<(Entity, &Name), Added<GameUiList>>, mut command
pub struct GameUiSet; pub struct GameUiSet;
/// Manage UI Sets: collections of UI entities. /// Manage UI Sets: collections of UI entities.
fn manage_ui_set(events: Query<(Entity, &Name), Added<GameUiSet>>, mut commands: Commands) { fn init_ui_set(events: Query<(Entity, &Name), Added<GameUiSet>>, mut commands: Commands) {
events.iter().for_each(|(entity, name)| { events.iter().for_each(|(entity, name)| {
commands commands
.entity(entity) .entity(entity)
.insert(NodeBundle { .insert(NodeBundle {
style: Style { style: Style {
width: Val::Px(200.0), // align_content: AlignContent::FlexStart,
margin: UiRect::all(Val::Px(2.0)),
padding: UiRect::all(Val::Px(2.0)),
border: UiRect::all(Val::Px(2.0)),
align_items: AlignItems::FlexStart, align_items: AlignItems::FlexStart,
align_content: AlignContent::FlexStart, border: UiRect::all(Val::Px(2.0)),
flex_direction: FlexDirection::Row, flex_direction: FlexDirection::Row,
flex_wrap: FlexWrap::Wrap, flex_wrap: FlexWrap::Wrap,
margin: UiRect::all(Val::Px(2.0)),
padding: UiRect::all(Val::Px(2.0)),
// overflow: Overflow {
// x: OverflowAxis::Clip,
// y: OverflowAxis::Clip,
// },
..default() ..default()
}, },
background_color: BackgroundColor(Color::BLUE), background_color: BackgroundColor(Color::BLUE),
@ -85,7 +199,13 @@ fn manage_ui_set(events: Query<(Entity, &Name), Added<GameUiSet>>, mut commands:
..default() ..default()
}) })
.with_children(|parent| { .with_children(|parent| {
parent.spawn(TextBundle::from_section(name, TextStyle { ..default() })); parent.spawn(
TextBundle::from_section(name, TextStyle { ..default() }).with_style(Style {
top: Val::Px(0.0),
left: Val::Px(0.0),
..default()
}),
);
}); });
}); });
} }
@ -95,7 +215,7 @@ fn manage_ui_set(events: Query<(Entity, &Name), Added<GameUiSet>>, mut commands:
pub struct GameUiButton; pub struct GameUiButton;
/// Manage UI Buttons. interactive buttons. /// Manage UI Buttons. interactive buttons.
fn manage_ui_button(events: Query<(Entity, &Name), Added<GameUiButton>>, mut commands: Commands) { fn init_ui_button(events: Query<(Entity, &Name), Added<GameUiButton>>, mut commands: Commands) {
events.iter().for_each(|(entity, name)| { events.iter().for_each(|(entity, name)| {
commands commands
.entity(entity) .entity(entity)
@ -120,23 +240,7 @@ fn manage_ui_button(events: Query<(Entity, &Name), Added<GameUiButton>>, mut com
}); });
} }
/// Manage the cursor icon for better immersion /// Manage button style for interactivity
fn manage_cursor(
mut primary_window: Query<&mut Window, With<PrimaryWindow>>,
events: Query<&Interaction, (Changed<Interaction>, With<Button>)>,
) {
if !events.is_empty() {
let mut window = primary_window.single_mut();
window.cursor.icon = events
.iter()
.find(|&event| *event != Interaction::None)
.map_or(CursorIcon::Default, |event| match event {
Interaction::Hovered | Interaction::Pressed => CursorIcon::Hand,
Interaction::None => CursorIcon::Help, // Shouldn't be reachable
});
}
}
fn manage_button_interaction( fn manage_button_interaction(
mut events: Query< mut events: Query<
(&Interaction, &UiElementState, &mut BackgroundColor), (&Interaction, &UiElementState, &mut BackgroundColor),
@ -158,3 +262,33 @@ fn manage_button_interaction(
} }
}); });
} }
fn manage_button_title(
events: Query<(&Name, &Children), (Changed<Name>, With<Button>)>,
mut texts: Query<&mut Text, With<Parent>>,
) {
events.iter().for_each(|(name, children)| {
children.iter().for_each(|&child| {
if let Ok(mut text) = texts.get_mut(child) {
text.sections[0].value = name.into();
}
});
});
}
/// Manage the cursor icon for better immersion
fn manage_cursor(
mut primary_window: Query<&mut Window, With<PrimaryWindow>>,
events: Query<&Interaction, (Changed<Interaction>, With<Button>)>,
) {
if !events.is_empty() {
let mut window = primary_window.single_mut();
window.cursor.icon = events
.iter()
.find(|&event| *event != Interaction::None)
.map_or(CursorIcon::Default, |event| match event {
Interaction::Hovered | Interaction::Pressed => CursorIcon::Hand,
Interaction::None => CursorIcon::Help, // Shouldn't be reachable
});
}
}

14
tmp

@ -1,14 +0,0 @@
if *duration > Duration::ZERO {
let mut text = texts.single_mut();
let total_sections = text.sections.len();
*duration = duration.saturating_sub(time.delta());
for (idx, section) in text.sections.iter_mut().enumerate() {
let ratio = ((idx + 1) as f32) / (total_sections as f32);
let cursor = 1.0 - ((*duration).as_secs_f32() / 30.0);
let alpha = if cursor > ratio { 1.0 } else { 0.0 };
section.style.color.set_a(alpha);
}
}
Loading…
Cancel
Save