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:
Jonathan Kelley authored and GitHub committed 2025-10-03 18:26:19 -07:00
1 parent 8421c3aef0
commit f2b1c6d97f
424 files changed
+17284 -6624

No files matched your search

+20
View File
@@ -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)
}
}
+110
View File
@@ -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 }))
}
+11
View File
@@ -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"] }
+27
View File
@@ -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"]
+21
View File
@@ -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.
+45
View File
@@ -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.
+18
View File
@@ -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"
}
+198
View File
@@ -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.');
})
);
});
+15
View File
@@ -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;
}
}