Add an example rendering dioxus-native to a texture in a bevy app (#4427)
Co-authored-by: Jerome Caucat <jerome.caucat@smartshape.com>
This commit is contained in:
1 parent
98f7667fb4
commit
86086477d0
9 files changed
+1569
No files matched your search
Generated
+20
@@ -10644,6 +10644,26 @@ dependencies = [
|
|||||||
"wgpu",
|
"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]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ members = [
|
|||||||
"examples/fullstack-websockets",
|
"examples/fullstack-websockets",
|
||||||
"examples/wgpu-texture",
|
"examples/wgpu-texture",
|
||||||
"examples/native-headless",
|
"examples/native-headless",
|
||||||
|
"examples/native-headless-in-bevy",
|
||||||
"examples/bevy",
|
"examples/bevy",
|
||||||
|
|
||||||
# Playwright tests
|
# Playwright tests
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
# 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 }
|
||||||
|
|
||||||
@@ -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.
|
||||||
|
|
||||||
@@ -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<UIMessage>);
|
||||||
|
|
||||||
|
#[derive(Resource, Deref)]
|
||||||
|
struct UIMessageReceiver(Receiver<UIMessage>);
|
||||||
|
|
||||||
|
#[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<UIMessage>,
|
||||||
|
pub ui_receiver: Receiver<UIMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
) {
|
||||||
|
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<UIMessageSender>,
|
||||||
|
receiver: Res<UIMessageReceiver>,
|
||||||
|
cube_query: Query<&MeshMaterial3d<StandardMaterial>, With<DynamicCube>>,
|
||||||
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
mut translation_speed: ResMut<CubeTranslationSpeed>,
|
||||||
|
mut rotation_speed: ResMut<CubeRotationSpeed>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
let fps = 1000.0 / time.delta().as_millis() as f32;
|
||||||
|
sender.send(UIMessage::Fps(fps)).unwrap();
|
||||||
|
|
||||||
|
while let Ok(message) = receiver.try_recv() {
|
||||||
|
match message {
|
||||||
|
UIMessage::CubeColor(c) => {
|
||||||
|
for cube_material in cube_query.iter() {
|
||||||
|
if let Some(material) = materials.get_mut(&cube_material.0) {
|
||||||
|
material.base_color = Color::Srgba(bevy::color::Srgba::from_f32_array(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UIMessage::CubeTranslationSpeed(speed) => {
|
||||||
|
translation_speed.0 = speed;
|
||||||
|
}
|
||||||
|
UIMessage::CubeRotationSpeed(speed) => {
|
||||||
|
rotation_speed.0 = speed;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn animate(
|
||||||
|
time: Res<Time>,
|
||||||
|
mut cube_query: Query<&mut Transform, With<DynamicCube>>,
|
||||||
|
translation_speed: Res<CubeTranslationSpeed>,
|
||||||
|
rotation_speed: Res<CubeRotationSpeed>,
|
||||||
|
) {
|
||||||
|
for mut transform in cube_query.iter_mut() {
|
||||||
|
transform.rotation = Quat::from_rotation_y(time.elapsed_secs() * rotation_speed.0);
|
||||||
|
transform.translation.x = (time.elapsed_secs() * translation_speed.0).sin() * 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn orbit_camera_system(
|
||||||
|
mut camera_query: Query<(&mut Transform, &mut OrbitCamera), With<Camera3d>>,
|
||||||
|
mut mouse_motion_events: EventReader<MouseMotion>,
|
||||||
|
mouse_button_input: Res<ButtonInput<MouseButton>>,
|
||||||
|
) {
|
||||||
|
for (mut transform, mut orbit_camera) in camera_query.iter_mut() {
|
||||||
|
// Handle mouse input for camera rotation
|
||||||
|
if mouse_button_input.pressed(MouseButton::Left) {
|
||||||
|
for mouse_motion in mouse_motion_events.read() {
|
||||||
|
orbit_camera.yaw -= mouse_motion.delta.x * orbit_camera.sensitivity;
|
||||||
|
orbit_camera.pitch -= mouse_motion.delta.y * orbit_camera.sensitivity;
|
||||||
|
|
||||||
|
// Clamp pitch to prevent camera flipping
|
||||||
|
orbit_camera.pitch = orbit_camera.pitch.clamp(-1.5, 1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate camera position based on spherical coordinates
|
||||||
|
let yaw_quat = Quat::from_rotation_y(orbit_camera.yaw);
|
||||||
|
let pitch_quat = Quat::from_rotation_x(orbit_camera.pitch);
|
||||||
|
|
||||||
|
let rotation = yaw_quat * pitch_quat;
|
||||||
|
let position = rotation * Vec3::new(0.0, 0.0, orbit_camera.distance);
|
||||||
|
|
||||||
|
transform.translation = position;
|
||||||
|
transform.look_at(Vec3::ZERO, Vec3::Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,29 @@
|
|||||||
|
mod bevy_scene_plugin;
|
||||||
|
mod dioxus_in_bevy_plugin;
|
||||||
|
mod ui;
|
||||||
|
|
||||||
|
use crate::bevy_scene_plugin::BevyScenePlugin;
|
||||||
|
use crate::dioxus_in_bevy_plugin::DioxusInBevyPlugin;
|
||||||
|
use crate::ui::{ui, UIProps};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let (ui_sender, ui_receiver) = crossbeam_channel::unbounded();
|
||||||
|
let (app_sender, app_receiver) = crossbeam_channel::unbounded();
|
||||||
|
let props = UIProps {
|
||||||
|
ui_sender,
|
||||||
|
app_receiver,
|
||||||
|
};
|
||||||
|
|
||||||
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
.add_plugins(DioxusInBevyPlugin::<UIProps> { ui, props })
|
||||||
|
.add_plugins(BevyScenePlugin {
|
||||||
|
app_sender,
|
||||||
|
ui_receiver,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
html, body, #main {
|
||||||
|
color: white;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#panel {
|
||||||
|
background-color: #C0C0C033;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 30%;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
|
color: #FFFFFF;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-bottom: 1px solid #a8a8a8;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-button {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-button:hover {
|
||||||
|
box-shadow: 0px 0px 1px #c1c1c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-button:active {
|
||||||
|
box-shadow: 0px 0px 5px #c1c1c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button-red {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button-green {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button-blue {
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
#translation-speed-control, #rotation-speed-control {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: start;
|
||||||
|
padding: 20px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
width: 3em;
|
||||||
|
height: 1.5em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
background: #444;
|
||||||
|
border: 2px solid #666;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
outline: none;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
use async_std::task::sleep;
|
||||||
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use paste::paste;
|
||||||
|
|
||||||
|
macro_rules! define_ui_state {
|
||||||
|
(
|
||||||
|
$($field:ident : $type:ty = $default:expr),* $(,)?
|
||||||
|
) => { paste! {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct UiState {
|
||||||
|
$($field: Signal<$type>,)*
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl UiState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
$($field: Signal::new($default),)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(pub const [<DEFAULT_ $field:upper>]: $type = $default;)*
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum UIMessage {
|
||||||
|
$([<$field:camel>]($type),)*
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
define_ui_state! {
|
||||||
|
cube_color: [f32; 4] = [0.0, 0.0, 1.0, 1.0],
|
||||||
|
cube_translation_speed: f32 = 2.0,
|
||||||
|
cube_rotation_speed: f32 = 1.0,
|
||||||
|
fps: f32 = 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UIProps {
|
||||||
|
pub ui_sender: Sender<UIMessage>,
|
||||||
|
pub app_receiver: Receiver<UIMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(props: UIProps) -> Element {
|
||||||
|
let mut state = use_context_provider(UiState::default);
|
||||||
|
|
||||||
|
use_effect({
|
||||||
|
let ui_sender = props.ui_sender.clone();
|
||||||
|
move || {
|
||||||
|
println!("Color changed to {:?}", state.cube_color);
|
||||||
|
ui_sender
|
||||||
|
.send(UIMessage::CubeColor((state.cube_color)()))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
use_effect({
|
||||||
|
let ui_sender = props.ui_sender.clone();
|
||||||
|
move || {
|
||||||
|
println!("Rotation speed changed to {:?}", state.cube_rotation_speed);
|
||||||
|
ui_sender
|
||||||
|
.send(UIMessage::CubeRotationSpeed((state.cube_rotation_speed)()))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
use_effect({
|
||||||
|
let ui_sender = props.ui_sender.clone();
|
||||||
|
move || {
|
||||||
|
println!(
|
||||||
|
"Translation speed changed to {:?}",
|
||||||
|
state.cube_translation_speed
|
||||||
|
);
|
||||||
|
ui_sender
|
||||||
|
.send(UIMessage::CubeTranslationSpeed((state
|
||||||
|
.cube_translation_speed)(
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
use_future(move || {
|
||||||
|
let app_receiver = props.app_receiver.clone();
|
||||||
|
async move {
|
||||||
|
loop {
|
||||||
|
// Update UI every 1s in this demo.
|
||||||
|
sleep(std::time::Duration::from_millis(1000)).await;
|
||||||
|
|
||||||
|
let mut fps = Option::<f32>::None;
|
||||||
|
|
||||||
|
while let Ok(message) = app_receiver.try_recv() {
|
||||||
|
if let UIMessage::Fps(v) = message {
|
||||||
|
fps = Some(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(fps) = fps {
|
||||||
|
state.fps.set(fps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
style { {include_str!("./ui.css")} }
|
||||||
|
div {
|
||||||
|
id: "panel",
|
||||||
|
class: "catch-events",
|
||||||
|
div {
|
||||||
|
id: "title",
|
||||||
|
h1 { "Dioxus In Bevy Example" }
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
id: "buttons",
|
||||||
|
button {
|
||||||
|
id: "button-red",
|
||||||
|
class: "color-button",
|
||||||
|
onclick: move |_| state.cube_color.set([1.0, 0.0, 0.0, 1.0]),
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
id: "button-green",
|
||||||
|
class: "color-button",
|
||||||
|
onclick: move |_| state.cube_color.set([0.0, 1.0, 0.0, 1.0]),
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
id: "button-blue",
|
||||||
|
class: "color-button",
|
||||||
|
onclick: move |_| state.cube_color.set([0.0, 0.0, 1.0, 1.0]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
id: "translation-speed-control",
|
||||||
|
label { "Translation Speed:" }
|
||||||
|
input {
|
||||||
|
r#type: "number",
|
||||||
|
min: "0.0",
|
||||||
|
max: "10.0",
|
||||||
|
step: "0.1",
|
||||||
|
value: "{state.cube_translation_speed}",
|
||||||
|
oninput: move |event| {
|
||||||
|
if let Ok(speed) = event.value().parse::<f32>() {
|
||||||
|
state.cube_translation_speed.set(speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
id: "rotation-speed-control",
|
||||||
|
label { "Rotation Speed:" }
|
||||||
|
input {
|
||||||
|
r#type: "number",
|
||||||
|
min: "0.0",
|
||||||
|
max: "10.0",
|
||||||
|
step: "0.1",
|
||||||
|
value: "{state.cube_rotation_speed}",
|
||||||
|
oninput: move |event| {
|
||||||
|
if let Ok(speed) = event.value().parse::<f32>() {
|
||||||
|
state.cube_rotation_speed.set(speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
id: "footer",
|
||||||
|
p { "Bevy framerate: {state.fps}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user