ui: focus/click/hover system. initial buttons

This commit is contained in:
Carter Anderson 2020-07-18 14:08:46 -07:00
parent 19fe299f5a
commit fe1adb6cf6
18 changed files with 363 additions and 60 deletions

View file

@ -176,6 +176,10 @@ path = "examples/shader/shader_custom_material.rs"
name = "shader_defs"
path = "examples/shader/shader_defs.rs"
[[example]]
name = "button"
path = "examples/ui/button.rs"
[[example]]
name = "text"
path = "examples/ui/text.rs"

View file

@ -14,7 +14,11 @@
// modified by Bevy contributors
use core::{marker::PhantomData, ptr::NonNull, ops::{Deref, DerefMut}};
use core::{
marker::PhantomData,
ops::{Deref, DerefMut},
ptr::NonNull,
};
use crate::{archetype::Archetype, Component, Entity};
@ -615,7 +619,7 @@ macro_rules! tuple_impl {
let ($($name,)*) = self;
($($name.next(),)*)
}
unsafe fn should_skip(&self) -> bool {
#[allow(non_snake_case)]
let ($($name,)*) = self;
@ -702,58 +706,65 @@ impl<'a, T: Component> Fetch<'a> for FetchMut<T> {
}
#[allow(missing_docs)]
pub struct Changed<T, Q>(PhantomData<(Q, fn(T))>);
pub struct Changed<'a, T> {
value: &'a T,
}
impl<T: Component, Q: Query> Query for Changed<T, Q> {
type Fetch = FetchChanged<T, Q::Fetch>;
impl<'a, T: Component> Deref for Changed<'a, T> {
type Target = T;
fn deref(&self) -> &T {
self.value
}
}
impl<'a, T: Component> Query for Changed<'a, T> {
type Fetch = FetchChanged<T>;
}
#[doc(hidden)]
pub struct FetchChanged<T, F>(F, PhantomData<fn(T)>, NonNull<bool>);
pub struct FetchChanged<T>(NonNull<T>, NonNull<bool>);
impl<'a, T: Component, F: Fetch<'a>> Fetch<'a> for FetchChanged<T, F> {
type Item = F::Item;
impl<'a, T: Component> Fetch<'a> for FetchChanged<T> {
type Item = Changed<'a, T>;
fn access(archetype: &Archetype) -> Option<Access> {
if archetype.has::<T>() {
F::access(archetype)
Some(Access::Read)
} else {
None
}
}
fn borrow(archetype: &Archetype) {
F::borrow(archetype)
archetype.borrow::<T>();
}
unsafe fn get(archetype: &'a Archetype, offset: usize) -> Option<Self> {
if !archetype.has::<T>() {
return None;
}
Some(Self(
F::get(archetype, offset)?,
PhantomData,
NonNull::new_unchecked(archetype.get_modified::<T>()?.as_ptr().add(offset)),
))
let components = NonNull::new_unchecked(archetype.get::<T>()?.as_ptr().add(offset));
let modified = NonNull::new_unchecked(archetype.get_modified::<T>()?.as_ptr().add(offset));
Some(Self(components, modified))
}
fn release(archetype: &Archetype) {
F::release(archetype)
archetype.release::<T>();
}
unsafe fn should_skip(&self) -> bool {
// skip if the current item wasn't changed
!*self.2.as_ref() || self.0.should_skip()
!*self.1.as_ref()
}
unsafe fn next(&mut self) -> F::Item {
self.2 = NonNull::new_unchecked(self.2.as_ptr().add(1));
self.0.next()
#[inline]
unsafe fn next(&mut self) -> Self::Item {
self.1 = NonNull::new_unchecked(self.1.as_ptr().add(1));
let value = self.0.as_ptr();
self.0 = NonNull::new_unchecked(value.add(1));
Changed { value: &*value }
}
}
#[cfg(test)]
mod tests {
use crate::{Entity, World, Mut, Changed};
use std::{vec::Vec, vec};
use crate::{Changed, Entity, Mut, World};
use std::{vec, vec::Vec};
use super::*;
@ -784,8 +795,9 @@ mod tests {
fn get_changed_a(world: &World) -> Vec<Entity> {
world
.query::<Changed<A, Entity>>()
.query::<(Changed<A>, Entity)>()
.iter()
.map(|(_a, e)| e)
.collect::<Vec<Entity>>()
};
@ -823,14 +835,15 @@ mod tests {
world.clear_trackers();
assert!(world
.query::<Changed<A, Entity>>()
.query::<(Changed<A>, Entity)>()
.iter()
.map(|(_a, e)| e)
.collect::<Vec<Entity>>()
.is_empty());
}
#[test]
fn nested_changed_query() {
fn multiple_changed_query() {
let mut world = World::default();
world.spawn((A(0), B(0)));
let e2 = world.spawn((A(0), B(0)));
@ -845,16 +858,10 @@ mod tests {
}
let a_b_changed = world
.query::<Changed<A, Changed<B, Entity>>>()
.query::<(Changed<A>, Changed<B>, Entity)>()
.iter()
.map(|(_a, _b, e)| e)
.collect::<Vec<Entity>>();
assert_eq!(a_b_changed, vec![e2]);
let a_b_changed_tuple = world
.query::<(Changed<A, Entity>, Changed<B, &B>)>()
.iter()
.map(|(e, _b)| e)
.collect::<Vec<Entity>>();
assert_eq!(a_b_changed_tuple, vec![e2]);
}
}
}

View file

@ -16,6 +16,6 @@ pub mod prelude {
Commands, IntoForEachSystem, IntoQuerySystem, IntoThreadLocalSystem, Query, System,
},
world::WorldBuilderSource,
Bundle, Component, Entity, Ref, RefMut, With, Without, World,
Bundle, Changed, Component, Entity, Ref, RefMut, With, Without, World,
};
}

View file

@ -52,6 +52,8 @@ impl ParallelExecutor {
executor_stage.run(world, resources, stage_systems);
}
}
world.clear_trackers();
}
}

View file

@ -131,6 +131,8 @@ impl Schedule {
}
}
}
world.clear_trackers();
}
// TODO: move this code to ParallelExecutor

View file

@ -150,6 +150,8 @@ impl<'a, Q: HecsQuery> Query<'a, Q> {
self.world.query::<Q>()
}
/// Gets a reference to the entity's component of the given type. This will fail if the entity does not have
/// the given component type or if the given component type does not match this query.
pub fn get<T: Component>(&self, entity: Entity) -> Result<Ref<'_, T>, QueryComponentError> {
if let Some(location) = self.world.get_entity_location(entity) {
if self
@ -170,6 +172,8 @@ impl<'a, Q: HecsQuery> Query<'a, Q> {
}
}
/// Gets a mutable reference to the entity's component of the given type. This will fail if the entity does not have
/// the given component type or if the given component type does not match this query.
pub fn get_mut<T: Component>(
&self,
entity: Entity,
@ -192,6 +196,18 @@ impl<'a, Q: HecsQuery> Query<'a, Q> {
))
}
}
/// Sets the entity's component to the given value. This will fail if the entity does not already have
/// the given component type or if the given component type does not match this query.
pub fn set<T: Component>(
&self,
entity: Entity,
component: T,
) -> Result<(), QueryComponentError> {
let mut current = self.get_mut::<T>(entity)?;
*current = component;
Ok(())
}
}
struct QuerySystemState {

View file

@ -23,7 +23,7 @@ unsafe impl Byteable for Color {}
impl Color {
pub const WHITE: Color = Color::rgb(1.0, 1.0, 1.0);
pub const BLACK: Color = Color::rgb(0.0, 1.0, 0.0);
pub const BLACK: Color = Color::rgb(0.0, 0.0, 0.0);
pub const RED: Color = Color::rgb(1.0, 0.0, 0.0);
pub const GREEN: Color = Color::rgb(0.0, 1.0, 0.0);
pub const BLUE: Color = Color::rgb(0.0, 0.0, 1.0);

View file

@ -7,9 +7,11 @@ edition = "2018"
[dependencies]
bevy_app = { path = "../bevy_app" }
bevy_asset = { path = "../bevy_asset" }
bevy_core = { path = "../bevy_core" }
bevy_type_registry = { path = "../bevy_type_registry" }
bevy_derive = { path = "../bevy_derive" }
bevy_ecs = { path = "../bevy_ecs" }
bevy_input = { path = "../bevy_input" }
bevy_sprite = { path = "../bevy_sprite" }
bevy_text = { path = "../bevy_text" }
bevy_transform = { path = "../bevy_transform" }

View file

@ -1,5 +1,9 @@
use super::Node;
use crate::{render::UI_PIPELINE_HANDLE, widget::Label};
use crate::{
render::UI_PIPELINE_HANDLE,
widget::{Button, Label},
Click, Hover, FocusPolicy,
};
use bevy_asset::Handle;
use bevy_ecs::Bundle;
use bevy_render::{
@ -11,7 +15,7 @@ use bevy_sprite::{ColorMaterial, QUAD_HANDLE};
use bevy_transform::prelude::{Rotation, Scale, Transform, Translation};
#[derive(Bundle)]
pub struct UiComponents {
pub struct NodeComponents {
pub node: Node,
pub mesh: Handle<Mesh>, // TODO: maybe abstract this out
pub material: Handle<ColorMaterial>,
@ -23,9 +27,9 @@ pub struct UiComponents {
pub scale: Scale,
}
impl Default for UiComponents {
impl Default for NodeComponents {
fn default() -> Self {
UiComponents {
NodeComponents {
mesh: QUAD_HANDLE,
render_pipelines: RenderPipelines::from_pipelines(vec![RenderPipeline::specialized(
UI_PIPELINE_HANDLE,
@ -83,3 +87,57 @@ impl Default for LabelComponents {
}
}
}
#[derive(Bundle)]
pub struct ButtonComponents {
pub node: Node,
pub button: Button,
pub click: Click,
pub hover: Hover,
pub focus_policy: FocusPolicy,
pub mesh: Handle<Mesh>, // TODO: maybe abstract this out
pub material: Handle<ColorMaterial>,
pub draw: Draw,
pub render_pipelines: RenderPipelines,
pub transform: Transform,
pub translation: Translation,
pub rotation: Rotation,
pub scale: Scale,
}
impl Default for ButtonComponents {
fn default() -> Self {
ButtonComponents {
button: Button,
click: Click::default(),
hover: Hover::default(),
focus_policy: FocusPolicy::default(),
mesh: QUAD_HANDLE,
render_pipelines: RenderPipelines::from_pipelines(vec![RenderPipeline::specialized(
UI_PIPELINE_HANDLE,
PipelineSpecialization {
dynamic_bindings: vec![
// Transform
DynamicBinding {
bind_group: 1,
binding: 0,
},
// Node_size
DynamicBinding {
bind_group: 1,
binding: 1,
},
],
..Default::default()
},
)]),
node: Default::default(),
material: Default::default(),
draw: Default::default(),
transform: Default::default(),
translation: Default::default(),
rotation: Default::default(),
scale: Default::default(),
}
}
}

133
crates/bevy_ui/src/focus.rs Normal file
View file

@ -0,0 +1,133 @@
use crate::Node;
use bevy_app::{EventReader, Events};
use bevy_core::FloatOrd;
use bevy_ecs::prelude::*;
use bevy_input::{mouse::MouseButton, Input};
use bevy_math::Vec2;
use bevy_transform::components::Transform;
use bevy_window::{CursorMoved, Windows};
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum Click {
Released,
Pressed,
}
impl Default for Click {
fn default() -> Self {
Click::Released
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum Hover {
Hovered,
NotHovered,
}
impl Default for Hover {
fn default() -> Self {
Hover::NotHovered
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum FocusPolicy {
Block,
Pass,
}
impl Default for FocusPolicy {
fn default() -> Self {
FocusPolicy::Block
}
}
#[derive(Default)]
pub struct State {
cursor_moved_event_reader: EventReader<CursorMoved>,
cursor_position: Vec2,
}
pub fn ui_focus_system(
mut state: Local<State>,
windows: Res<Windows>,
mouse_button_input: Res<Input<MouseButton>>,
cursor_moved_events: Res<Events<CursorMoved>>,
mut node_query: Query<(
&Node,
&Transform,
Option<&mut Click>,
Option<&mut Hover>,
Option<&FocusPolicy>,
)>,
) {
if let Some(cursor_moved) = state.cursor_moved_event_reader.latest(&cursor_moved_events) {
state.cursor_position = cursor_moved.position;
}
if mouse_button_input.just_released(MouseButton::Left) {
for (_node, _transform, click, _hover, _focus_policy) in &mut node_query.iter() {
if let Some(mut click) = click {
if *click == Click::Pressed {
*click = Click::Released;
}
}
}
}
let mouse_clicked = mouse_button_input.just_pressed(MouseButton::Left);
let window = windows.get_primary().unwrap();
let mut query_iter = node_query.iter();
let mut moused_over_z_sorted_nodes = query_iter
.iter()
.filter_map(|(node, transform, click, hover, focus_policy)| {
let position = transform.value.w_axis();
// TODO: ui transform is currently in world space, so we need to move it to ui space. we should make these transforms ui space
let ui_position = position.truncate().truncate()
+ Vec2::new(window.width as f32 / 2.0, window.height as f32 / 2.0);
let extents = node.size / 2.0;
let min = ui_position - extents;
let max = ui_position + extents;
// if the current cursor position is within the bounds of the node, consider it for clicking
if (min.x()..max.x()).contains(&state.cursor_position.x())
&& (min.y()..max.y()).contains(&state.cursor_position.y())
{
Some((focus_policy, click, hover, FloatOrd(position.z())))
} else {
if let Some(mut hover) = hover {
if *hover == Hover::Hovered {
*hover = Hover::NotHovered;
}
}
None
}
})
.collect::<Vec<_>>();
// TODO: sort by negative when we move back to a right handed coordinate system
moused_over_z_sorted_nodes.sort_by_key(|(_, _, _, z)| *z);
for (focus_policy, click, hover, _) in moused_over_z_sorted_nodes {
if mouse_clicked {
// only consider nodes with ClickState "clickable"
if let Some(mut click) = click {
if *click == Click::Released {
*click = Click::Pressed;
}
}
}
// only consider nodes with Hover "hoverable"
if let Some(mut hover) = hover {
if *hover == Hover::NotHovered {
*hover = Hover::Hovered;
}
}
match focus_policy.cloned().unwrap_or(FocusPolicy::Block) {
FocusPolicy::Block => {
break;
}
FocusPolicy::Pass => { /* allow the next node to be hovered/clicked */ }
}
}
}

View file

@ -1,24 +1,30 @@
mod anchors;
mod focus;
pub mod entity;
mod margins;
mod node;
mod render;
mod ui_update_system;
pub mod update;
pub mod widget;
pub use anchors::*;
pub use focus::*;
pub use margins::*;
pub use node::*;
pub use render::*;
pub use ui_update_system::*;
pub mod prelude {
pub use crate::{entity::*, widget::Label, Anchors, Margins, Node};
pub use crate::{
entity::*,
widget::{Button, Label},
Anchors, Click, Hover, Margins, Node,
};
}
use bevy_app::prelude::*;
use bevy_ecs::IntoQuerySystem;
use bevy_render::render_graph::RenderGraph;
use update::ui_update_system;
use widget::Label;
#[derive(Default)]
@ -26,7 +32,8 @@ pub struct UiPlugin;
impl AppPlugin for UiPlugin {
fn build(&self, app: &mut AppBuilder) {
app.add_system_to_stage(stage::POST_UPDATE, ui_update_system.system())
app.add_system_to_stage(stage::PRE_UPDATE, ui_focus_system.system())
.add_system_to_stage(stage::POST_UPDATE, ui_update_system.system())
.add_system_to_stage(stage::POST_UPDATE, Label::label_system.system())
.add_system_to_stage(bevy_render::stage::DRAW, Label::draw_label_system.system());

View file

@ -0,0 +1 @@
pub struct Button;

View file

@ -1,3 +1,5 @@
mod button;
mod label;
pub use button::*;
pub use label::*;

View file

@ -107,9 +107,13 @@ pub fn winit_runner(mut app: App) {
app.resources.get_mut::<Events<CursorMoved>>().unwrap();
let winit_windows = app.resources.get_mut::<WinitWindows>().unwrap();
let window_id = winit_windows.get_window_id(winit_window_id).unwrap();
let window = winit_windows.get_window(window_id).unwrap();
let inner_size = window.inner_size();
// move origin to bottom left
let y_position = inner_size.height as f32 - position.y as f32;
cursor_moved_events.send(CursorMoved {
id: window_id,
position: Vec2::new(position.x as f32, position.y as f32),
position: Vec2::new(position.x as f32, y_position as f32),
});
}
WindowEvent::MouseInput { state, button, .. } => {

65
examples/ui/button.rs Normal file
View file

@ -0,0 +1,65 @@
use bevy::prelude::*;
fn main() {
App::build()
.add_default_plugins()
.add_startup_system(setup.system())
.add_system(button_system.system())
.run();
}
fn button_system(
mut click_query: Query<(&Button, Changed<Click>)>,
mut hover_query: Query<(&Button, Changed<Hover>)>,
) {
for (_button, click) in &mut click_query.iter() {
match *click {
Click::Pressed => {
println!("pressed");
}
Click::Released => {
println!("released");
}
}
}
for (_button, hover) in &mut hover_query.iter() {
match *hover {
Hover::Hovered => {
println!("hovered");
}
Hover::NotHovered => {
println!("unhovered");
}
}
}
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
commands
// ui camera
.spawn(OrthographicCameraComponents::default())
.spawn(ButtonComponents {
node: Node::new(Anchors::BOTTOM_LEFT, Margins::new(10.0, 160.0, 10.0, 80.0)),
material: materials.add(Color::rgb(0.2, 0.8, 0.2).into()),
..Default::default()
})
.with_children(|parent| {
parent.spawn(LabelComponents {
node: Node::new(Anchors::CENTER, Margins::new(52.0, 10.0, 20.0, 20.0)),
label: Label {
text: "Button".to_string(),
font: asset_server.load("assets/fonts/FiraSans-Bold.ttf").unwrap(),
style: TextStyle {
font_size: 40.0,
color: Color::rgb(0.1, 0.1, 0.1),
},
},
..Default::default()
});
});
}

View file

@ -25,7 +25,7 @@ fn setup(
// ui camera
.spawn(OrthographicCameraComponents::default())
// root node
.spawn(UiComponents {
.spawn(NodeComponents {
node: Node::new(Anchors::FULL, Margins::default()),
material: materials.add(Color::NONE.into()),
..Default::default()
@ -33,7 +33,7 @@ fn setup(
.with_children(|parent| {
parent
// left vertical fill
.spawn(UiComponents {
.spawn(NodeComponents {
node: Node::new(Anchors::LEFT_FULL, Margins::new(10.0, 200.0, 10.0, 10.0)),
material: materials.add(Color::rgb(0.02, 0.02, 0.02).into()),
..Default::default()
@ -53,13 +53,13 @@ fn setup(
});
})
// right vertical fill
.spawn(UiComponents {
.spawn(NodeComponents {
node: Node::new(Anchors::RIGHT_FULL, Margins::new(10.0, 100.0, 100.0, 100.0)),
material: materials.add(Color::rgb(0.02, 0.02, 0.02).into()),
..Default::default()
})
// render order test: reddest in the back, whitest in the front
.spawn(UiComponents {
.spawn(NodeComponents {
node: Node::positioned(
Vec2::new(75.0, 60.0),
Anchors::CENTER,
@ -68,7 +68,7 @@ fn setup(
material: materials.add(Color::rgb(1.0, 0.0, 0.0).into()),
..Default::default()
})
.spawn(UiComponents {
.spawn(NodeComponents {
node: Node::positioned(
Vec2::new(50.0, 35.0),
Anchors::CENTER,
@ -77,7 +77,7 @@ fn setup(
material: materials.add(Color::rgb(1.0, 0.3, 0.3).into()),
..Default::default()
})
.spawn(UiComponents {
.spawn(NodeComponents {
node: Node::positioned(
Vec2::new(100.0, 85.0),
Anchors::CENTER,
@ -86,7 +86,7 @@ fn setup(
material: materials.add(Color::rgb(1.0, 0.5, 0.5).into()),
..Default::default()
})
.spawn(UiComponents {
.spawn(NodeComponents {
node: Node::positioned(
Vec2::new(150.0, 135.0),
Anchors::CENTER,
@ -96,7 +96,7 @@ fn setup(
..Default::default()
})
// parenting
.spawn(UiComponents {
.spawn(NodeComponents {
node: Node::positioned(
Vec2::new(210.0, 0.0),
Anchors::BOTTOM_LEFT,
@ -106,14 +106,14 @@ fn setup(
..Default::default()
})
.with_children(|parent| {
parent.spawn(UiComponents {
parent.spawn(NodeComponents {
node: Node::new(Anchors::FULL, Margins::new(20.0, 20.0, 20.0, 20.0)),
material: materials.add(Color::rgb(0.6, 0.6, 1.0).into()),
..Default::default()
});
})
// alpha test
.spawn(UiComponents {
.spawn(NodeComponents {
node: Node::positioned(
Vec2::new(200.0, 185.0),
Anchors::CENTER,
@ -123,7 +123,7 @@ fn setup(
..Default::default()
})
// texture
.spawn(UiComponents {
.spawn(NodeComponents {
node: Node::new(
Anchors::CENTER_TOP,
Margins::new(-250.0, 250.0, 510.0 * aspect, 10.0),

View file

@ -29,7 +29,7 @@ fn setup(mut commands: Commands, mut materials: ResMut<Assets<ColorMaterial>>) {
for i in 0..count {
// 2d camera
let cur = Vec2::new(1.0, 1.0) + prev;
commands.spawn(UiComponents {
commands.spawn(NodeComponents {
node: Node {
position: Vec2::new(75.0, 75.0) + cur,
anchors: Anchors::new(0.5, 0.5, 0.5, 0.5),