bevy/crates/bevy_ui/src/flex/mod.rs
François c95d0ddfb9
remove panics when mixing UI and non UI entities in hierarchy (#1180)
* replace unwrap with logs when mixing UI and non UI entities in hierarchy
2021-01-01 16:06:40 -06:00

293 lines
10 KiB
Rust

mod convert;
use crate::{Node, Style};
use bevy_app::{EventReader, Events};
use bevy_ecs::{Changed, Entity, Flags, Local, Query, QueryFilter, Res, ResMut, With, Without};
use bevy_log::warn;
use bevy_math::Vec2;
use bevy_text::CalculatedSize;
use bevy_transform::prelude::{Children, Parent, Transform};
use bevy_utils::HashMap;
use bevy_window::{Window, WindowId, WindowScaleFactorChanged, Windows};
use std::fmt;
use stretch::{number::Number, Stretch};
pub struct FlexSurface {
entity_to_stretch: HashMap<Entity, stretch::node::Node>,
window_nodes: HashMap<WindowId, stretch::node::Node>,
stretch: Stretch,
}
impl fmt::Debug for FlexSurface {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("FlexSurface")
.field("entity_to_stretch", &self.entity_to_stretch)
.field("window_nodes", &self.window_nodes)
.finish()
}
}
impl Default for FlexSurface {
fn default() -> Self {
Self {
entity_to_stretch: Default::default(),
window_nodes: Default::default(),
stretch: Stretch::new(),
}
}
}
impl FlexSurface {
pub fn upsert_node(&mut self, entity: Entity, style: &Style, scale_factor: f64) {
let mut added = false;
let stretch = &mut self.stretch;
let stretch_style = convert::from_style(scale_factor, style);
let stretch_node = self.entity_to_stretch.entry(entity).or_insert_with(|| {
added = true;
stretch.new_node(stretch_style, Vec::new()).unwrap()
});
if !added {
self.stretch
.set_style(*stretch_node, stretch_style)
.unwrap();
}
}
pub fn upsert_leaf(
&mut self,
entity: Entity,
style: &Style,
calculated_size: CalculatedSize,
scale_factor: f64,
) {
let stretch = &mut self.stretch;
let stretch_style = convert::from_style(scale_factor, style);
let measure = Box::new(move |constraints: stretch::geometry::Size<Number>| {
let mut size = convert::from_f32_size(scale_factor, calculated_size.size);
match (constraints.width, constraints.height) {
(Number::Undefined, Number::Undefined) => {}
(Number::Defined(width), Number::Undefined) => {
size.height = width * size.height / size.width;
size.width = width;
}
(Number::Undefined, Number::Defined(height)) => {
size.width = height * size.width / size.height;
size.height = height;
}
(Number::Defined(width), Number::Defined(height)) => {
size.width = width;
size.height = height;
}
}
Ok(size)
});
if let Some(stretch_node) = self.entity_to_stretch.get(&entity) {
self.stretch
.set_style(*stretch_node, stretch_style)
.unwrap();
self.stretch
.set_measure(*stretch_node, Some(measure))
.unwrap();
} else {
let stretch_node = stretch.new_leaf(stretch_style, measure).unwrap();
self.entity_to_stretch.insert(entity, stretch_node);
}
}
pub fn update_children(&mut self, entity: Entity, children: &Children) {
let mut stretch_children = Vec::with_capacity(children.len());
for child in children.iter() {
if let Some(stretch_node) = self.entity_to_stretch.get(child) {
stretch_children.push(*stretch_node);
} else {
warn!(
"Unstyled child in a UI entity hierarchy. You are using an entity \
without UI components as a child of an entity with UI components, results may be unexpected."
);
}
}
let stretch_node = self.entity_to_stretch.get(&entity).unwrap();
self.stretch
.set_children(*stretch_node, stretch_children)
.unwrap();
}
pub fn update_window(&mut self, window: &Window) {
let stretch = &mut self.stretch;
let node = self.window_nodes.entry(window.id()).or_insert_with(|| {
stretch
.new_node(stretch::style::Style::default(), Vec::new())
.unwrap()
});
stretch
.set_style(
*node,
stretch::style::Style {
size: stretch::geometry::Size {
width: stretch::style::Dimension::Points(window.physical_width() as f32),
height: stretch::style::Dimension::Points(window.physical_height() as f32),
},
..Default::default()
},
)
.unwrap();
}
pub fn set_window_children(
&mut self,
window_id: WindowId,
children: impl Iterator<Item = Entity>,
) {
let stretch_node = self.window_nodes.get(&window_id).unwrap();
let child_nodes = children
.map(|e| *self.entity_to_stretch.get(&e).unwrap())
.collect::<Vec<stretch::node::Node>>();
self.stretch
.set_children(*stretch_node, child_nodes)
.unwrap();
}
pub fn compute_window_layouts(&mut self) {
for window_node in self.window_nodes.values() {
self.stretch
.compute_layout(*window_node, stretch::geometry::Size::undefined())
.unwrap();
}
}
pub fn get_layout(&self, entity: Entity) -> Result<&stretch::result::Layout, FlexError> {
if let Some(stretch_node) = self.entity_to_stretch.get(&entity) {
self.stretch
.layout(*stretch_node)
.map_err(FlexError::StretchError)
} else {
warn!(
"Styled child in a non-UI entity hierarchy. You are using an entity \
with UI components as a child of an entity without UI components, results may be unexpected."
);
Err(FlexError::InvalidHierarchy)
}
}
}
#[derive(Debug)]
pub enum FlexError {
InvalidHierarchy,
StretchError(stretch::Error),
}
// SAFE: as long as MeasureFunc is Send + Sync. https://github.com/vislyhq/stretch/issues/69
unsafe impl Send for FlexSurface {}
unsafe impl Sync for FlexSurface {}
#[allow(clippy::too_many_arguments)]
pub fn flex_node_system(
windows: Res<Windows>,
mut scale_factor_reader: Local<EventReader<WindowScaleFactorChanged>>,
scale_factor_events: Res<Events<WindowScaleFactorChanged>>,
mut flex_surface: ResMut<FlexSurface>,
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
node_query: Query<(Entity, &Style, Option<&CalculatedSize>), (With<Node>, Changed<Style>)>,
full_node_query: Query<(Entity, &Style, Option<&CalculatedSize>), With<Node>>,
changed_size_query: Query<
(Entity, &Style, &CalculatedSize),
(With<Node>, Changed<CalculatedSize>),
>,
children_query: Query<(Entity, &Children), (With<Node>, Changed<Children>)>,
mut node_transform_query: Query<(
Entity,
&mut Node,
&mut Transform,
Option<&Parent>,
Flags<Parent>,
Flags<Transform>,
)>,
) {
// update window root nodes
for window in windows.iter() {
flex_surface.update_window(window);
}
// assume one window for time being...
let logical_to_physical_factor = if let Some(primary_window) = windows.get_primary() {
primary_window.scale_factor()
} else {
1.
};
if scale_factor_reader.latest(&scale_factor_events).is_some() {
update_changed(
&mut *flex_surface,
logical_to_physical_factor,
full_node_query,
);
} else {
update_changed(&mut *flex_surface, logical_to_physical_factor, node_query);
}
fn update_changed<F>(
flex_surface: &mut FlexSurface,
scaling_factor: f64,
query: Query<(Entity, &Style, Option<&CalculatedSize>), F>,
) where
F: QueryFilter,
{
// update changed nodes
for (entity, style, calculated_size) in query.iter() {
// TODO: remove node from old hierarchy if its root has changed
if let Some(calculated_size) = calculated_size {
flex_surface.upsert_leaf(entity, &style, *calculated_size, scaling_factor);
} else {
flex_surface.upsert_node(entity, &style, scaling_factor);
}
}
}
for (entity, style, calculated_size) in changed_size_query.iter() {
flex_surface.upsert_leaf(entity, &style, *calculated_size, logical_to_physical_factor);
}
// TODO: handle removed nodes
// update window children (for now assuming all Nodes live in the primary window)
if let Some(primary_window) = windows.get_primary() {
flex_surface.set_window_children(primary_window.id(), root_node_query.iter());
}
// update children
for (entity, children) in children_query.iter() {
flex_surface.update_children(entity, &children);
}
// compute layouts
flex_surface.compute_window_layouts();
let physical_to_logical_factor = 1. / logical_to_physical_factor;
let to_logical = |v| (physical_to_logical_factor * v as f64) as f32;
for (entity, mut node, mut transform, parent, parent_flags, transform_flags) in
node_transform_query.iter_mut()
{
let layout = flex_surface.get_layout(entity).unwrap();
node.size = Vec2::new(
to_logical(layout.size.width),
to_logical(layout.size.height),
);
let position = &mut transform.translation;
position.x = to_logical(layout.location.x + layout.size.width / 2.0);
position.y = to_logical(layout.location.y + layout.size.height / 2.0);
if parent_flags.changed() || transform_flags.changed() {
if let Some(parent) = parent {
if let Ok(parent_layout) = flex_surface.get_layout(parent.0) {
position.x -= to_logical(parent_layout.size.width / 2.0);
position.y -= to_logical(parent_layout.size.height / 2.0);
}
}
}
}
}