codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)
overhaul dioxus fullstack using a new implementation of server_fn This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
1 parent
8421c3aef0
commit
f2b1c6d97f
424 files changed
+17284
-6624
No files matched your search
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "bevy-example"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
default = ["desktop"]
|
||||
desktop = ["dioxus/desktop"]
|
||||
native = ["dioxus/native"]
|
||||
tracing = ["dep:tracing-subscriber", "dioxus-native/tracing"]
|
||||
|
||||
[dependencies]
|
||||
bevy = { version = "0.16" }
|
||||
dioxus-native = { workspace = true }
|
||||
dioxus = { workspace = true }
|
||||
wgpu = { workspace = true }
|
||||
color = "0.3"
|
||||
tracing-subscriber = { workspace = true, optional = true }
|
||||
@@ -0,0 +1,163 @@
|
||||
use crate::bevy_scene_plugin::BevyScenePlugin;
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
render::{
|
||||
camera::{ManualTextureView, ManualTextureViewHandle, ManualTextureViews, RenderTarget},
|
||||
render_resource::TextureFormat,
|
||||
renderer::{
|
||||
RenderAdapter, RenderAdapterInfo, RenderDevice, RenderInstance, RenderQueue,
|
||||
WgpuWrapper,
|
||||
},
|
||||
settings::{RenderCreation, RenderResources},
|
||||
RenderPlugin,
|
||||
},
|
||||
};
|
||||
use dioxus_native::{CustomPaintCtx, DeviceHandle, TextureHandle};
|
||||
use std::sync::Arc;
|
||||
use wgpu::Instance;
|
||||
|
||||
#[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(instance: &Instance, 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(instance.clone()))),
|
||||
)),
|
||||
synchronous_pipeline_compilation: true,
|
||||
..default()
|
||||
})
|
||||
.set(WindowPlugin {
|
||||
primary_window: None,
|
||||
exit_condition: bevy::window::ExitCondition::DontExit,
|
||||
close_when_requested: false,
|
||||
})
|
||||
.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
|
||||
}
|
||||
|
||||
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 let Some(texture_handle) = self.texture_handle {
|
||||
ctx.unregister_texture(texture_handle);
|
||||
self.texture_handle = None;
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use crate::bevy_renderer::BevyRenderer;
|
||||
use crate::Color;
|
||||
use dioxus_native::{CustomPaintCtx, CustomPaintSource, DeviceHandle, TextureHandle};
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use wgpu::Instance;
|
||||
|
||||
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, instance: &Instance, device_handle: &DeviceHandle) {
|
||||
let active_state = BevyRenderer::new(instance, 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
use std::any::Any;
|
||||
|
||||
use color::{palette::css::WHITE, parse_color};
|
||||
use color::{OpaqueColor, Srgb};
|
||||
use demo_renderer::{DemoMessage, DemoPaintSource};
|
||||
use dioxus::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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
* {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
dioxus = { workspace = true }
|
||||
dioxus-native-dom = { workspace = true }
|
||||
blitz-dom = { workspace = true, default-features = true }
|
||||
blitz-paint = { workspace = true, default-features = true }
|
||||
blitz-traits = { workspace = true, default-features = true }
|
||||
anyrender_vello = { workspace = true }
|
||||
wgpu = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, optional = true }
|
||||
bevy = { version = "0.16" }
|
||||
async-std = "1.13"
|
||||
crossbeam-channel = "0.5"
|
||||
paste = "1.0"
|
||||
rustc-hash = "1"
|
||||
vello = "0.5"
|
||||
|
||||
@@ -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}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "native-headless"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
tracing = ["dep:tracing-subscriber", "dioxus-native-dom/tracing"]
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["html", "hooks", "signals"] }
|
||||
dioxus-native-dom = { workspace = true }
|
||||
anyrender_vello = { workspace = true }
|
||||
vello = "0.5"
|
||||
rustc-hash = "1"
|
||||
futures-util = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, optional = true }
|
||||
wgpu = { workspace = true }
|
||||
pollster = "0.4"
|
||||
bytemuck = "1"
|
||||
blitz-paint = { workspace = true, default-features = true }
|
||||
blitz-traits = { workspace = true, default-features = true }
|
||||
blitz-dom = { workspace = true, default-features = true }
|
||||
@@ -0,0 +1,177 @@
|
||||
//! A "sketch" of how to integrate a Dioxus Native app to into a wider application
|
||||
//! by rendering the UI to a texture and driving it with your own event loop
|
||||
//!
|
||||
//! (this example is not really intended to be run as-is, and requires you to fill
|
||||
//! in the missing pieces)
|
||||
use anyrender_vello::{wgpu_context::WGPUContext, CustomPaintSource, VelloScenePainter};
|
||||
use blitz_dom::{Document as _, DocumentConfig};
|
||||
use blitz_paint::paint_scene;
|
||||
use blitz_traits::{
|
||||
events::{BlitzMouseButtonEvent, MouseEventButton, MouseEventButtons, UiEvent},
|
||||
shell::{ColorScheme, Viewport},
|
||||
};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_native_dom::DioxusDocument;
|
||||
use pollster::FutureExt as _;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::sync::Arc;
|
||||
use std::task::Context;
|
||||
use vello::{
|
||||
peniko::color::AlphaColor, RenderParams, Renderer as VelloRenderer, RendererOptions, Scene,
|
||||
};
|
||||
use wgpu::TextureFormat;
|
||||
|
||||
// Constant width, height, scale factor and color schemefor example purposes
|
||||
const SCALE_FACTOR: f32 = 1.0;
|
||||
const WIDTH: u32 = 800;
|
||||
const HEIGHT: u32 = 600;
|
||||
const COLOR_SCHEME: ColorScheme = ColorScheme::Light;
|
||||
|
||||
// Example Dioxus app.
|
||||
fn app() -> Element {
|
||||
rsx! {
|
||||
div { "Hello, world!" }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// =============
|
||||
// INITIAL SETUP
|
||||
// =============
|
||||
|
||||
let waker = create_waker(Box::new(|| {
|
||||
// This should wake up and "poll" your event loop
|
||||
}));
|
||||
|
||||
// Create the dioxus virtual dom and the dioxus-native document
|
||||
// It is important to set the width, height, and scale factor on the document as these are used for layout.
|
||||
let vdom = VirtualDom::new(app);
|
||||
let mut dioxus_doc = DioxusDocument::new(
|
||||
vdom,
|
||||
DocumentConfig {
|
||||
viewport: Some(Viewport::new(WIDTH, HEIGHT, SCALE_FACTOR, COLOR_SCHEME)),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// Setup a WGPU Device and Queue
|
||||
//
|
||||
// There is nothing special about WGPUContext. It is just used to
|
||||
// reduce the amount of boilerplate associated with setting up WGPU
|
||||
let mut wgpu_context = WGPUContext::new();
|
||||
let device_id = wgpu_context
|
||||
.find_or_create_device(None)
|
||||
.block_on()
|
||||
.expect("Failed to create WGPU device");
|
||||
let device_handle = &wgpu_context.device_pool[device_id];
|
||||
let device = device_handle.device.clone();
|
||||
let queue = device_handle.queue.clone();
|
||||
|
||||
// Create Vello renderer
|
||||
// Note: creating a VelloRenderer is expensive, so it should be done once per Device.
|
||||
let mut vello_renderer = VelloRenderer::new(&device, RendererOptions::default()).unwrap();
|
||||
|
||||
// =============
|
||||
// CREATE TEXTURE (RECREATE ON RESIZE)
|
||||
// =============
|
||||
|
||||
// Create texture and texture view
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
view_formats: &[],
|
||||
});
|
||||
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
// =============
|
||||
// EACH FRAME
|
||||
// =============
|
||||
|
||||
// Poll the vdom
|
||||
dioxus_doc.poll(Some(Context::from_waker(&waker)));
|
||||
|
||||
// Create a `VelloScenePainter` to paint into
|
||||
let mut custom_paint_sources =
|
||||
FxHashMap::<u64, Box<dyn CustomPaintSource + 'static>>::default();
|
||||
let mut scene_painter = VelloScenePainter {
|
||||
inner: Scene::new(),
|
||||
renderer: &mut vello_renderer,
|
||||
custom_paint_sources: &mut custom_paint_sources,
|
||||
};
|
||||
|
||||
// Paint the document using `blitz_paint::paint_scene`
|
||||
//
|
||||
// Note: the `paint_scene` will work with any implementation of `anyrender::PaintScene`
|
||||
// so you could also write your own implementation if you want more control over rendering
|
||||
// (i.e. to render a custom renderer instead of Vello)
|
||||
paint_scene(
|
||||
&mut scene_painter,
|
||||
&dioxus_doc,
|
||||
SCALE_FACTOR as f64,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
);
|
||||
|
||||
// Extract the `vello::Scene` from the `VelloScenePainter`
|
||||
let scene = scene_painter.finish();
|
||||
|
||||
// Render the `vello::Scene` to the Texture using the `VelloRenderer`
|
||||
vello_renderer
|
||||
.render_to_texture(
|
||||
&device,
|
||||
&queue,
|
||||
&scene,
|
||||
&texture_view,
|
||||
&RenderParams {
|
||||
base_color: AlphaColor::TRANSPARENT,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
antialiasing_method: vello::AaConfig::Msaa16,
|
||||
},
|
||||
)
|
||||
.expect("failed to render to texture");
|
||||
|
||||
// `texture` will now contain the rendered Scene
|
||||
|
||||
// =============
|
||||
// EVENT HANDLING
|
||||
// =============
|
||||
|
||||
let event = UiEvent::MouseDown(BlitzMouseButtonEvent {
|
||||
x: 30.0,
|
||||
y: 40.0,
|
||||
button: MouseEventButton::Main,
|
||||
buttons: MouseEventButtons::Primary, // keep track of all pressed buttons
|
||||
mods: Modifiers::empty(), // ctrl, alt, shift, etc
|
||||
});
|
||||
dioxus_doc.handle_ui_event(event);
|
||||
|
||||
// Trigger a poll via your event loop (or wait for next frame)
|
||||
}
|
||||
|
||||
/// Create a waker that will call an arbitrary callback
|
||||
pub fn create_waker(callback: Box<dyn Fn() + 'static + Send + Sync>) -> std::task::Waker {
|
||||
struct DomHandle {
|
||||
callback: Box<dyn Fn() + 'static + Send + Sync>,
|
||||
}
|
||||
|
||||
impl futures_util::task::ArcWake for DomHandle {
|
||||
fn wake_by_ref(arc_self: &Arc<Self>) {
|
||||
(arc_self.callback)()
|
||||
}
|
||||
}
|
||||
|
||||
futures_util::task::waker(Arc::new(DomHandle { callback }))
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "dioxus-pwa-example"
|
||||
version = "0.1.0"
|
||||
authors = ["Antonio Curavalea <one.kyonblack@gmail.com>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { workspace = true, features = ["web"] }
|
||||
@@ -0,0 +1,27 @@
|
||||
[application]
|
||||
|
||||
# App (Project) Name
|
||||
name = "dioxus-pwa-example"
|
||||
|
||||
# Dioxus App Default Platform
|
||||
# desktop, web, mobile, ssr
|
||||
default_platform = "web"
|
||||
|
||||
# `build` & `serve` dist path
|
||||
out_dir = "dist"
|
||||
|
||||
# resource (public) file folder
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "dioxus | ⛺"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
# when watcher trigger, regenerate the `index.html`
|
||||
reload_html = true
|
||||
|
||||
# which files or dirs will be watcher monitoring
|
||||
watch_path = ["src", "public"]
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Dioxus
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Dioxus PWA example
|
||||
|
||||
This is a basic example of a progressive web app (PWA) using Dioxus and Dioxus CLI.
|
||||
Currently PWA functionality requires the use of a service worker and manifest file, so this isn't 100% Rust yet.
|
||||
|
||||
It is also very much usable as a template for your projects, if you're aiming to create a PWA.
|
||||
|
||||
## Try the example
|
||||
|
||||
Make sure you have Dioxus CLI installed (if you're unsure, run `cargo install dioxus-cli --locked`).
|
||||
|
||||
You can run `dx serve` in this directory to start the web server locally, or run
|
||||
`dx build --release` to build the project so you can deploy it on a separate web-server.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
├── Cargo.toml
|
||||
├── Dioxus.toml
|
||||
├── index.html // Custom HTML is needed for this, to load the SW and manifest.
|
||||
├── LICENSE
|
||||
├── public
|
||||
│ ├── favicon.ico
|
||||
│ ├── logo_192.png
|
||||
│ ├── logo_512.png
|
||||
│ ├── manifest.json // The manifest file - edit this as you need to.
|
||||
│ └── sw.js // The service worker - you must edit this for actual projects.
|
||||
├── README.md
|
||||
└── src
|
||||
└── main.rs
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
If you're just getting started with PWAs, here are some useful resources:
|
||||
|
||||
- [PWABuilder docs](https://docs.pwabuilder.com/#/)
|
||||
- [MDN article on PWAs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)
|
||||
|
||||
For service worker scripting (in JavaScript):
|
||||
|
||||
- [Service worker guide from PWABuilder](https://docs.pwabuilder.com/#/home/sw-intro)
|
||||
- [Service worker examples, also from PWABuilder](https://github.com/pwa-builder/pwabuilder-serviceworkers)
|
||||
|
||||
If you want to stay as close to 100% Rust as possible, you can try using [wasi-worker](https://github.com/dunnock/wasi-worker) to replace the JS service worker file. The JSON manifest will still be required though.
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{app_title}</title>
|
||||
<script>
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("/{base_path}/assets/sw.js");
|
||||
}
|
||||
</script>
|
||||
<link rel="manifest" href="/assets/manifest.json" />
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "Dioxus",
|
||||
"icons": [
|
||||
{
|
||||
"src": "logo_192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo_512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "logo_512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "any",
|
||||
"purpose": "any"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"id": "/",
|
||||
"display": "standalone",
|
||||
"display_override": ["window-control-overlay", "standalone"],
|
||||
"scope": "/",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff",
|
||||
"short_name": "Dioxus",
|
||||
"description": "Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust.",
|
||||
"dir": "ltr",
|
||||
"lang": "en",
|
||||
"orientation": "portrait"
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
"use strict";
|
||||
|
||||
//console.log('WORKER: executing.');
|
||||
|
||||
/* A version number is useful when updating the worker logic,
|
||||
allowing you to remove outdated cache entries during the update.
|
||||
*/
|
||||
var version = 'v1.0.0::';
|
||||
|
||||
/* These resources will be downloaded and cached by the service worker
|
||||
during the installation process. If any resource fails to be downloaded,
|
||||
then the service worker won't be installed either.
|
||||
*/
|
||||
var offlineFundamentals = [
|
||||
// add here the files you want to cache
|
||||
'favicon.ico'
|
||||
];
|
||||
|
||||
/* The install event fires when the service worker is first installed.
|
||||
You can use this event to prepare the service worker to be able to serve
|
||||
files while visitors are offline.
|
||||
*/
|
||||
self.addEventListener("install", function (event) {
|
||||
//console.log('WORKER: install event in progress.');
|
||||
/* Using event.waitUntil(p) blocks the installation process on the provided
|
||||
promise. If the promise is rejected, the service worker won't be installed.
|
||||
*/
|
||||
event.waitUntil(
|
||||
/* The caches built-in is a promise-based API that helps you cache responses,
|
||||
as well as finding and deleting them.
|
||||
*/
|
||||
caches
|
||||
/* You can open a cache by name, and this method returns a promise. We use
|
||||
a versioned cache name here so that we can remove old cache entries in
|
||||
one fell swoop later, when phasing out an older service worker.
|
||||
*/
|
||||
.open(version + 'fundamentals')
|
||||
.then(function (cache) {
|
||||
/* After the cache is opened, we can fill it with the offline fundamentals.
|
||||
The method below will add all resources in `offlineFundamentals` to the
|
||||
cache, after making requests for them.
|
||||
*/
|
||||
return cache.addAll(offlineFundamentals);
|
||||
})
|
||||
.then(function () {
|
||||
//console.log('WORKER: install completed');
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/* The fetch event fires whenever a page controlled by this service worker requests
|
||||
a resource. This isn't limited to `fetch` or even XMLHttpRequest. Instead, it
|
||||
comprehends even the request for the HTML page on first load, as well as JS and
|
||||
CSS resources, fonts, any images, etc.
|
||||
*/
|
||||
self.addEventListener("fetch", function (event) {
|
||||
//console.log('WORKER: fetch event in progress.');
|
||||
|
||||
/* We should only cache GET requests, and deal with the rest of method in the
|
||||
client-side, by handling failed POST,PUT,PATCH,etc. requests.
|
||||
*/
|
||||
if (event.request.method !== 'GET') {
|
||||
/* If we don't block the event as shown below, then the request will go to
|
||||
the network as usual.
|
||||
*/
|
||||
//console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);
|
||||
return;
|
||||
}
|
||||
/* Similar to event.waitUntil in that it blocks the fetch event on a promise.
|
||||
Fulfillment result will be used as the response, and rejection will end in a
|
||||
HTTP response indicating failure.
|
||||
*/
|
||||
event.respondWith(
|
||||
caches
|
||||
/* This method returns a promise that resolves to a cache entry matching
|
||||
the request. Once the promise is settled, we can then provide a response
|
||||
to the fetch request.
|
||||
*/
|
||||
.match(event.request)
|
||||
.then(function (cached) {
|
||||
/* Even if the response is in our cache, we go to the network as well.
|
||||
This pattern is known for producing "eventually fresh" responses,
|
||||
where we return cached responses immediately, and meanwhile pull
|
||||
a network response and store that in the cache.
|
||||
|
||||
Read more:
|
||||
https://ponyfoo.com/articles/progressive-networking-serviceworker
|
||||
*/
|
||||
var networked = fetch(event.request)
|
||||
// We handle the network request with success and failure scenarios.
|
||||
.then(fetchedFromNetwork, unableToResolve)
|
||||
// We should catch errors on the fetchedFromNetwork handler as well.
|
||||
.catch(unableToResolve);
|
||||
|
||||
/* We return the cached response immediately if there is one, and fall
|
||||
back to waiting on the network as usual.
|
||||
*/
|
||||
//console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url);
|
||||
return cached || networked;
|
||||
|
||||
function fetchedFromNetwork(response) {
|
||||
/* We copy the response before replying to the network request.
|
||||
This is the response that will be stored on the ServiceWorker cache.
|
||||
*/
|
||||
var cacheCopy = response.clone();
|
||||
|
||||
//console.log('WORKER: fetch response from network.', event.request.url);
|
||||
|
||||
caches
|
||||
// We open a cache to store the response for this request.
|
||||
.open(version + 'pages')
|
||||
.then(function add(cache) {
|
||||
/* We store the response for this request. It'll later become
|
||||
available to caches.match(event.request) calls, when looking
|
||||
for cached responses.
|
||||
*/
|
||||
cache.put(event.request, cacheCopy);
|
||||
})
|
||||
.then(function () {
|
||||
//console.log('WORKER: fetch response stored in cache.', event.request.url);
|
||||
});
|
||||
|
||||
// Return the response so that the promise is settled in fulfillment.
|
||||
return response;
|
||||
}
|
||||
|
||||
/* When this method is called, it means we were unable to produce a response
|
||||
from either the cache or the network. This is our opportunity to produce
|
||||
a meaningful response even when all else fails. It's the last chance, so
|
||||
you probably want to display a "Service Unavailable" view or a generic
|
||||
error response.
|
||||
*/
|
||||
function unableToResolve() {
|
||||
/* There's a couple of things we can do here.
|
||||
- Test the Accept header and then return one of the `offlineFundamentals`
|
||||
e.g: `return caches.match('/some/cached/image.png')`
|
||||
- You should also consider the origin. It's easier to decide what
|
||||
"unavailable" means for requests against your origins than for requests
|
||||
against a third party, such as an ad provider.
|
||||
- Generate a Response programmatically, as shown below, and return that.
|
||||
*/
|
||||
|
||||
//console.log('WORKER: fetch request failed in both cache and network.');
|
||||
|
||||
/* Here we're creating a response programmatically. The first parameter is the
|
||||
response body, and the second one defines the options for the response.
|
||||
*/
|
||||
return new Response('<h1>Service Unavailable</h1>', {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'text/html'
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/* The activate event fires after a service worker has been successfully installed.
|
||||
It is most useful when phasing out an older version of a service worker, as at
|
||||
this point you know that the new worker was installed correctly. In this example,
|
||||
we delete old caches that don't match the version in the worker we just finished
|
||||
installing.
|
||||
*/
|
||||
self.addEventListener("activate", function (event) {
|
||||
/* Just like with the install event, event.waitUntil blocks activate on a promise.
|
||||
Activation will fail unless the promise is fulfilled.
|
||||
*/
|
||||
//console.log('WORKER: activate event in progress.');
|
||||
|
||||
event.waitUntil(
|
||||
caches
|
||||
/* This method returns a promise which will resolve to an array of available
|
||||
cache keys.
|
||||
*/
|
||||
.keys()
|
||||
.then(function (keys) {
|
||||
// We return a promise that settles when all outdated caches are deleted.
|
||||
return Promise.all(
|
||||
keys
|
||||
.filter(function (key) {
|
||||
// Filter by keys that don't start with the latest version prefix.
|
||||
return !key.startsWith(version);
|
||||
})
|
||||
.map(function (key) {
|
||||
/* Return a promise that's fulfilled
|
||||
when each outdated cache is deleted.
|
||||
*/
|
||||
return caches.delete(key);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(function () {
|
||||
//console.log('WORKER: activate completed.');
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
rsx! (
|
||||
div { style: "text-align: center;",
|
||||
h1 { "🌗 Dioxus 🚀" }
|
||||
h3 { "Frontend that scales." }
|
||||
p { "Build web, desktop, and mobile apps with Dioxus" }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
dist
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "dioxus-tailwind"
|
||||
version = "0.0.0"
|
||||
authors = []
|
||||
edition = "2021"
|
||||
description = "A tailwindcss example using Dioxus"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
manganis = { workspace = true }
|
||||
dioxus = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["desktop"]
|
||||
web = ["dioxus/web"]
|
||||
desktop = ["dioxus/desktop"]
|
||||
@@ -0,0 +1,9 @@
|
||||
Example: Basic Tailwind usage
|
||||
|
||||
This example shows how an app might be styled with TailwindCSS.
|
||||
|
||||
## Running
|
||||
|
||||
Our [Tailwind](https://dioxuslabs.com/learn/0.6/cookbook/tailwind) guide explains how to setup and run Dioxus-Tailwind projects.
|
||||
|
||||
Note that in Dioxus 0.7, the Tailwind watcher is initialized automatically if a `tailwind.css` file is find in your app's root.
|
||||
@@ -0,0 +1,465 @@
|
||||
/*! tailwindcss v4.1.0 | MIT License | https://tailwindcss.com */
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
@layer base {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-border-style: solid;
|
||||
--tw-leading: initial;
|
||||
--tw-font-weight: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
@layer theme, base, components, utilities;
|
||||
@layer theme {
|
||||
:root, :host {
|
||||
--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
--color-indigo-500: oklch(58.5% 0.233 277.117);
|
||||
--color-indigo-600: oklch(51.1% 0.262 276.966);
|
||||
--color-gray-400: oklch(70.7% 0.022 261.325);
|
||||
--color-gray-700: oklch(37.3% 0.034 259.733);
|
||||
--color-gray-800: oklch(27.8% 0.033 256.848);
|
||||
--color-gray-900: oklch(21% 0.034 264.665);
|
||||
--color-white: #fff;
|
||||
--spacing: 0.25rem;
|
||||
--container-lg: 32rem;
|
||||
--text-base: 1rem;
|
||||
--text-base--line-height: calc(1.5 / 1);
|
||||
--text-lg: 1.125rem;
|
||||
--text-lg--line-height: calc(1.75 / 1.125);
|
||||
--text-xl: 1.25rem;
|
||||
--text-xl--line-height: calc(1.75 / 1.25);
|
||||
--text-3xl: 1.875rem;
|
||||
--text-3xl--line-height: calc(2.25 / 1.875);
|
||||
--text-4xl: 2.25rem;
|
||||
--text-4xl--line-height: calc(2.5 / 2.25);
|
||||
--font-weight-medium: 500;
|
||||
--leading-relaxed: 1.625;
|
||||
--radius-sm: 0.25rem;
|
||||
--default-font-family: var(--font-sans);
|
||||
--default-mono-font-family: var(--font-mono);
|
||||
}
|
||||
}
|
||||
@layer base {
|
||||
*, ::after, ::before, ::backdrop, ::file-selector-button {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0 solid;
|
||||
}
|
||||
html, :host {
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
tab-size: 4;
|
||||
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
|
||||
font-feature-settings: var(--default-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-font-variation-settings, normal);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
hr {
|
||||
height: 0;
|
||||
color: inherit;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
-webkit-text-decoration: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
b, strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
code, kbd, samp, pre {
|
||||
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
|
||||
font-feature-settings: var(--default-mono-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-mono-font-variation-settings, normal);
|
||||
font-size: 1em;
|
||||
}
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
sub, sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
table {
|
||||
text-indent: 0;
|
||||
border-color: inherit;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
ol, ul, menu {
|
||||
list-style: none;
|
||||
}
|
||||
img, svg, video, canvas, audio, iframe, embed, object {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
button, input, select, optgroup, textarea, ::file-selector-button {
|
||||
font: inherit;
|
||||
font-feature-settings: inherit;
|
||||
font-variation-settings: inherit;
|
||||
letter-spacing: inherit;
|
||||
color: inherit;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
opacity: 1;
|
||||
}
|
||||
:where(select:is([multiple], [size])) optgroup {
|
||||
font-weight: bolder;
|
||||
}
|
||||
:where(select:is([multiple], [size])) optgroup option {
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
::file-selector-button {
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
::placeholder {
|
||||
opacity: 1;
|
||||
}
|
||||
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
|
||||
::placeholder {
|
||||
color: color-mix(in oklab, currentColor 50%, transparent);
|
||||
}
|
||||
}
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
::-webkit-date-and-time-value {
|
||||
min-height: 1lh;
|
||||
text-align: inherit;
|
||||
}
|
||||
::-webkit-datetime-edit {
|
||||
display: inline-flex;
|
||||
}
|
||||
::-webkit-datetime-edit-fields-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
|
||||
padding-block: 0;
|
||||
}
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
|
||||
appearance: button;
|
||||
}
|
||||
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
[hidden]:where(:not([hidden="until-found"])) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@layer utilities {
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
max-width: 64rem;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
max-width: 80rem;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.mx-auto {
|
||||
margin-inline: auto;
|
||||
}
|
||||
.mt-4 {
|
||||
margin-top: calc(var(--spacing) * 4);
|
||||
}
|
||||
.mr-5 {
|
||||
margin-right: calc(var(--spacing) * 5);
|
||||
}
|
||||
.mb-4 {
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
}
|
||||
.mb-8 {
|
||||
margin-bottom: calc(var(--spacing) * 8);
|
||||
}
|
||||
.mb-16 {
|
||||
margin-bottom: calc(var(--spacing) * 16);
|
||||
}
|
||||
.ml-1 {
|
||||
margin-left: calc(var(--spacing) * 1);
|
||||
}
|
||||
.ml-3 {
|
||||
margin-left: calc(var(--spacing) * 3);
|
||||
}
|
||||
.ml-4 {
|
||||
margin-left: calc(var(--spacing) * 4);
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.inline-flex {
|
||||
display: inline-flex;
|
||||
}
|
||||
.h-4 {
|
||||
height: calc(var(--spacing) * 4);
|
||||
}
|
||||
.h-10 {
|
||||
height: calc(var(--spacing) * 10);
|
||||
}
|
||||
.w-4 {
|
||||
width: calc(var(--spacing) * 4);
|
||||
}
|
||||
.w-5\/6 {
|
||||
width: calc(5/6 * 100%);
|
||||
}
|
||||
.w-10 {
|
||||
width: calc(var(--spacing) * 10);
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.rounded-full {
|
||||
border-radius: calc(infinity * 1px);
|
||||
}
|
||||
.rounded-sm {
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.border-0 {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 0px;
|
||||
}
|
||||
.bg-gray-800 {
|
||||
background-color: var(--color-gray-800);
|
||||
}
|
||||
.bg-gray-900 {
|
||||
background-color: var(--color-gray-900);
|
||||
}
|
||||
.bg-indigo-500 {
|
||||
background-color: var(--color-indigo-500);
|
||||
}
|
||||
.object-cover {
|
||||
object-fit: cover;
|
||||
}
|
||||
.object-center {
|
||||
object-position: center;
|
||||
}
|
||||
.p-2 {
|
||||
padding: calc(var(--spacing) * 2);
|
||||
}
|
||||
.p-5 {
|
||||
padding: calc(var(--spacing) * 5);
|
||||
}
|
||||
.px-3 {
|
||||
padding-inline: calc(var(--spacing) * 3);
|
||||
}
|
||||
.px-5 {
|
||||
padding-inline: calc(var(--spacing) * 5);
|
||||
}
|
||||
.px-6 {
|
||||
padding-inline: calc(var(--spacing) * 6);
|
||||
}
|
||||
.py-1 {
|
||||
padding-block: calc(var(--spacing) * 1);
|
||||
}
|
||||
.py-2 {
|
||||
padding-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
.py-24 {
|
||||
padding-block: calc(var(--spacing) * 24);
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.text-3xl {
|
||||
font-size: var(--text-3xl);
|
||||
line-height: var(--tw-leading, var(--text-3xl--line-height));
|
||||
}
|
||||
.text-base {
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--tw-leading, var(--text-base--line-height));
|
||||
}
|
||||
.text-lg {
|
||||
font-size: var(--text-lg);
|
||||
line-height: var(--tw-leading, var(--text-lg--line-height));
|
||||
}
|
||||
.text-xl {
|
||||
font-size: var(--text-xl);
|
||||
line-height: var(--tw-leading, var(--text-xl--line-height));
|
||||
}
|
||||
.leading-relaxed {
|
||||
--tw-leading: var(--leading-relaxed);
|
||||
line-height: var(--leading-relaxed);
|
||||
}
|
||||
.font-medium {
|
||||
--tw-font-weight: var(--font-weight-medium);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
.text-gray-400 {
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
.text-white {
|
||||
color: var(--color-white);
|
||||
}
|
||||
.hover\:bg-gray-700 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-gray-700);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-indigo-600 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-indigo-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:text-white {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--color-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
.focus\:outline-hidden {
|
||||
&:focus {
|
||||
--tw-outline-style: none;
|
||||
outline-style: none;
|
||||
@media (forced-colors: active) {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.sm\:text-4xl {
|
||||
@media (width >= 40rem) {
|
||||
font-size: var(--text-4xl);
|
||||
line-height: var(--tw-leading, var(--text-4xl--line-height));
|
||||
}
|
||||
}
|
||||
.md\:mt-0 {
|
||||
@media (width >= 48rem) {
|
||||
margin-top: calc(var(--spacing) * 0);
|
||||
}
|
||||
}
|
||||
.md\:mb-0 {
|
||||
@media (width >= 48rem) {
|
||||
margin-bottom: calc(var(--spacing) * 0);
|
||||
}
|
||||
}
|
||||
.md\:ml-auto {
|
||||
@media (width >= 48rem) {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
.md\:w-1\/2 {
|
||||
@media (width >= 48rem) {
|
||||
width: calc(1/2 * 100%);
|
||||
}
|
||||
}
|
||||
.md\:flex-row {
|
||||
@media (width >= 48rem) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
.md\:items-start {
|
||||
@media (width >= 48rem) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
.md\:pr-16 {
|
||||
@media (width >= 48rem) {
|
||||
padding-right: calc(var(--spacing) * 16);
|
||||
}
|
||||
}
|
||||
.md\:text-left {
|
||||
@media (width >= 48rem) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
.lg\:inline-block {
|
||||
@media (width >= 64rem) {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
.lg\:w-full {
|
||||
@media (width >= 64rem) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.lg\:max-w-lg {
|
||||
@media (width >= 64rem) {
|
||||
max-width: var(--container-lg);
|
||||
}
|
||||
}
|
||||
.lg\:grow {
|
||||
@media (width >= 64rem) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
.lg\:pr-24 {
|
||||
@media (width >= 64rem) {
|
||||
padding-right: calc(var(--spacing) * 24);
|
||||
}
|
||||
}
|
||||
}
|
||||
@property --tw-border-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@property --tw-leading {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-font-weight {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::launch(app);
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
let grey_background = true;
|
||||
|
||||
rsx! {
|
||||
Stylesheet { href: asset!("/assets/tailwind.css") }
|
||||
div {
|
||||
header {
|
||||
class: "text-gray-400 body-font",
|
||||
// you can use optional attributes to optionally apply a tailwind class
|
||||
class: if grey_background { "bg-gray-900" },
|
||||
div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center",
|
||||
a { class: "flex title-font font-medium items-center text-white mb-4 md:mb-0",
|
||||
StacksIcon {}
|
||||
span { class: "ml-3 text-xl", "Hello Dioxus!" }
|
||||
}
|
||||
nav { class: "md:ml-auto flex flex-wrap items-center text-base justify-center",
|
||||
a { class: "mr-5 hover:text-white", "First Link" }
|
||||
a { class: "mr-5 hover:text-white", "Second Link" }
|
||||
a { class: "mr-5 hover:text-white", "Third Link" }
|
||||
a { class: "mr-5 hover:text-white", "Fourth Link" }
|
||||
}
|
||||
button { class: "inline-flex items-center bg-gray-800 border-0 py-1 px-3 focus:outline-hidden hover:bg-gray-700 rounded-sm text-base mt-4 md:mt-0",
|
||||
"Button"
|
||||
RightArrowIcon {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section { class: "text-gray-400 bg-gray-900 body-font",
|
||||
div { class: "container mx-auto flex px-5 py-24 md:flex-row flex-col items-center",
|
||||
div { class: "lg:grow md:w-1/2 lg:pr-24 md:pr-16 flex flex-col md:items-start md:text-left mb-16 md:mb-0 items-center text-center",
|
||||
h1 { class: "title-font sm:text-4xl text-3xl mb-4 font-medium text-white",
|
||||
br { class: "hidden lg:inline-block" }
|
||||
"Dioxus Sneak Peek"
|
||||
}
|
||||
p { class: "mb-8 leading-relaxed",
|
||||
|
||||
"Dioxus is a new UI framework that makes it easy and simple to write cross-platform apps using web
|
||||
technologies! It is functional, fast, and portable. Dioxus can run on the web, on the desktop, and
|
||||
on mobile and embedded platforms."
|
||||
}
|
||||
div { class: "flex justify-center",
|
||||
button { class: "inline-flex text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-hidden hover:bg-indigo-600 rounded-sm text-lg",
|
||||
"Learn more"
|
||||
}
|
||||
button { class: "ml-4 inline-flex text-gray-400 bg-gray-800 border-0 py-2 px-6 focus:outline-hidden hover:bg-gray-700 hover:text-white rounded-sm text-lg",
|
||||
"Build an app"
|
||||
}
|
||||
}
|
||||
}
|
||||
div { class: "lg:max-w-lg lg:w-full md:w-1/2 w-5/6",
|
||||
img {
|
||||
class: "object-cover object-center rounded-sm",
|
||||
src: "https://i.imgur.com/oK6BLtw.png",
|
||||
referrerpolicy: "no-referrer",
|
||||
alt: "hero",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn StacksIcon() -> Element {
|
||||
rsx! {
|
||||
svg {
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
stroke_linecap: "round",
|
||||
stroke_linejoin: "round",
|
||||
stroke_width: "2",
|
||||
class: "w-10 h-10 text-white p-2 bg-indigo-500 rounded-full",
|
||||
view_box: "0 0 24 24",
|
||||
path { d: "M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RightArrowIcon() -> Element {
|
||||
rsx! {
|
||||
svg {
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
stroke_linecap: "round",
|
||||
stroke_linejoin: "round",
|
||||
stroke_width: "2",
|
||||
class: "w-4 h-4 ml-1",
|
||||
view_box: "0 0 24 24",
|
||||
path { d: "M5 12h14M12 5l7 7-7 7" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@source "./src/**/*.{rs,html,css}";
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "wgpu-texture"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
default = ["desktop"]
|
||||
desktop = ["dioxus/desktop"]
|
||||
native = ["dioxus/native"]
|
||||
tracing = ["dep:tracing-subscriber", "dioxus-native/tracing"]
|
||||
|
||||
[dependencies]
|
||||
dioxus-native = { workspace = true }
|
||||
dioxus = { workspace = true }
|
||||
wgpu = { workspace = true }
|
||||
winit = "0.30"
|
||||
bytemuck = "1"
|
||||
color = "0.3"
|
||||
tracing-subscriber = { workspace = true, optional = true }
|
||||
@@ -0,0 +1,260 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
use crate::Color;
|
||||
use dioxus_native::{CustomPaintCtx, CustomPaintSource, DeviceHandle, TextureHandle};
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::time::Instant;
|
||||
use wgpu::{
|
||||
CommandEncoderDescriptor, Device, Extent3d, FragmentState, Instance, LoadOp, MultisampleState,
|
||||
Operations, PipelineLayoutDescriptor, PrimitiveState, PushConstantRange, Queue,
|
||||
RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline, RenderPipelineDescriptor,
|
||||
ShaderModuleDescriptor, ShaderSource, ShaderStages, StoreOp, Texture, TextureDescriptor,
|
||||
TextureDimension, TextureFormat, TextureUsages, TextureViewDescriptor, VertexState,
|
||||
};
|
||||
|
||||
pub struct DemoPaintSource {
|
||||
state: DemoRendererState,
|
||||
start_time: std::time::Instant,
|
||||
tx: Sender<DemoMessage>,
|
||||
rx: Receiver<DemoMessage>,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl CustomPaintSource for DemoPaintSource {
|
||||
fn resume(&mut self, _instance: &Instance, device_handle: &DeviceHandle) {
|
||||
// Extract device and queue from device_handle
|
||||
let device = &device_handle.device;
|
||||
let queue = &device_handle.queue;
|
||||
let active_state = ActiveDemoRenderer::new(device, queue);
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DemoMessage {
|
||||
// Color in RGB format
|
||||
SetColor(Color),
|
||||
}
|
||||
|
||||
enum DemoRendererState {
|
||||
Active(Box<ActiveDemoRenderer>),
|
||||
Suspended,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TextureAndHandle {
|
||||
texture: Texture,
|
||||
handle: TextureHandle,
|
||||
}
|
||||
|
||||
struct ActiveDemoRenderer {
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
pipeline: RenderPipeline,
|
||||
displayed_texture: Option<TextureAndHandle>,
|
||||
next_texture: Option<TextureAndHandle>,
|
||||
}
|
||||
|
||||
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 ActiveDemoRenderer {
|
||||
pub(crate) fn new(device: &Device, queue: &Queue) -> Self {
|
||||
let shader = device.create_shader_module(ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!("shader.wgsl"))),
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &[],
|
||||
push_constant_ranges: &[PushConstantRange {
|
||||
stages: ShaderStages::FRAGMENT,
|
||||
range: 0..16, // full size in bytes, aligned
|
||||
}],
|
||||
});
|
||||
|
||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(TextureFormat::Rgba8Unorm.into())],
|
||||
}),
|
||||
primitive: PrimitiveState::default(),
|
||||
depth_stencil: None,
|
||||
multisample: MultisampleState::default(),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
pipeline,
|
||||
displayed_texture: None,
|
||||
next_texture: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn render(
|
||||
&mut self,
|
||||
mut ctx: CustomPaintCtx<'_>,
|
||||
light: [f32; 3],
|
||||
width: u32,
|
||||
height: u32,
|
||||
start_time: &Instant,
|
||||
) -> Option<TextureHandle> {
|
||||
// If "next texture" size doesn't match specified size then unregister and drop texture
|
||||
if let Some(next) = &self.next_texture {
|
||||
if next.texture.width() != width || next.texture.height() != height {
|
||||
ctx.unregister_texture(next.handle);
|
||||
self.next_texture = None;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no "next texture" then create one and register it.
|
||||
let texture_and_handle = match &self.next_texture {
|
||||
Some(next) => next,
|
||||
None => {
|
||||
let texture = create_texture(&self.device, width, height);
|
||||
let handle = ctx.register_texture(texture.clone());
|
||||
self.next_texture = Some(TextureAndHandle { texture, handle });
|
||||
self.next_texture.as_ref().unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
let next_texture = &texture_and_handle.texture;
|
||||
let next_texture_handle = texture_and_handle.handle;
|
||||
|
||||
let elapsed: f32 = start_time.elapsed().as_millis() as f32 / 500.;
|
||||
let [light_red, light_green, light_blue] = light;
|
||||
let push_constants = PushConstants {
|
||||
light_color_and_time: [light_red, light_green, light_blue, elapsed],
|
||||
};
|
||||
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&CommandEncoderDescriptor { label: None });
|
||||
{
|
||||
let mut rpass = encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
label: None,
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &next_texture.create_view(&TextureViewDescriptor::default()),
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(wgpu::Color::GREEN),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
rpass.set_pipeline(&self.pipeline);
|
||||
rpass.set_push_constants(
|
||||
ShaderStages::FRAGMENT, // Stage (your constants are for fragment shader)
|
||||
0, // Offset in bytes (start at 0)
|
||||
bytemuck::bytes_of(&push_constants),
|
||||
);
|
||||
rpass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
self.queue.submit(Some(encoder.finish()));
|
||||
|
||||
std::mem::swap(&mut self.next_texture, &mut self.displayed_texture);
|
||||
Some(next_texture_handle)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct PushConstants {
|
||||
light_color_and_time: [f32; 4],
|
||||
}
|
||||
|
||||
fn create_texture(device: &Device, width: u32, height: u32) -> Texture {
|
||||
device.create_texture(&TextureDescriptor {
|
||||
label: None,
|
||||
size: Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::RENDER_ATTACHMENT
|
||||
| TextureUsages::TEXTURE_BINDING
|
||||
| TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
use color::{palette::css::WHITE, parse_color};
|
||||
use color::{OpaqueColor, Srgb};
|
||||
use demo_renderer::{DemoMessage, DemoPaintSource};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_native::use_wgpu;
|
||||
use std::any::Any;
|
||||
use wgpu::{Features, Limits};
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::window::WindowAttributes;
|
||||
|
||||
mod demo_renderer;
|
||||
|
||||
// CSS Styles
|
||||
static STYLES: Asset = asset!("/src/styles.css");
|
||||
|
||||
// WGPU settings required by this example
|
||||
const FEATURES: Features = Features::PUSH_CONSTANTS;
|
||||
fn limits() -> Limits {
|
||||
Limits {
|
||||
max_push_constant_size: 16,
|
||||
..Limits::default()
|
||||
}
|
||||
}
|
||||
fn window_attributes() -> WindowAttributes {
|
||||
// You can also use a `<title>` element to set the window title
|
||||
// but this demonstrates the use of `WindowAttributes`
|
||||
WindowAttributes::default()
|
||||
.with_title("WGPU Example")
|
||||
.with_inner_size(LogicalSize::new(800, 600))
|
||||
}
|
||||
|
||||
type Color = OpaqueColor<Srgb>;
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let config: Vec<Box<dyn Any>> = vec![
|
||||
Box::new(FEATURES),
|
||||
Box::new(limits()),
|
||||
Box::new(window_attributes()),
|
||||
];
|
||||
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 WGPU content can be rendered beneath layers of HTML content" }
|
||||
}
|
||||
div { id:"underlay",
|
||||
h2 { "Underlay" },
|
||||
p { "This underlay demonstrates that the custom WGPU content can be rendered above layers and blended with the content underneath" }
|
||||
}
|
||||
header {
|
||||
h1 { "Blitz WGPU 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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) frag_position: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@builtin(vertex_index) vertex_index: u32
|
||||
) -> VertexOutput {
|
||||
var output: VertexOutput;
|
||||
|
||||
var positions = array<vec2<f32>, 3>(
|
||||
vec2<f32>(-1.0, 3.0),
|
||||
vec2<f32>(-1.0, -1.0),
|
||||
vec2<f32>( 3.0, -1.0)
|
||||
);
|
||||
|
||||
let pos = positions[vertex_index];
|
||||
output.position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
||||
output.frag_position = pos;
|
||||
return output;
|
||||
}
|
||||
|
||||
struct PushConstants {
|
||||
light_color_and_time: vec4<f32>,
|
||||
};
|
||||
|
||||
var<push_constant> pc: PushConstants;
|
||||
|
||||
fn sdRoundBox(p: vec3<f32>, b: vec3<f32>, r: f32) -> f32 {
|
||||
let q = abs(p) - b;
|
||||
return length(max(q, vec3<f32>(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0) - r;
|
||||
}
|
||||
|
||||
fn rotateY(r: vec3<f32>, angle: f32) -> vec3<f32> {
|
||||
let c = cos(angle);
|
||||
let s = sin(angle);
|
||||
let rotation_matrix = mat3x3<f32>(
|
||||
vec3<f32>( c, 0.0, s),
|
||||
vec3<f32>(0.0, 1.0, 0.0),
|
||||
vec3<f32>(-s, 0.0, c)
|
||||
);
|
||||
return rotation_matrix * r;
|
||||
}
|
||||
|
||||
fn rotateZ(r: vec3<f32>, angle: f32) -> vec3<f32> {
|
||||
let c = cos(angle);
|
||||
let s = sin(angle);
|
||||
let rotation_matrix = mat3x3<f32>(
|
||||
vec3<f32>( c, -s, 0.0),
|
||||
vec3<f32>( s, c, 0.0),
|
||||
vec3<f32>(0.0, 0.0, 1.0)
|
||||
);
|
||||
return rotation_matrix * r;
|
||||
}
|
||||
|
||||
// Distance from the scene
|
||||
fn scene(r: vec3<f32>) -> f32 {
|
||||
let iTime = pc.light_color_and_time.w;
|
||||
let pos = rotateZ(rotateY(r + vec3<f32>(-1.0, -1.0, 4.0), iTime), iTime);
|
||||
let cube = vec3<f32>(0.5, 0.5, 0.5);
|
||||
let edge = 0.1;
|
||||
return sdRoundBox(pos, cube, edge);
|
||||
}
|
||||
|
||||
// https://iquilezles.org/articles/normalsSDF
|
||||
fn normal(pos: vec3<f32>) -> vec3<f32> {
|
||||
let e = vec2<f32>(1.0, -1.0) * 0.5773;
|
||||
let eps = 0.0005;
|
||||
return normalize(
|
||||
e.xyy * scene(pos + e.xyy * eps) +
|
||||
e.yyx * scene(pos + e.yyx * eps) +
|
||||
e.yxy * scene(pos + e.yxy * eps) +
|
||||
e.xxx * scene(pos + e.xxx * eps)
|
||||
);
|
||||
}
|
||||
|
||||
fn render(fragCoord: vec2<f32>, light_color: vec3<f32>) -> vec4<f32> {
|
||||
var color = vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
var camera = vec3<f32>(1.0, 2.0, 1.0);
|
||||
var p = vec3<f32>(fragCoord.x, fragCoord.y + 1.0, -1.0);
|
||||
var dir = normalize(p - camera);
|
||||
|
||||
var i = 0;
|
||||
loop {
|
||||
if (i >= 90) { break; }
|
||||
let dist = scene(p);
|
||||
if (dist < 0.0001) { break; }
|
||||
p = p + dir * dist;
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
let surf_normal = normal(p);
|
||||
let light_position = vec3<f32>(2.0, 4.0, -0.5);
|
||||
var light = 7.0 + 2.0 * dot(surf_normal, light_position);
|
||||
light = light / (0.2 * pow(length(light_position - p), 3.5));
|
||||
|
||||
let alpha = select(0.0, 1.0, i < 90);
|
||||
return vec4<f32>(light * light_color, alpha) * 2.0;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(@location(0) frag_position: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
let selected_light_color = pc.light_color_and_time.xyz;
|
||||
let r = vec2<f32>(0.5 * frag_position.x + 1.0, 0.5 - 0.5 * frag_position.y);
|
||||
return render(r, selected_light_color);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
* {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user