Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi window UI #8780

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1792,6 +1792,16 @@ description = "Illustrates how FontAtlases are populated (used to optimize text
category = "UI (User Interface)"
wasm = true

[[example]]
name = "multi_window_ui"
path = "examples/ui/multi_window_ui.rs"

[package.metadata.example.multi_window_ui]
name = "Multiple Window UI"
description = "Demonstrates multiple windows each with their own UI layout"
category = "UI (User Interface)"
wasm = false

[[example]]
name = "overflow"
path = "examples/ui/overflow.rs"
Expand Down
212 changes: 119 additions & 93 deletions crates/bevy_ui/src/focus.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiStack};
use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiStacks};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
change_detection::DetectChangesMut,
entity::Entity,
prelude::{Component, With},
query::WorldQuery,
reflect::ReflectComponent,
system::{Local, Query, Res},
system::{Local, Query, Res, ResMut, Resource},
};
use bevy_input::{mouse::MouseButton, touch::Touches, Input};
use bevy_math::Vec2;
use bevy_reflect::{
FromReflect, Reflect, ReflectDeserialize, ReflectFromReflect, ReflectSerialize,
};
use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::ComputedVisibility};
use bevy_render::{prelude::Camera, view::ComputedVisibility};
use bevy_transform::components::GlobalTransform;

use bevy_window::{PrimaryWindow, Window};
Expand Down Expand Up @@ -133,28 +133,81 @@ pub struct NodeQuery {
computed_visibility: Option<&'static ComputedVisibility>,
}

#[derive(Debug)]
pub struct CursorState {
/// Views that output to the window the cursor is above.
pub views: Vec<Entity>,
/// The cursor's relative postion within the window.
pub position: Vec2,
}

#[derive(Resource, Debug, Default)]
pub struct UiCursorOverride {
/// If set to some value, overrides the cursor position with a custom value.
pub cursor_state: Option<CursorState>,
}

/// The system that sets Interaction for all UI elements based on the mouse cursor activity
///
/// Entities with a hidden [`ComputedVisibility`] are always treated as released.
#[allow(clippy::too_many_arguments)]
pub fn ui_focus_system(
mut state: Local<State>,
camera: Query<(&Camera, Option<&UiCameraConfig>)>,
windows: Query<&Window>,
camera_query: Query<(Entity, &Camera, Option<&UiCameraConfig>)>,
windows: Query<(Entity, &Window)>,
mouse_button_input: Res<Input<MouseButton>>,
touches_input: Res<Touches>,
ui_stack: Res<UiStack>,
ui_stacks: Res<UiStacks>,
mut node_query: Query<NodeQuery>,
primary_window: Query<Entity, With<PrimaryWindow>>,
primary_window_query: Query<Entity, With<PrimaryWindow>>,
cursor_override: Option<ResMut<UiCursorOverride>>,
) {
let primary_window = primary_window.iter().next();

// reset entities that were both clicked and released in the last frame
for entity in state.entities_to_reset.drain(..) {
if let Ok(mut interaction) = node_query.get_component_mut::<Interaction>(entity) {
*interaction = Interaction::None;
}
}
let cursor_state = {
cursor_override
.and_then(|mut cursor_override| cursor_override.cursor_state.take())
.or_else(|| {
let primary_window = primary_window_query.get_single().ok();
let window_cursor = {
let mut cursor_state = None;
for (window_entity, window) in windows.iter() {
if let Some(position) = window.cursor_position() {
cursor_state = Some((window_entity, position));
break;
}
}
cursor_state
}
.or_else(|| {
touches_input.first_pressed_position().and_then(|position| {
primary_window.map(|primary_window| (primary_window, position))
})
});
window_cursor.map(|(window, position)| CursorState {
views: camera_query
.iter()
.filter(|(_, _, config)| {
matches!(config, Some(&UiCameraConfig { show_ui: true, .. }) | None)
})
.filter_map(|(camera_entity, camera, _)| match camera.target {
bevy_render::camera::RenderTarget::Window(window_ref) => {
if window_ref.normalize(primary_window).map_or(
false,
|normalized_window_ref| {
normalized_window_ref.entity() == window
},
) {
Some(camera_entity)
} else {
None
}
}
_ => None,
})
.collect(),
position,
})
})
};

let mouse_released =
mouse_button_input.just_released(MouseButton::Left) || touches_input.any_just_released();
Expand All @@ -171,100 +224,73 @@ pub fn ui_focus_system(
let mouse_clicked =
mouse_button_input.just_pressed(MouseButton::Left) || touches_input.any_just_pressed();

let is_ui_disabled =
|camera_ui| matches!(camera_ui, Some(&UiCameraConfig { show_ui: false, .. }));

let cursor_position = camera
.iter()
.filter(|(_, camera_ui)| !is_ui_disabled(*camera_ui))
.filter_map(|(camera, _)| {
if let Some(NormalizedRenderTarget::Window(window_ref)) =
camera.target.normalize(primary_window)
{
Some(window_ref)
} else {
None
}
})
.find_map(|window_ref| {
windows
.get(window_ref.entity())
.ok()
.and_then(|window| window.cursor_position())
})
.or_else(|| touches_input.first_pressed_position());

// prepare an iterator that contains all the nodes that have the cursor in their rect,
// from the top node to the bottom one. this will also reset the interaction to `None`
// for all nodes encountered that are no longer hovered.
let mut hovered_nodes = ui_stack
.uinodes
.iter()
// reverse the iterator to traverse the tree from closest nodes to furthest
.rev()
.filter_map(|entity| {
if let Ok(node) = node_query.get_mut(*entity) {
// Nodes that are not rendered should not be interactable
if let Some(computed_visibility) = node.computed_visibility {
if !computed_visibility.is_visible() {
// Reset their interaction to None to avoid strange stuck state
if let Some(mut interaction) = node.interaction {
// We cannot simply set the interaction to None, as that will trigger change detection repeatedly
interaction.set_if_neq(Interaction::None);
let mut hovered_nodes = vec![];
for ui_stack in &ui_stacks.stacks {
if cursor_state
.as_ref()
.map_or(false, |state| state.views.contains(&ui_stack.view))
{
// reverse the iterator to traverse the tree from closest nodes to furthest
for &entity in ui_stack.uinodes.iter().rev() {
if let Ok(node) = node_query.get_mut(entity) {
// Nodes that are not rendered should not be interactable
if let Some(computed_visibility) = node.computed_visibility {
if !computed_visibility.is_visible() {
// Reset their interaction to None to avoid strange stuck state
if let Some(mut interaction) = node.interaction {
// We cannot simply set the interaction to None, as that will trigger change detection repeatedly
interaction.set_if_neq(Interaction::None);
}
continue;
}

return None;
}
}

let position = node.global_transform.translation();
let ui_position = position.truncate();
let extents = node.node.size() / 2.0;
let mut min = ui_position - extents;
if let Some(clip) = node.calculated_clip {
min = Vec2::max(min, clip.clip.min);
}
let position = node.global_transform.translation();
let ui_position = position.truncate();
let extents = node.node.size() / 2.0;
let mut min = ui_position - extents;
if let Some(clip) = node.calculated_clip {
min = Vec2::max(min, clip.clip.min);
}

// The mouse position relative to the node
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
let relative_cursor_position = cursor_position.map(|cursor_position| {
Vec2::new(
(cursor_position.x - min.x) / node.node.size().x,
(cursor_position.y - min.y) / node.node.size().y,
)
});
// The mouse position relative to the node
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
let relative_cursor_position = cursor_state
.as_ref()
.map(|cursor| (cursor.position - min) / node.node.size());

// If the current cursor position is within the bounds of the node, consider it for
// clicking
let relative_cursor_position_component = RelativeCursorPosition {
normalized: relative_cursor_position,
};
// If the current cursor position is within the bounds of the node, consider it for
// clicking
let relative_cursor_position_component = RelativeCursorPosition {
normalized: relative_cursor_position,
};

let contains_cursor = relative_cursor_position_component.mouse_over();
let contains_cursor = relative_cursor_position_component.mouse_over();

// Save the relative cursor position to the correct component
if let Some(mut node_relative_cursor_position_component) =
node.relative_cursor_position
{
*node_relative_cursor_position_component = relative_cursor_position_component;
}
// Save the relative cursor position to the correct component
if let Some(mut node_relative_cursor_position_component) =
node.relative_cursor_position
{
*node_relative_cursor_position_component =
relative_cursor_position_component;
}

if contains_cursor {
Some(*entity)
} else {
if let Some(mut interaction) = node.interaction {
if *interaction == Interaction::Hovered || (cursor_position.is_none()) {
if contains_cursor {
hovered_nodes.push(entity);
} else if let Some(mut interaction) = node.interaction {
if *interaction == Interaction::Hovered || (cursor_state.is_none()) {
interaction.set_if_neq(Interaction::None);
}
}
None
}
} else {
None
}
})
.collect::<Vec<Entity>>()
.into_iter();
}
}

let mut hovered_nodes = hovered_nodes.into_iter();

// set Clicked or Hovered on top nodes. as soon as a node with a `Block` focus policy is detected,
// the iteration will stop on it because it "captures" the interaction.
Expand Down
23 changes: 10 additions & 13 deletions crates/bevy_ui/src/layout/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ impl Val {
Val::Auto => taffy::style::LengthPercentageAuto::Auto,
Val::Percent(value) => taffy::style::LengthPercentageAuto::Percent(value / 100.),
Val::Px(value) => taffy::style::LengthPercentageAuto::Points(
(context.scale_factor * value as f64) as f32,
(context.combined_scale_factor * value as f64) as f32,
),
Val::VMin(value) => taffy::style::LengthPercentageAuto::Points(
context.physical_size.x.min(context.physical_size.y) * value / 100.,
),
Val::VMax(value) => taffy::style::LengthPercentageAuto::Points(
context.physical_size.x.max(context.physical_size.y) * value / 100.,
),
Val::VMin(value) => {
taffy::style::LengthPercentageAuto::Points(context.min_size * value / 100.)
}
Val::VMax(value) => {
taffy::style::LengthPercentageAuto::Points(context.max_size * value / 100.)
}
Val::Vw(value) => {
taffy::style::LengthPercentageAuto::Points(context.physical_size.x * value / 100.)
}
Expand Down Expand Up @@ -343,10 +343,7 @@ impl MaxTrackSizingFunction {
}

impl GridTrack {
fn into_taffy_track(
self,
context: &LayoutContext,
) -> taffy::style::NonRepeatedTrackSizingFunction {
fn into_taffy_track(self, context: &LayoutContext) -> taffy::style::NonRepeatedTrackSizingFunction {
let min = self.min_sizing_function.into_taffy(context);
let max = self.max_sizing_function.into_taffy(context);
taffy::style_helpers::minmax(min, max)
Expand Down Expand Up @@ -466,7 +463,7 @@ mod tests {
grid_column: GridPlacement::start(4),
grid_row: GridPlacement::span(3),
};
let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.));
let viewport_values = LayoutContext::new(bevy_math::Vec2::new(800., 600.), 1.0, 1.0);
let taffy_style = from_style(&viewport_values, &bevy_style);
assert_eq!(taffy_style.display, taffy::style::Display::Flex);
assert_eq!(taffy_style.position, taffy::style::Position::Absolute);
Expand Down Expand Up @@ -633,7 +630,7 @@ mod tests {
#[test]
fn test_into_length_percentage() {
use taffy::style::LengthPercentage;
let context = LayoutContext::new(2.0, bevy_math::Vec2::new(800., 600.));
let context = LayoutContext::new(bevy_math::Vec2::new(800., 600.), 2.0, 1.0);
let cases = [
(Val::Auto, LengthPercentage::Points(0.)),
(Val::Percent(1.), LengthPercentage::Percent(0.01)),
Expand Down
7 changes: 4 additions & 3 deletions crates/bevy_ui/src/layout/debug.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::UiLayouts;
use crate::UiSurface;
use bevy_ecs::prelude::Entity;
use bevy_utils::HashMap;
Expand All @@ -6,19 +7,19 @@ use taffy::prelude::Node;
use taffy::tree::LayoutTree;

/// Prints a debug representation of the computed layout of the UI layout tree for each window.
pub fn print_ui_layout_tree(ui_surface: &UiSurface) {
pub fn print_ui_layout_tree(ui_surface: &UiSurface, layouts: &UiLayouts) {
let taffy_to_entity: HashMap<Node, Entity> = ui_surface
.entity_to_taffy
.iter()
.map(|(entity, node)| (*node, *entity))
.collect();
for (&entity, &node) in ui_surface.window_nodes.iter() {
for (&entity, layout) in layouts.iter() {
let mut out = String::new();
print_node(
ui_surface,
&taffy_to_entity,
entity,
node,
layout.taffy_root,
false,
String::new(),
&mut out,
Expand Down
Loading