Skip to content

Commit 3d718ee

Browse files
committed
Add user control of UI cameras
Add fields to UiCameraConfig to control the UI camera scale and position. Fixes #5242. It is again possible to manipulate the ui camera. An alternative design was considered, where instead of having a component that controls all of the UI camera settings, the component would only hold an Entity referencing another camera. That design was abandoned in favor of the current one because the viewport is tightly bound to the "actual" camera the UI camera is attached to. So it would be awkward to maintain independently two different cameras.
1 parent 47f1944 commit 3d718ee

File tree

5 files changed

+109
-84
lines changed

5 files changed

+109
-84
lines changed

crates/bevy_ui/src/entity.rs

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
33
use crate::{
44
widget::{Button, ImageMode},
5-
CalculatedSize, FocusPolicy, Interaction, Node, Style, UiColor, UiImage,
5+
CalculatedSize, FocusPolicy, Interaction, Node, Style, UiColor, UiImage, UI_CAMERA_FAR,
66
};
7-
use bevy_ecs::{
8-
bundle::Bundle,
9-
prelude::{Component, With},
10-
query::QueryItem,
7+
use bevy_ecs::{bundle::Bundle, prelude::Component};
8+
use bevy_math::Vec2;
9+
use bevy_render::{
10+
camera::{DepthCalculation, OrthographicProjection, WindowOrigin},
11+
view::Visibility,
1112
};
12-
use bevy_render::{camera::Camera, extract_component::ExtractComponent, view::Visibility};
1313
use bevy_text::Text;
1414
use bevy_transform::prelude::{GlobalTransform, Transform};
1515

@@ -96,7 +96,7 @@ impl Default for TextBundle {
9696
}
9797

9898
/// A UI node that is a button
99-
#[derive(Bundle, Clone, Debug)]
99+
#[derive(Bundle, Clone, Debug, Default)]
100100
pub struct ButtonBundle {
101101
/// Describes the size of the node
102102
pub node: Node,
@@ -120,22 +120,6 @@ pub struct ButtonBundle {
120120
pub visibility: Visibility,
121121
}
122122

123-
impl Default for ButtonBundle {
124-
fn default() -> Self {
125-
ButtonBundle {
126-
button: Button,
127-
interaction: Default::default(),
128-
focus_policy: Default::default(),
129-
node: Default::default(),
130-
style: Default::default(),
131-
color: Default::default(),
132-
image: Default::default(),
133-
transform: Default::default(),
134-
global_transform: Default::default(),
135-
visibility: Default::default(),
136-
}
137-
}
138-
}
139123
/// Configuration for cameras related to UI.
140124
///
141125
/// When a [`Camera`] doesn't have the [`UiCameraConfig`] component,
@@ -149,19 +133,41 @@ pub struct UiCameraConfig {
149133
/// When a `Camera` doesn't have the [`UiCameraConfig`] component,
150134
/// it will display the UI by default.
151135
pub show_ui: bool,
136+
/// The position of the UI camera in UI space.
137+
pub position: Vec2,
138+
/// The projection data for the UI camera.
139+
///
140+
/// The code relies on this not being set,
141+
/// please use [`UiCameraConfig::scale_mut`] and [`UiCameraConfig::projection`]
142+
/// instead.
143+
/// This is only public so it is possible to use the struct update syntax.
144+
#[doc(hidden)]
145+
pub projection: OrthographicProjection,
152146
}
153147

154-
impl Default for UiCameraConfig {
155-
fn default() -> Self {
156-
Self { show_ui: true }
148+
impl UiCameraConfig {
149+
/// Get mutably the scale of the UI camera, useful for zoom effects.
150+
pub fn scale_mut(&mut self) -> &mut f32 {
151+
&mut self.projection.scale
157152
}
158-
}
159153

160-
impl ExtractComponent for UiCameraConfig {
161-
type Query = &'static Self;
162-
type Filter = With<Camera>;
154+
/// The projection data for the UI camera.
155+
pub fn projection(&self) -> &OrthographicProjection {
156+
&self.projection
157+
}
158+
}
163159

164-
fn extract_component(item: QueryItem<Self::Query>) -> Self {
165-
item.clone()
160+
impl Default for UiCameraConfig {
161+
fn default() -> Self {
162+
Self {
163+
show_ui: true,
164+
position: Vec2::ZERO,
165+
projection: OrthographicProjection {
166+
far: UI_CAMERA_FAR,
167+
window_origin: WindowOrigin::BottomLeft,
168+
depth_calculation: DepthCalculation::ZDifference,
169+
..Default::default()
170+
},
171+
}
166172
}
167173
}

crates/bevy_ui/src/lib.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ pub mod entity;
1212
pub mod update;
1313
pub mod widget;
1414

15-
use bevy_render::extract_component::ExtractComponentPlugin;
1615
pub use flex::*;
1716
pub use focus::*;
1817
pub use geometry::*;
@@ -33,8 +32,6 @@ use bevy_transform::TransformSystem;
3332
use bevy_window::ModifiesWindows;
3433
use update::{ui_z_system, update_clipping_system};
3534

36-
use crate::prelude::UiCameraConfig;
37-
3835
/// The basic plugin for Bevy UI
3936
#[derive(Default)]
4037
pub struct UiPlugin;
@@ -50,8 +47,7 @@ pub enum UiSystem {
5047

5148
impl Plugin for UiPlugin {
5249
fn build(&self, app: &mut App) {
53-
app.add_plugin(ExtractComponentPlugin::<UiCameraConfig>::default())
54-
.init_resource::<FlexSurface>()
50+
app.init_resource::<FlexSurface>()
5551
.register_type::<AlignContent>()
5652
.register_type::<AlignItems>()
5753
.register_type::<AlignSelf>()

crates/bevy_ui/src/render/mod.rs

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use bevy_ecs::prelude::*;
1212
use bevy_math::{Mat4, Vec2, Vec3, Vec4Swizzles};
1313
use bevy_reflect::TypeUuid;
1414
use bevy_render::{
15-
camera::{Camera, CameraProjection, DepthCalculation, OrthographicProjection, WindowOrigin},
15+
camera::{Camera, CameraProjection},
1616
color::Color,
1717
render_asset::RenderAssets,
1818
render_graph::{RenderGraph, RunGraphOnViewNode, SlotInfo, SlotType},
@@ -215,52 +215,52 @@ pub fn extract_uinodes(
215215
/// as ui elements are "stacked on top of each other", they are within the camera's view
216216
/// and have room to grow.
217217
// TODO: Consider computing this value at runtime based on the maximum z-value.
218-
const UI_CAMERA_FAR: f32 = 1000.0;
218+
pub(crate) const UI_CAMERA_FAR: f32 = 1000.0;
219219

220220
// This value is subtracted from the far distance for the camera's z-position to ensure nodes at z == 0.0 are rendered
221221
// TODO: Evaluate if we still need this.
222222
const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1;
223223

224-
#[derive(Component)]
225-
pub struct DefaultCameraView(pub Entity);
224+
#[derive(Component, Debug)]
225+
pub struct UiCamera {
226+
pub entity: Entity,
227+
}
226228

227229
pub fn extract_default_ui_camera_view<T: Component>(
228230
mut commands: Commands,
229231
render_world: Res<RenderWorld>,
230-
query: Query<(Entity, &Camera, Option<&UiCameraConfig>), With<T>>,
232+
mut query: Query<(Entity, &Camera, Option<&mut UiCameraConfig>), With<T>>,
231233
) {
232-
for (entity, camera, camera_ui) in query.iter() {
234+
for (camera_entity, camera, opt_ui_config) in query.iter_mut() {
235+
let ui_config = opt_ui_config.as_deref().cloned().unwrap_or_default();
233236
// ignore cameras with disabled ui
234-
if matches!(camera_ui, Some(&UiCameraConfig { show_ui: false, .. })) {
237+
if !ui_config.show_ui {
235238
continue;
236239
}
237240
if let (Some(logical_size), Some(physical_size)) = (
238241
camera.logical_viewport_size(),
239242
camera.physical_viewport_size(),
240243
) {
241-
let mut projection = OrthographicProjection {
242-
far: UI_CAMERA_FAR,
243-
window_origin: WindowOrigin::BottomLeft,
244-
depth_calculation: DepthCalculation::ZDifference,
245-
..Default::default()
246-
};
244+
let mut projection = ui_config.projection.clone();
247245
projection.update(logical_size.x, logical_size.y);
246+
let projection_matrix = projection.get_projection_matrix();
247+
if let Some(mut config_to_update) = opt_ui_config {
248+
config_to_update.projection = projection;
249+
}
248250
// This roundabout approach is required because spawn().id() won't work in this context
249-
let default_camera_view = render_world.entities().reserve_entity();
250-
commands
251-
.get_or_spawn(default_camera_view)
252-
.insert(ExtractedView {
253-
projection: projection.get_projection_matrix(),
254-
transform: GlobalTransform::from_xyz(
255-
0.0,
256-
0.0,
257-
UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET,
258-
),
259-
width: physical_size.x,
260-
height: physical_size.y,
261-
});
262-
commands.get_or_spawn(entity).insert_bundle((
263-
DefaultCameraView(default_camera_view),
251+
let ui_camera = render_world.entities().reserve_entity();
252+
commands.get_or_spawn(ui_camera).insert(ExtractedView {
253+
projection: projection_matrix,
254+
transform: GlobalTransform::from_xyz(
255+
ui_config.position.x,
256+
ui_config.position.y,
257+
UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET,
258+
),
259+
width: physical_size.x,
260+
height: physical_size.y,
261+
});
262+
commands.get_or_spawn(camera_entity).insert_bundle((
263+
UiCamera { entity: ui_camera },
264264
RenderPhase::<TransparentUi>::default(),
265265
));
266266
}

crates/bevy_ui/src/render/render_pass.rs

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
use crate::UiCamera;
2+
13
use super::{UiBatch, UiImageBindGroups, UiMeta};
2-
use crate::{prelude::UiCameraConfig, DefaultCameraView};
34
use bevy_ecs::{
45
prelude::*,
56
system::{lifetimeless::*, SystemParamItem},
@@ -16,15 +17,9 @@ use bevy_render::{
1617
use bevy_utils::FloatOrd;
1718

1819
pub struct UiPassNode {
19-
ui_view_query: QueryState<
20-
(
21-
&'static RenderPhase<TransparentUi>,
22-
&'static ViewTarget,
23-
Option<&'static UiCameraConfig>,
24-
),
25-
With<ExtractedView>,
26-
>,
27-
default_camera_view_query: QueryState<&'static DefaultCameraView>,
20+
ui_view_query:
21+
QueryState<(&'static RenderPhase<TransparentUi>, &'static ViewTarget), With<ExtractedView>>,
22+
default_camera_view_query: QueryState<&'static UiCamera>,
2823
}
2924

3025
impl UiPassNode {
@@ -56,29 +51,25 @@ impl Node for UiPassNode {
5651
) -> Result<(), NodeRunError> {
5752
let input_view_entity = graph.get_input_entity(Self::IN_VIEW)?;
5853

59-
let (transparent_phase, target, camera_ui) =
54+
let (transparent_phase, target) =
6055
if let Ok(result) = self.ui_view_query.get_manual(world, input_view_entity) {
6156
result
6257
} else {
6358
return Ok(());
6459
};
60+
6561
if transparent_phase.items.is_empty() {
6662
return Ok(());
6763
}
68-
// Don't render UI for cameras where it is explicitly disabled
69-
if matches!(camera_ui, Some(&UiCameraConfig { show_ui: false })) {
70-
return Ok(());
71-
}
72-
73-
// use the "default" view entity if it is defined
7464
let view_entity = if let Ok(default_view) = self
7565
.default_camera_view_query
7666
.get_manual(world, input_view_entity)
7767
{
78-
default_view.0
68+
default_view.entity
7969
} else {
8070
input_view_entity
8171
};
72+
8273
let pass_descriptor = RenderPassDescriptor {
8374
label: Some("ui_pass"),
8475
color_attachments: &[RenderPassColorAttachment {

examples/ui/ui.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,19 @@ fn main() {
1313
.insert_resource(WinitSettings::desktop_app())
1414
.add_startup_system(setup)
1515
.add_system(mouse_scroll)
16+
.add_system(change_ui_camera)
1617
.run();
1718
}
1819

1920
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
2021
// Camera
21-
commands.spawn_bundle(Camera2dBundle::default());
22+
commands
23+
.spawn_bundle(Camera2dBundle::default())
24+
// By default, the UI is already displayed, but here we want to be able to toggle it later
25+
.insert(UiCameraConfig {
26+
show_ui: true,
27+
..default()
28+
});
2229

2330
// root node
2431
commands
@@ -309,6 +316,31 @@ struct ScrollingList {
309316
position: f32,
310317
}
311318

319+
fn change_ui_camera(
320+
mouse: Res<Input<MouseButton>>,
321+
keyboard: Res<Input<KeyCode>>,
322+
mut ui_config: Query<&mut UiCameraConfig>,
323+
) {
324+
for mut config in ui_config.iter_mut() {
325+
if mouse.just_pressed(MouseButton::Left) {
326+
config.show_ui = !config.show_ui;
327+
}
328+
if keyboard.pressed(KeyCode::Left) {
329+
config.position.x -= 1.0;
330+
}
331+
if keyboard.pressed(KeyCode::Right) {
332+
config.position.x += 1.0;
333+
}
334+
if keyboard.pressed(KeyCode::Up) {
335+
let scale = config.scale_mut();
336+
*scale *= 0.99;
337+
}
338+
if keyboard.pressed(KeyCode::Down) {
339+
let scale = config.scale_mut();
340+
*scale *= 1.01;
341+
}
342+
}
343+
}
312344
fn mouse_scroll(
313345
mut mouse_wheel_events: EventReader<MouseWheel>,
314346
mut query_list: Query<(&mut ScrollingList, &mut Style, &Children, &Node)>,

0 commit comments

Comments
 (0)