bevy/crates
Antony bf42cb3532
Add a viewport UI widget (#17253)
# Objective

Add a viewport widget.

## Solution

- Add a new `ViewportNode` component to turn a UI node into a viewport.
- Add `viewport_picking` to pass pointer inputs from other pointers to
the viewport's pointer.
- Notably, this is somewhat functionally different from the viewport
widget in [the editor
prototype](https://github.com/bevyengine/bevy_editor_prototypes/pull/110/files#L124),
which just moves the pointer's location onto the render target. Viewport
widgets have their own pointers.
  - Care is taken to handle dragging in and out of viewports.
- Add `update_viewport_render_target_size` to update the viewport node's
render target's size if the node size changes.
- Feature gate picking-related viewport items behind
`bevy_ui_picking_backend`.

## Testing

I've been using an example I made to test the widget (and added it as
`viewport_node`):

<details><summary>Code</summary>

```rust
//! A simple scene to demonstrate spawning a viewport widget. The example will demonstrate how to
//! pick entities visible in the widget's view.

use bevy::picking::pointer::PointerInteraction;
use bevy::prelude::*;

use bevy::ui::widget::ViewportNode;
use bevy::{
    image::{TextureFormatPixelInfo, Volume},
    window::PrimaryWindow,
};
use bevy_render::{
    camera::RenderTarget,
    render_resource::{
        Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
    },
};

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, MeshPickingPlugin))
        .add_systems(Startup, test)
        .add_systems(Update, draw_mesh_intersections)
        .run();
}

#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
struct Shape;

fn test(
    mut commands: Commands,
    window: Query<&Window, With<PrimaryWindow>>,
    mut images: ResMut<Assets<Image>>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // Spawn a UI camera
    commands.spawn(Camera3d::default());

    // Set up an texture for the 3D camera to render to
    let window = window.get_single().unwrap();
    let window_size = window.physical_size();
    let size = Extent3d {
        width: window_size.x,
        height: window_size.y,
        ..default()
    };
    let format = TextureFormat::Bgra8UnormSrgb;
    let image = Image {
        data: Some(vec![0; size.volume() * format.pixel_size()]),
        texture_descriptor: TextureDescriptor {
            label: None,
            size,
            dimension: TextureDimension::D2,
            format,
            mip_level_count: 1,
            sample_count: 1,
            usage: TextureUsages::TEXTURE_BINDING
                | TextureUsages::COPY_DST
                | TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        },
        ..default()
    };
    let image_handle = images.add(image);

    // Spawn the 3D camera
    let camera = commands
        .spawn((
            Camera3d::default(),
            Camera {
                // Render this camera before our UI camera
                order: -1,
                target: RenderTarget::Image(image_handle.clone().into()),
                ..default()
            },
        ))
        .id();

    // Spawn something for the 3D camera to look at
    commands
        .spawn((
            Mesh3d(meshes.add(Cuboid::new(5.0, 5.0, 5.0))),
            MeshMaterial3d(materials.add(Color::WHITE)),
            Transform::from_xyz(0.0, 0.0, -10.0),
            Shape,
        ))
        // We can observe pointer events on our objects as normal, the
        // `bevy::ui::widgets::viewport_picking` system will take care of ensuring our viewport
        // clicks pass through
        .observe(on_drag_cuboid);

    // Spawn our viewport widget
    commands
        .spawn((
            Node {
                position_type: PositionType::Absolute,
                top: Val::Px(50.0),
                left: Val::Px(50.0),
                width: Val::Px(200.0),
                height: Val::Px(200.0),
                border: UiRect::all(Val::Px(5.0)),
                ..default()
            },
            BorderColor(Color::WHITE),
            ViewportNode::new(camera),
        ))
        .observe(on_drag_viewport);
}

fn on_drag_viewport(drag: Trigger<Pointer<Drag>>, mut node_query: Query<&mut Node>) {
    if matches!(drag.button, PointerButton::Secondary) {
        let mut node = node_query.get_mut(drag.target()).unwrap();

        if let (Val::Px(top), Val::Px(left)) = (node.top, node.left) {
            node.left = Val::Px(left + drag.delta.x);
            node.top = Val::Px(top + drag.delta.y);
        };
    }
}

fn on_drag_cuboid(drag: Trigger<Pointer<Drag>>, mut transform_query: Query<&mut Transform>) {
    if matches!(drag.button, PointerButton::Primary) {
        let mut transform = transform_query.get_mut(drag.target()).unwrap();
        transform.rotate_y(drag.delta.x * 0.02);
        transform.rotate_x(drag.delta.y * 0.02);
    }
}

fn draw_mesh_intersections(
    pointers: Query<&PointerInteraction>,
    untargetable: Query<Entity, Without<Shape>>,
    mut gizmos: Gizmos,
) {
    for (point, normal) in pointers
        .iter()
        .flat_map(|interaction| interaction.iter())
        .filter_map(|(entity, hit)| {
            if !untargetable.contains(*entity) {
                hit.position.zip(hit.normal)
            } else {
                None
            }
        })
    {
        gizmos.arrow(point, point + normal.normalize() * 0.5, Color::WHITE);
    }
}
```

</details>

## Showcase


https://github.com/user-attachments/assets/39f44eac-2c2a-4fd9-a606-04171f806dc1

## Open Questions

- <del>Not sure whether the entire widget should be feature gated behind
`bevy_ui_picking_backend` or not? I chose a partial approach since maybe
someone will want to use the widget without any picking being
involved.</del>
- <del>Is `PickSet::Last` the expected set for `viewport_picking`?
Perhaps `PickSet::Input` is more suited.</del>
- <del>Can `dragged_last_frame` be removed in favor of a better dragging
check? Another option that comes to mind is reading `Drag` and `DragEnd`
events, but this seems messier.</del>

---------

Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
Co-authored-by: François Mockers <mockersf@gmail.com>
2025-05-05 22:57:37 +00:00
..
bevy_a11y
bevy_animation Revert "Allow partial support for bevy_log in no_std (#18782)" (#18816) 2025-04-14 21:15:01 +00:00
bevy_anti_aliasing
bevy_app Rename bevy_platform_support to bevy_platform (#18813) 2025-04-11 23:13:28 +00:00
bevy_asset ignore files starting with . when loading folders (#11214) 2025-05-05 22:42:01 +00:00
bevy_audio Audio sink seek adopted (#18971) 2025-05-03 11:29:38 +00:00
bevy_color fix typo (#18696) 2025-04-03 17:18:09 +00:00
bevy_core_pipeline Rename bevy_platform_support to bevy_platform (#18813) 2025-04-11 23:13:28 +00:00
bevy_derive
bevy_dev_tools
bevy_diagnostic Update sysinfo version to 0.35.0 (#19028) 2025-05-05 05:49:23 +00:00
bevy_dylib don't disable std in bevy_dylib (#18807) 2025-04-11 18:44:53 +00:00
bevy_ecs Stop using ArchetypeComponentId in the executor (#16885) 2025-05-05 22:52:44 +00:00
bevy_encase_derive
bevy_gilrs Rename bevy_platform_support to bevy_platform (#18813) 2025-04-11 23:13:28 +00:00
bevy_gizmos
bevy_gltf Revert "Allow partial support for bevy_log in no_std (#18782)" (#18816) 2025-04-14 21:15:01 +00:00
bevy_image Update ktx2 to 0.4.0 (#19073) 2025-05-05 16:42:36 +00:00
bevy_input Rename bevy_platform_support to bevy_platform (#18813) 2025-04-11 23:13:28 +00:00
bevy_input_focus Switch ChildOf back to tuple struct (#18672) 2025-04-02 00:10:10 +00:00
bevy_internal Revert "Allow partial support for bevy_log in no_std (#18782)" (#18816) 2025-04-14 21:15:01 +00:00
bevy_log Revert "Allow partial support for bevy_log in no_std (#18782)" (#18816) 2025-04-14 21:15:01 +00:00
bevy_macro_utils Fully qualify crate paths in BevyManifest (#18938) 2025-04-28 21:43:48 +00:00
bevy_math Fix rotate_by implementation for Aabb2d (#19015) 2025-05-04 13:05:27 +00:00
bevy_mesh Rename bevy_platform_support to bevy_platform (#18813) 2025-04-11 23:13:28 +00:00
bevy_mikktspace fix new nightly lint on mikktspace (#18988) 2025-04-30 05:19:01 +00:00
bevy_pbr Swap order of eviction/extraction when extracting for specialization (#18846) 2025-04-15 06:44:01 +00:00
bevy_picking Removed conversion from pointer physical coordinates to viewport local coordinates in bevy_picking make_ray function (#18870) 2025-04-27 13:54:28 +00:00
bevy_platform Update spin requirement from 0.9.8 to 0.10.0 (#18655) 2025-04-27 06:18:24 +00:00
bevy_ptr
bevy_reflect Add NonNilUuid support to bevy_reflect (#18604) 2025-05-04 08:22:57 +00:00
bevy_remote Rename bevy_platform_support to bevy_platform (#18813) 2025-04-11 23:13:28 +00:00
bevy_render Update ktx2 to 0.4.0 (#19073) 2025-05-05 16:42:36 +00:00
bevy_scene Rename bevy_platform_support to bevy_platform (#18813) 2025-04-11 23:13:28 +00:00
bevy_sprite Sprite picking docs fix (#19016) 2025-05-05 17:45:14 +00:00
bevy_state Rename bevy_platform_support to bevy_platform (#18813) 2025-04-11 23:13:28 +00:00
bevy_tasks Rename bevy_platform_support to bevy_platform (#18813) 2025-04-11 23:13:28 +00:00
bevy_text Text background colors (#18892) 2025-05-04 08:18:46 +00:00
bevy_time Rename bevy_platform_support to bevy_platform (#18813) 2025-04-11 23:13:28 +00:00
bevy_transform Revert "Allow partial support for bevy_log in no_std (#18782)" (#18816) 2025-04-14 21:15:01 +00:00
bevy_ui Add a viewport UI widget (#17253) 2025-05-05 22:57:37 +00:00
bevy_utils Rename bevy_platform_support to bevy_platform (#18813) 2025-04-11 23:13:28 +00:00
bevy_window Expose deferred screen edges setting for ios devices (#18729) 2025-04-30 21:24:53 +00:00
bevy_winit Expose deferred screen edges setting for ios devices (#18729) 2025-04-30 21:24:53 +00:00