Remove bevy from the workspace (#5471)
Signed-off-by: Nico Burns <nico@nicoburns.com>
This commit is contained in:
1 parent
da445fdeb6
commit
348208d1cb
15 files changed
+44
-4496
No files matched your search
Generated
+44
-2190
File diff suppressed because it is too large.
Load diff
@@ -110,8 +110,6 @@ members = [
|
|||||||
"examples/10-integrations/pwa",
|
"examples/10-integrations/pwa",
|
||||||
"examples/10-integrations/wgpu-texture",
|
"examples/10-integrations/wgpu-texture",
|
||||||
"examples/10-integrations/native-headless",
|
"examples/10-integrations/native-headless",
|
||||||
"examples/10-integrations/native-headless-in-bevy",
|
|
||||||
"examples/10-integrations/bevy",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -204,7 +202,6 @@ anyrender_vello_cpu = { version = "0.8", default-features = false }
|
|||||||
wgpu_context = { version = "0.1", default-features = false }
|
wgpu_context = { version = "0.1", default-features = false }
|
||||||
wgpu = { version = "26.0.1" }
|
wgpu = { version = "26.0.1" }
|
||||||
vello = "0.6"
|
vello = "0.6"
|
||||||
bevy = "0.17"
|
|
||||||
|
|
||||||
|
|
||||||
# a fork of pretty please for tests - let's get off of this if we can!
|
# a fork of pretty please for tests - let's get off of this if we can!
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "bevy-example"
|
|
||||||
version = "0.0.0"
|
|
||||||
edition = "2021"
|
|
||||||
license = "MIT"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[features]
|
|
||||||
tracing = ["dep:tracing-subscriber", "dioxus-native/tracing"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bevy = { workspace = true }
|
|
||||||
dioxus-native = { workspace = true, features = ["prelude"] }
|
|
||||||
wgpu = { workspace = true }
|
|
||||||
color = "0.3.2"
|
|
||||||
tracing-subscriber = { workspace = true, optional = true }
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
use crate::bevy_scene_plugin::BevyScenePlugin;
|
|
||||||
use bevy::{
|
|
||||||
camera::{ManualTextureViewHandle, RenderTarget},
|
|
||||||
prelude::*,
|
|
||||||
render::{
|
|
||||||
render_resource::TextureFormat,
|
|
||||||
renderer::{
|
|
||||||
RenderAdapter, RenderAdapterInfo, RenderDevice, RenderInstance, RenderQueue,
|
|
||||||
WgpuWrapper,
|
|
||||||
},
|
|
||||||
settings::{RenderCreation, RenderResources},
|
|
||||||
texture::ManualTextureView,
|
|
||||||
RenderPlugin,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use dioxus_native::{CustomPaintCtx, DeviceHandle, TextureHandle};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
|
||||||
pub struct UIData {
|
|
||||||
pub color: [f32; 3],
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct BevyRenderer {
|
|
||||||
app: App,
|
|
||||||
wgpu_device: wgpu::Device,
|
|
||||||
last_texture_size: (u32, u32),
|
|
||||||
texture_handle: Option<TextureHandle>,
|
|
||||||
manual_texture_view_handle: Option<ManualTextureViewHandle>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BevyRenderer {
|
|
||||||
pub fn new(device_handle: &DeviceHandle) -> Self {
|
|
||||||
// Create a headless Bevy App.
|
|
||||||
let mut app = App::new();
|
|
||||||
app.add_plugins(
|
|
||||||
DefaultPlugins
|
|
||||||
.set(RenderPlugin {
|
|
||||||
// Reuse the render resources from the Dioxus native renderer.
|
|
||||||
render_creation: RenderCreation::Manual(RenderResources(
|
|
||||||
RenderDevice::new(WgpuWrapper::new(device_handle.device.clone())),
|
|
||||||
RenderQueue(Arc::new(WgpuWrapper::new(device_handle.queue.clone()))),
|
|
||||||
RenderAdapterInfo(WgpuWrapper::new(device_handle.adapter.get_info())),
|
|
||||||
RenderAdapter(Arc::new(WgpuWrapper::new(device_handle.adapter.clone()))),
|
|
||||||
RenderInstance(Arc::new(WgpuWrapper::new(device_handle.instance.clone()))),
|
|
||||||
)),
|
|
||||||
synchronous_pipeline_compilation: true,
|
|
||||||
..default()
|
|
||||||
})
|
|
||||||
.set(WindowPlugin {
|
|
||||||
primary_window: None,
|
|
||||||
exit_condition: bevy::window::ExitCondition::DontExit,
|
|
||||||
close_when_requested: false,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.disable::<bevy::winit::WinitPlugin>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Setup the rendering to texture.
|
|
||||||
app.insert_resource(ManualTextureViews::default());
|
|
||||||
|
|
||||||
// Add data from the UI.
|
|
||||||
app.insert_resource(UIData::default());
|
|
||||||
|
|
||||||
// Add the Bevy scene.
|
|
||||||
app.add_plugins(BevyScenePlugin {});
|
|
||||||
|
|
||||||
// Initialize the app to set up the render world properly.
|
|
||||||
app.finish();
|
|
||||||
app.cleanup();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
app,
|
|
||||||
wgpu_device: device_handle.device.clone(),
|
|
||||||
last_texture_size: (0, 0),
|
|
||||||
texture_handle: None,
|
|
||||||
manual_texture_view_handle: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render(
|
|
||||||
&mut self,
|
|
||||||
ctx: CustomPaintCtx<'_>,
|
|
||||||
color: [f32; 3],
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
_start_time: &std::time::Instant,
|
|
||||||
) -> Option<TextureHandle> {
|
|
||||||
// Update the UI data.
|
|
||||||
if let Some(mut ui) = self.app.world_mut().get_resource_mut::<UIData>() {
|
|
||||||
ui.color = color;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init self.texture_handle if None or if width/height changed.
|
|
||||||
self.init_texture(ctx, width, height);
|
|
||||||
// Run one frame of the Bevy app to render the 3D scene.
|
|
||||||
self.app.update();
|
|
||||||
|
|
||||||
self.texture_handle.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_texture(&mut self, mut ctx: CustomPaintCtx<'_>, width: u32, height: u32) {
|
|
||||||
// Reuse self.texture_handle if already initialized to the correct size.
|
|
||||||
let current_size = (width, height);
|
|
||||||
if self.texture_handle.is_some() && self.last_texture_size == current_size {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let world = self.app.world_mut();
|
|
||||||
|
|
||||||
// Skip if no camera.
|
|
||||||
if world.query::<&Camera>().single(world).is_err() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(mut manual_texture_views) = world.get_resource_mut::<ManualTextureViews>() {
|
|
||||||
// Clean previous texture if any.
|
|
||||||
if self.texture_handle.is_some() {
|
|
||||||
ctx.unregister_texture(self.texture_handle.take().unwrap());
|
|
||||||
}
|
|
||||||
if let Some(old_handle) = self.manual_texture_view_handle {
|
|
||||||
manual_texture_views.remove(&old_handle);
|
|
||||||
self.manual_texture_view_handle = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the texture for the camera target and the CustomPaintCtx.
|
|
||||||
let format = TextureFormat::Rgba8UnormSrgb;
|
|
||||||
let wgpu_texture = self.wgpu_device.create_texture(&wgpu::TextureDescriptor {
|
|
||||||
label: None,
|
|
||||||
size: wgpu::Extent3d {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
depth_or_array_layers: 1,
|
|
||||||
},
|
|
||||||
mip_level_count: 1,
|
|
||||||
sample_count: 1,
|
|
||||||
dimension: wgpu::TextureDimension::D2,
|
|
||||||
format,
|
|
||||||
usage: wgpu::TextureUsages::TEXTURE_BINDING
|
|
||||||
| wgpu::TextureUsages::RENDER_ATTACHMENT
|
|
||||||
| wgpu::TextureUsages::COPY_SRC,
|
|
||||||
view_formats: &[],
|
|
||||||
});
|
|
||||||
let wgpu_texture_view =
|
|
||||||
wgpu_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
|
||||||
let manual_texture_view = ManualTextureView {
|
|
||||||
texture_view: wgpu_texture_view.into(),
|
|
||||||
size: bevy::math::UVec2::new(width, height),
|
|
||||||
format,
|
|
||||||
};
|
|
||||||
let manual_texture_view_handle = ManualTextureViewHandle(0);
|
|
||||||
manual_texture_views.insert(manual_texture_view_handle, manual_texture_view);
|
|
||||||
|
|
||||||
if let Ok(mut camera) = world.query::<&mut Camera>().single_mut(world) {
|
|
||||||
camera.target = RenderTarget::TextureView(manual_texture_view_handle);
|
|
||||||
|
|
||||||
self.last_texture_size = current_size;
|
|
||||||
self.manual_texture_view_handle = Some(manual_texture_view_handle);
|
|
||||||
self.texture_handle = Some(ctx.register_texture(wgpu_texture));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
use crate::bevy_renderer::UIData;
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct DynamicColoredCube;
|
|
||||||
|
|
||||||
pub struct BevyScenePlugin {}
|
|
||||||
|
|
||||||
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.add_systems(Startup, setup);
|
|
||||||
app.add_systems(Update, (animate, update_cube_color));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: bevy::color::Color::srgb(1.0, 0.0, 0.0),
|
|
||||||
metallic: 0.0,
|
|
||||||
perceptual_roughness: 0.5,
|
|
||||||
..default()
|
|
||||||
})),
|
|
||||||
Transform::from_xyz(0.0, 0.0, 0.0),
|
|
||||||
DynamicColoredCube,
|
|
||||||
));
|
|
||||||
|
|
||||||
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"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn animate(time: Res<Time>, mut cube_query: Query<&mut Transform, With<DynamicColoredCube>>) {
|
|
||||||
for mut transform in cube_query.iter_mut() {
|
|
||||||
transform.rotation = Quat::from_rotation_y(time.elapsed_secs());
|
|
||||||
transform.translation.x = (time.elapsed_secs() * 2.0).sin() * 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_cube_color(
|
|
||||||
ui: Res<UIData>,
|
|
||||||
cube_query: Query<&MeshMaterial3d<StandardMaterial>, With<DynamicColoredCube>>,
|
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
||||||
) {
|
|
||||||
if ui.is_changed() {
|
|
||||||
for mesh_material in cube_query.iter() {
|
|
||||||
if let Some(material) = materials.get_mut(&mesh_material.0) {
|
|
||||||
let [r, g, b] = ui.color;
|
|
||||||
material.base_color = bevy::color::Color::srgb(r, g, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
use crate::bevy_renderer::BevyRenderer;
|
|
||||||
use crate::Color;
|
|
||||||
use dioxus_native::{CustomPaintCtx, CustomPaintSource, DeviceHandle, TextureHandle};
|
|
||||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
|
||||||
|
|
||||||
pub enum DemoMessage {
|
|
||||||
// Color in RGB format
|
|
||||||
SetColor(Color),
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DemoRendererState {
|
|
||||||
Active(Box<BevyRenderer>),
|
|
||||||
Suspended,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DemoPaintSource {
|
|
||||||
state: DemoRendererState,
|
|
||||||
start_time: std::time::Instant,
|
|
||||||
tx: Sender<DemoMessage>,
|
|
||||||
rx: Receiver<DemoMessage>,
|
|
||||||
color: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DemoPaintSource {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let (tx, rx) = channel();
|
|
||||||
Self::with_channel(tx, rx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_channel(tx: Sender<DemoMessage>, rx: Receiver<DemoMessage>) -> Self {
|
|
||||||
Self {
|
|
||||||
state: DemoRendererState::Suspended,
|
|
||||||
start_time: std::time::Instant::now(),
|
|
||||||
tx,
|
|
||||||
rx,
|
|
||||||
color: Color::WHITE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sender(&self) -> Sender<DemoMessage> {
|
|
||||||
self.tx.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_messages(&mut self) {
|
|
||||||
loop {
|
|
||||||
match self.rx.try_recv() {
|
|
||||||
Err(_) => return,
|
|
||||||
Ok(msg) => match msg {
|
|
||||||
DemoMessage::SetColor(color) => self.color = color,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(
|
|
||||||
&mut self,
|
|
||||||
ctx: CustomPaintCtx<'_>,
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
) -> Option<TextureHandle> {
|
|
||||||
if width == 0 || height == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let DemoRendererState::Active(state) = &mut self.state else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
state.render(ctx, self.color.components, width, height, &self.start_time)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CustomPaintSource for DemoPaintSource {
|
|
||||||
fn resume(&mut self, device_handle: &DeviceHandle) {
|
|
||||||
let active_state = BevyRenderer::new(device_handle);
|
|
||||||
self.state = DemoRendererState::Active(Box::new(active_state));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn suspend(&mut self) {
|
|
||||||
self.state = DemoRendererState::Suspended;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(
|
|
||||||
&mut self,
|
|
||||||
ctx: CustomPaintCtx<'_>,
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
_scale: f64,
|
|
||||||
) -> Option<TextureHandle> {
|
|
||||||
self.process_messages();
|
|
||||||
self.render(ctx, width, height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
use std::any::Any;
|
|
||||||
|
|
||||||
use color::{palette::css::WHITE, parse_color};
|
|
||||||
use color::{OpaqueColor, Srgb};
|
|
||||||
use demo_renderer::{DemoMessage, DemoPaintSource};
|
|
||||||
use dioxus_native::prelude::*;
|
|
||||||
use dioxus_native::use_wgpu;
|
|
||||||
use wgpu::Limits;
|
|
||||||
|
|
||||||
mod bevy_renderer;
|
|
||||||
mod bevy_scene_plugin;
|
|
||||||
mod demo_renderer;
|
|
||||||
|
|
||||||
// CSS Styles
|
|
||||||
static STYLES: Asset = asset!("/src/styles.css");
|
|
||||||
|
|
||||||
type Color = OpaqueColor<Srgb>;
|
|
||||||
|
|
||||||
fn limits() -> Limits {
|
|
||||||
Limits {
|
|
||||||
max_storage_buffers_per_shader_stage: 12,
|
|
||||||
..Limits::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
#[cfg(feature = "tracing")]
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
let config: Vec<Box<dyn Any>> = vec![Box::new(limits())];
|
|
||||||
dioxus_native::launch_cfg(app, Vec::new(), config);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app() -> Element {
|
|
||||||
let mut show_cube = use_signal(|| true);
|
|
||||||
|
|
||||||
let color_str = use_signal(|| String::from("red"));
|
|
||||||
let color = use_memo(move || {
|
|
||||||
parse_color(&color_str())
|
|
||||||
.map(|c| c.to_alpha_color())
|
|
||||||
.unwrap_or(WHITE)
|
|
||||||
.split()
|
|
||||||
.0
|
|
||||||
});
|
|
||||||
|
|
||||||
use_effect(move || println!("{:?}", color().components));
|
|
||||||
|
|
||||||
rsx!(
|
|
||||||
Stylesheet { href: STYLES }
|
|
||||||
div { id:"overlay",
|
|
||||||
h2 { "Control Panel" },
|
|
||||||
button {
|
|
||||||
onclick: move |_| show_cube.toggle(),
|
|
||||||
if show_cube() {
|
|
||||||
"Hide cube"
|
|
||||||
} else {
|
|
||||||
"Show cube"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
br {}
|
|
||||||
ColorControl { label: "Color:", color_str },
|
|
||||||
p { "This overlay demonstrates that the custom Bevy content can be rendered beneath layers of HTML content" }
|
|
||||||
}
|
|
||||||
div { id:"underlay",
|
|
||||||
h2 { "Underlay" },
|
|
||||||
p { "This underlay demonstrates that the custom Bevy content can be rendered above layers and blended with the content underneath" }
|
|
||||||
}
|
|
||||||
header {
|
|
||||||
h1 { "Blitz Bevy Demo" }
|
|
||||||
}
|
|
||||||
if show_cube() {
|
|
||||||
SpinningCube { color }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn ColorControl(label: &'static str, color_str: WriteSignal<String>) -> Element {
|
|
||||||
rsx!(div {
|
|
||||||
class: "color-control",
|
|
||||||
{ label },
|
|
||||||
input {
|
|
||||||
value: color_str(),
|
|
||||||
oninput: move |evt| {
|
|
||||||
*color_str.write() = evt.value()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn SpinningCube(color: Memo<Color>) -> Element {
|
|
||||||
// Create custom paint source and register it with the renderer
|
|
||||||
let paint_source = DemoPaintSource::new();
|
|
||||||
let sender = paint_source.sender();
|
|
||||||
let paint_source_id = use_wgpu(move || paint_source);
|
|
||||||
|
|
||||||
use_effect(move || {
|
|
||||||
sender.send(DemoMessage::SetColor(color())).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
rsx!(
|
|
||||||
div { id:"canvas-container",
|
|
||||||
canvas {
|
|
||||||
id: "demo-canvas",
|
|
||||||
"src": paint_source_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body, main {
|
|
||||||
height: 100%;
|
|
||||||
font-family: system-ui, sans;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 100px 1fr;
|
|
||||||
grid-template-columns: 100%;
|
|
||||||
background: #f4e8d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
#canvas-container {
|
|
||||||
display: grid;
|
|
||||||
opacity: 0.8;
|
|
||||||
grid-row: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
padding: 10px 40px;
|
|
||||||
background-color: white;
|
|
||||||
z-index: 100;
|
|
||||||
grid-row: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#overlay {
|
|
||||||
position: absolute;
|
|
||||||
width: 33%;
|
|
||||||
height: 100%;
|
|
||||||
right: 0;
|
|
||||||
z-index: 10;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
padding-top: 40%;
|
|
||||||
padding-inline: 20px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#underlay {
|
|
||||||
position: absolute;
|
|
||||||
width: 33%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: -10;
|
|
||||||
background-color: black;
|
|
||||||
padding-top: 40%;
|
|
||||||
padding-inline: 20px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-control {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
> input {
|
|
||||||
width: 150px;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# 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]
|
|
||||||
default = ["hot-reload"]
|
|
||||||
tracing = ["dep:tracing-subscriber", "dioxus-native-dom/tracing"]
|
|
||||||
hot-reload = ["dep:dioxus-devtools"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
dioxus = { workspace = true, features = ["document"]}
|
|
||||||
dioxus-native-dom = { workspace = true }
|
|
||||||
dioxus-asset-resolver = { workspace = true, features = ["native"] }
|
|
||||||
dioxus-devtools = { workspace = true, optional = true }
|
|
||||||
blitz-dom = { workspace = true, default-features = true }
|
|
||||||
blitz-paint = { workspace = true, default-features = true }
|
|
||||||
blitz-traits = { workspace = true, default-features = true }
|
|
||||||
data-url = { workspace = true, default-features = true }
|
|
||||||
anyrender_vello = { workspace = true }
|
|
||||||
wgpu = { workspace = true }
|
|
||||||
tracing-subscriber = { workspace = true, optional = true }
|
|
||||||
bevy = { workspace = true }
|
|
||||||
vello = { workspace = true }
|
|
||||||
bytes = { workspace = true }
|
|
||||||
async-std = "1.13.1"
|
|
||||||
crossbeam-channel = "0.5"
|
|
||||||
paste = "1.0.12"
|
|
||||||
rustc-hash = { workspace = true }
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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.
|
|
||||||
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
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: MessageReader<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
@@ -1,29 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spin-box {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
animation: spin 4s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from { transform:rotate(0deg); }
|
|
||||||
to { transform:rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let color = *state.cube_color.read();
|
|
||||||
let [r, g, b, a] = color.map(|c| (c * 255.0) as u8);
|
|
||||||
|
|
||||||
println!("rgba({r}, {g}, {b}, {a})");
|
|
||||||
|
|
||||||
rsx! {
|
|
||||||
document::Stylesheet { href: asset!("/src/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 {
|
|
||||||
flex: "0 0 150px",
|
|
||||||
display: "grid",
|
|
||||||
align_items: "center",
|
|
||||||
justify_items: "center",
|
|
||||||
div {
|
|
||||||
class: "spin-box",
|
|
||||||
background: "rgba({r}, {g}, {b}, {a}",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
id: "footer",
|
|
||||||
p { "Bevy framerate: {state.fps}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user