bevy_ui/widget/
viewport.rs

1use bevy_asset::Assets;
2use bevy_camera::Camera;
3#[cfg(feature = "bevy_ui_picking_backend")]
4use bevy_camera::NormalizedRenderTarget;
5use bevy_ecs::{
6    component::Component,
7    entity::Entity,
8    query::{Changed, Or},
9    reflect::ReflectComponent,
10    system::{Query, ResMut},
11};
12#[cfg(feature = "bevy_ui_picking_backend")]
13use bevy_ecs::{
14    message::MessageReader,
15    system::{Commands, Res},
16};
17use bevy_image::{Image, ToExtents};
18#[cfg(feature = "bevy_ui_picking_backend")]
19use bevy_math::Rect;
20use bevy_math::UVec2;
21#[cfg(feature = "bevy_ui_picking_backend")]
22use bevy_picking::{
23    events::PointerState,
24    hover::HoverMap,
25    pointer::{Location, PointerId, PointerInput, PointerLocation},
26};
27#[cfg(feature = "bevy_ui_picking_backend")]
28use bevy_platform::collections::HashMap;
29use bevy_reflect::Reflect;
30#[cfg(feature = "bevy_ui_picking_backend")]
31use bevy_transform::components::GlobalTransform;
32#[cfg(feature = "bevy_ui_picking_backend")]
33use uuid::Uuid;
34
35use crate::{ComputedNode, Node};
36
37/// Component used to render a [`Camera::target`]  to a node.
38///
39/// # See Also
40///
41/// [`update_viewport_render_target_size`]
42#[derive(Component, Debug, Clone, Copy, Reflect)]
43#[reflect(Component, Debug)]
44#[require(Node)]
45#[cfg_attr(
46    feature = "bevy_ui_picking_backend",
47    require(PointerId::Custom(Uuid::new_v4()))
48)]
49pub struct ViewportNode {
50    /// The entity representing the [`Camera`] associated with this viewport.
51    ///
52    /// Note that removing the [`ViewportNode`] component will not despawn this entity.
53    pub camera: Entity,
54}
55
56impl ViewportNode {
57    /// Creates a new [`ViewportNode`] with a given `camera`.
58    #[inline]
59    pub const fn new(camera: Entity) -> Self {
60        Self { camera }
61    }
62}
63
64#[cfg(feature = "bevy_ui_picking_backend")]
65/// Handles viewport picking logic.
66///
67/// Viewport entities that are being hovered or dragged will have all pointer inputs sent to them.
68pub fn viewport_picking(
69    mut commands: Commands,
70    mut viewport_query: Query<(
71        Entity,
72        &ViewportNode,
73        &PointerId,
74        &mut PointerLocation,
75        &ComputedNode,
76        &GlobalTransform,
77    )>,
78    camera_query: Query<&Camera>,
79    hover_map: Res<HoverMap>,
80    pointer_state: Res<PointerState>,
81    mut pointer_inputs: MessageReader<PointerInput>,
82) {
83    // Handle hovered entities.
84    let mut viewport_picks: HashMap<Entity, PointerId> = hover_map
85        .iter()
86        .flat_map(|(hover_pointer_id, hits)| {
87            hits.iter()
88                .filter(|(entity, _)| viewport_query.contains(**entity))
89                .map(|(entity, _)| (*entity, *hover_pointer_id))
90        })
91        .collect();
92
93    // Handle dragged entities, which need to be considered for dragging in and out of viewports.
94    for ((pointer_id, _), pointer_state) in pointer_state.pointer_buttons.iter() {
95        for &target in pointer_state
96            .dragging
97            .keys()
98            .filter(|&entity| viewport_query.contains(*entity))
99        {
100            viewport_picks.insert(target, *pointer_id);
101        }
102    }
103
104    for (
105        viewport_entity,
106        &viewport,
107        &viewport_pointer_id,
108        mut viewport_pointer_location,
109        computed_node,
110        global_transform,
111    ) in &mut viewport_query
112    {
113        let Some(pick_pointer_id) = viewport_picks.get(&viewport_entity) else {
114            // Lift the viewport pointer if it's not being used.
115            viewport_pointer_location.location = None;
116            continue;
117        };
118        let Ok(camera) = camera_query.get(viewport.camera) else {
119            continue;
120        };
121        let Some(cam_viewport_size) = camera.logical_viewport_size() else {
122            continue;
123        };
124
125        // Create a `Rect` in *physical* coordinates centered at the node's GlobalTransform
126        let node_rect = Rect::from_center_size(
127            global_transform.translation().truncate(),
128            computed_node.size(),
129        );
130        // Location::position uses *logical* coordinates
131        let top_left = node_rect.min * computed_node.inverse_scale_factor();
132        let logical_size = computed_node.size() * computed_node.inverse_scale_factor();
133
134        let Some(target) = camera.target.as_image() else {
135            continue;
136        };
137
138        for input in pointer_inputs
139            .read()
140            .filter(|input| &input.pointer_id == pick_pointer_id)
141        {
142            let local_position = (input.location.position - top_left) / logical_size;
143            let position = local_position * cam_viewport_size;
144
145            let location = Location {
146                position,
147                target: NormalizedRenderTarget::Image(target.clone().into()),
148            };
149            viewport_pointer_location.location = Some(location.clone());
150
151            commands.write_message(PointerInput {
152                location,
153                pointer_id: viewport_pointer_id,
154                action: input.action,
155            });
156        }
157    }
158}
159
160/// Updates the size of the associated render target for viewports when the node size changes.
161pub fn update_viewport_render_target_size(
162    viewport_query: Query<
163        (&ViewportNode, &ComputedNode),
164        Or<(Changed<ComputedNode>, Changed<ViewportNode>)>,
165    >,
166    camera_query: Query<&Camera>,
167    mut images: ResMut<Assets<Image>>,
168) {
169    for (viewport, computed_node) in &viewport_query {
170        let camera = camera_query.get(viewport.camera).unwrap();
171        let size = computed_node.size();
172
173        let Some(image_handle) = camera.target.as_image() else {
174            continue;
175        };
176        let size = size.as_uvec2().max(UVec2::ONE).to_extents();
177        images.get_mut(image_handle).unwrap().resize(size);
178    }
179}