From 86086477d0d63046a47e3c698989b1da39b78563 Mon Sep 17 00:00:00 2001 From: Jerome Caucat <31624981+jerome-caucat@users.noreply.github.com> Date: Tue, 22 Jul 2025 14:33:52 +0200 Subject: [PATCH] Add an example rendering dioxus-native to a texture in a bevy app (#4427) Co-authored-by: Jerome Caucat --- Cargo.lock | 20 + Cargo.toml | 1 + examples/native-headless-in-bevy/Cargo.toml | 29 + examples/native-headless-in-bevy/README.md | 11 + .../src/bevy_scene_plugin.rs | 183 +++ .../src/dioxus_in_bevy_plugin.rs | 1031 +++++++++++++++++ examples/native-headless-in-bevy/src/main.rs | 29 + examples/native-headless-in-bevy/src/ui.css | 93 ++ examples/native-headless-in-bevy/src/ui.rs | 172 +++ 9 files changed, 1569 insertions(+) create mode 100644 examples/native-headless-in-bevy/Cargo.toml create mode 100644 examples/native-headless-in-bevy/README.md create mode 100644 examples/native-headless-in-bevy/src/bevy_scene_plugin.rs create mode 100644 examples/native-headless-in-bevy/src/dioxus_in_bevy_plugin.rs create mode 100644 examples/native-headless-in-bevy/src/main.rs create mode 100644 examples/native-headless-in-bevy/src/ui.css create mode 100644 examples/native-headless-in-bevy/src/ui.rs diff --git a/Cargo.lock b/Cargo.lock index 7244aa647..5fa77fedb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10644,6 +10644,26 @@ dependencies = [ "wgpu", ] +[[package]] +name = "native-headless-in-bevy" +version = "0.0.0" +dependencies = [ + "anyrender_vello", + "async-std", + "bevy", + "blitz-dom", + "blitz-paint", + "blitz-traits", + "crossbeam-channel", + "dioxus", + "dioxus-native-dom", + "paste", + "rustc-hash 1.1.0", + "tracing-subscriber", + "vello", + "wgpu", +] + [[package]] name = "native-tls" version = "0.2.14" diff --git a/Cargo.toml b/Cargo.toml index d2ee10132..3a8477af5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,6 +113,7 @@ members = [ "examples/fullstack-websockets", "examples/wgpu-texture", "examples/native-headless", + "examples/native-headless-in-bevy", "examples/bevy", # Playwright tests diff --git a/examples/native-headless-in-bevy/Cargo.toml b/examples/native-headless-in-bevy/Cargo.toml new file mode 100644 index 000000000..117ca42fd --- /dev/null +++ b/examples/native-headless-in-bevy/Cargo.toml @@ -0,0 +1,29 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: MIT + +[package] +name = "native-headless-in-bevy" +version = "0.0.0" +edition = "2021" +license = "MIT" +publish = false + +[features] +tracing = ["dep:tracing-subscriber", "dioxus-native-dom/tracing"] + +[dependencies] +anyrender_vello = "0.4.1" +async-std = "1.13" +bevy = { version = "0.16" } +blitz-dom = { workspace = true, default-features = true } +blitz-paint = { workspace = true, default-features = true } +blitz-traits = { workspace = true, default-features = true } +crossbeam-channel = "0.5" +dioxus = { workspace = true } +dioxus-native-dom = { path = "../../packages/native-dom" } +paste = "1.0" +rustc-hash = "1" +tracing-subscriber = { workspace = true, optional = true } +vello = "0.5" +wgpu = { workspace = true } + diff --git a/examples/native-headless-in-bevy/README.md b/examples/native-headless-in-bevy/README.md new file mode 100644 index 000000000..35e75a8b6 --- /dev/null +++ b/examples/native-headless-in-bevy/README.md @@ -0,0 +1,11 @@ +Example: Integrating Dioxus into a Bevy application + +This example demonstrates rendering a Dioxus application onto a texture within a Bevy application. + +## Core Concepts +- Render a headless Dioxus-native app to a texture. +- Share and display the texture on a quad in the Bevy app. +- Transmit mouse and keyboard events from Bevy to Dioxus. + - Events are captured when hovering over elements with the `catch-events` class, ensuring only Dioxus receives them. +- Manage application state through channel messages between Dioxus and Bevy. + diff --git a/examples/native-headless-in-bevy/src/bevy_scene_plugin.rs b/examples/native-headless-in-bevy/src/bevy_scene_plugin.rs new file mode 100644 index 000000000..733b658df --- /dev/null +++ b/examples/native-headless-in-bevy/src/bevy_scene_plugin.rs @@ -0,0 +1,183 @@ +use crate::ui::{UIMessage, UiState}; +use bevy::input::mouse::{MouseButton, MouseMotion}; +use bevy::prelude::*; +use crossbeam_channel::{Receiver, Sender}; + +#[derive(Component)] +pub struct DynamicCube; + +#[derive(Component)] +pub struct OrbitCamera { + pub distance: f32, + pub yaw: f32, + pub pitch: f32, + pub sensitivity: f32, +} + +impl Default for OrbitCamera { + fn default() -> Self { + Self { + distance: 3.0, + yaw: 0.0, + pitch: 0.0, + sensitivity: 0.01, + } + } +} + +#[derive(Resource, Deref)] +struct UIMessageSender(Sender); + +#[derive(Resource, Deref)] +struct UIMessageReceiver(Receiver); + +#[derive(Resource)] +struct CubeTranslationSpeed(f32); + +impl Default for CubeTranslationSpeed { + fn default() -> Self { + Self(UiState::DEFAULT_CUBE_TRANSLATION_SPEED) + } +} + +#[derive(Resource)] +struct CubeRotationSpeed(f32); + +impl Default for CubeRotationSpeed { + fn default() -> Self { + Self(UiState::DEFAULT_CUBE_ROTATION_SPEED) + } +} + +pub struct BevyScenePlugin { + pub app_sender: Sender, + pub ui_receiver: Receiver, +} + +impl Plugin for BevyScenePlugin { + fn build(&self, app: &mut App) { + app.insert_resource(ClearColor(bevy::color::Color::srgba(0.0, 0.0, 0.0, 0.0))); + app.insert_resource(UIMessageSender(self.app_sender.clone())); + app.insert_resource(UIMessageReceiver(self.ui_receiver.clone())); + app.insert_resource(CubeTranslationSpeed::default()); + app.insert_resource(CubeRotationSpeed::default()); + app.add_systems(Startup, setup); + app.add_systems(Update, (sync_with_ui, animate, orbit_camera_system)); + } +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(( + Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: Color::Srgba(bevy::color::Srgba::from_f32_array( + UiState::DEFAULT_CUBE_COLOR, + )), + metallic: 0.0, + perceptual_roughness: 0.5, + ..default() + })), + Transform::from_xyz(0.0, 0.0, 0.0), + DynamicCube, + )); + + commands.spawn(( + DirectionalLight { + color: bevy::color::Color::WHITE, + illuminance: 10000.0, + shadows_enabled: false, + ..default() + }, + Transform::from_xyz(1.0, 1.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y), + )); + + commands.insert_resource(AmbientLight { + color: bevy::color::Color::WHITE, + brightness: 100.0, + affects_lightmapped_meshes: true, + }); + + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 0.0, 3.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y), + Name::new("MainCamera"), + OrbitCamera::default(), + )); +} + +fn sync_with_ui( + sender: Res, + receiver: Res, + cube_query: Query<&MeshMaterial3d, With>, + mut materials: ResMut>, + mut translation_speed: ResMut, + mut rotation_speed: ResMut, + time: Res