slint/examples/bevy/main.rs
Nigel Breslaw 182a4dc58a
Tweak Bevy demo to have a transparent background (#8481)
This shows that Bevy can be composed into a Slint app beyond just taking over a rectangular area. Slint can also be used underneath Bevy rendered objects.
2025-05-20 11:31:16 +03:00

284 lines
9.7 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
use bevy::prelude::*;
use slint::{Model, SharedString};
mod slint_bevy_adapter;
mod web_asset;
slint::slint! {
import { Palette, Button, ComboBox, GroupBox, GridBox, Slider, HorizontalBox, VerticalBox, ProgressIndicator } from "std-widgets.slint";
export component AppWindow inherits Window {
in property <image> texture <=> i.source;
out property <length> requested-texture-width: i.width;
out property <length> requested-texture-height: i.height;
in property <bool> show-loading-screen: false;
in property <string> download-url;
in property <percent> download-progress;
in property <[string]> available-models;
callback load-model(index: int);
title: @tr("Slint & Bevy");
preferred-width: 500px;
preferred-height: 600px;
VerticalBox {
alignment: start;
Rectangle {
background: Palette.alternate-background;
VerticalBox {
Text {
text: "This text is rendered using Slint. The animation below is rendered using Bevy code.";
wrap: word-wrap;
}
HorizontalBox {
Text {
text: "Select Model:";
vertical-alignment: center;
}
ComboBox {
model: root.available-models;
selected(current-value) => { root.load-model(self.current-index) }
}
}
}
}
Rectangle {
width: 100%;
height: 100%;
if !show-loading-screen: Text {
y: 80px;
width: 450px;
font-size: 14px;
text: "This text is also rendered using Slint. It can be seen because Bevy is rendering with a transparent background.";
wrap: word-wrap;
}
i := Image {
image-fit: fill;
width: 100%;
height: 100%;
preferred-width: self.source.width * 1px;
preferred-height: self.source.height * 1px;
if show-loading-screen: Rectangle {
VerticalBox {
alignment: start;
Text {
horizontal-alignment: center;
text: "Downloading Assets";
}
Text {
text: download-url;
overflow: elide;
}
ProgressIndicator {
indeterminate: download-url.is-empty;
progress: root.download-progress;
}
}
}
}
}
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let (model_selector_sender, model_selector_receiver) = smol::channel::bounded::<GLTFModel>(1);
let (download_progress_sender, download_progress_receiver) =
smol::channel::bounded::<(SharedString, f32)>(5);
let (new_texture_receiver, control_message_sender) =
spin_on::spin_on(slint_bevy_adapter::run_bevy_app_with_slint(
|app| {
app.add_plugins(web_asset::WebAssetReaderPlugin(download_progress_sender));
},
|mut app| {
app.insert_resource(CameraPos(Vec3::new(3., 4.0, 4.0)))
// .insert_resource(ModelBasePath("".into()))
.insert_resource(ModelBasePath(
"https://github.com/KhronosGroup/glTF-Sample-Assets/raw/refs/heads/main/"
.into(),
))
.add_systems(Startup, setup)
.add_systems(Update, reload_model_from_channel(model_selector_receiver))
.add_systems(Update, animate_camera)
.insert_resource(ClearColor(Color::NONE))
.run();
},
))?;
let app_window = AppWindow::new().unwrap();
let app2 = app_window.as_weak();
app_window.window().set_rendering_notifier(move |state, _| {
let slint::RenderingState::BeforeRendering = state else { return };
let Some(app) = app2.upgrade() else { return };
app.window().request_redraw();
let Ok(new_texture) = new_texture_receiver.try_recv() else { return };
if let Some(old_texture) = app.get_texture().to_wgpu_24_texture() {
let control_message_sender = control_message_sender.clone();
slint::spawn_local(async move {
control_message_sender
.send(slint_bevy_adapter::ControlMessage::ReleaseFrontBufferTexture {
texture: old_texture,
})
.await
.unwrap();
})
.unwrap();
}
let requested_width = app.get_requested_texture_width().round() as u32;
let requested_height = app.get_requested_texture_height().round() as u32;
if requested_width > 0 && requested_height > 0 {
let control_message_sender = control_message_sender.clone();
slint::spawn_local(async move {
control_message_sender
.send(slint_bevy_adapter::ControlMessage::ResizeBuffers {
width: requested_width,
height: requested_height,
})
.await
.unwrap();
})
.unwrap();
}
if let Ok(image) = new_texture.try_into() {
app.set_texture(image);
}
})?;
let app_weak = app_window.as_weak();
slint::spawn_local(async move {
loop {
let Ok((url, progress)) = download_progress_receiver.recv().await else {
break;
};
let Some(app) = app_weak.upgrade() else { return };
app.set_download_url(url);
app.set_download_progress(progress * 100.);
app.set_show_loading_screen(progress < 1.0);
}
})
.unwrap();
let models = slint::VecModel::from_slice(&[
GLTFModel {
name: "Damaged Helmet".into(),
path: "Models/DamagedHelmet/glTF-Binary/DamagedHelmet.glb".into(),
center: Vec3::new(3.0, 4.0, 4.0),
},
GLTFModel {
name: "Fish".into(),
path: "Models/BarramundiFish/glTF-Binary/BarramundiFish.glb".into(),
center: Vec3::new(3.0, 2.0, 1.0),
},
GLTFModel {
name: "Box".into(),
path: "Models/Box/glTF-Binary/Box.glb".into(),
center: Vec3::new(3.0, 4.0, 4.0),
},
]);
app_window
.set_available_models(slint::ModelRc::new(models.clone().map(|model| model.name.clone())));
model_selector_sender.send_blocking(models.row_data(0).unwrap()).unwrap();
app_window.on_load_model(move |index| {
let model = models.row_data(index as usize).unwrap();
let model_selector_sender = model_selector_sender.clone();
slint::spawn_local(async move {
model_selector_sender.send(model).await.ok();
})
.unwrap();
});
app_window.run()?;
Ok(())
}
#[derive(Clone)]
struct GLTFModel {
name: SharedString,
path: SharedString,
center: Vec3,
}
#[derive(Resource)]
struct CameraPos(Vec3);
#[derive(Resource)]
struct ModelBasePath(String);
fn setup(mut commands: Commands, camera: Res<CameraPos>) {
commands.spawn(DirectionalLight { illuminance: 100_000.0, ..default() });
commands.spawn((
Camera3d::default(),
Transform::from_translation(camera.0).looking_at(Vec3::new(0.0, -0.5, 0.0), Vec3::Y),
PointLight { color: Color::linear_rgb(0.5, 0., 0.), ..default() },
));
/*
commands.spawn(SceneRoot(
//asset_server.load(GltfAssetLabel::Scene(0).from_asset("DamagedHelmet.glb")),
asset_server.load(
GltfAssetLabel::Scene(0)
.from_asset("Models/DamagedHelmet/glTF-Binary/DamagedHelmet.glb"), // GltfAssetLabel::Scene(0)
// .from_asset("https://github.com/KhronosGroup/glTF-Sample-Assets/raw/refs/heads/main/Models/DamagedHelmet/glTF-Binary/DamagedHelmet.glb"),
),
));
*/
}
fn reload_model_from_channel(
receiver: smol::channel::Receiver<GLTFModel>,
) -> impl FnMut(
Commands,
Res<AssetServer>,
Query<Entity, With<SceneRoot>>,
ResMut<CameraPos>,
Res<ModelBasePath>,
) {
move |mut commands, asset_server, loaded_bundles, mut camera, base_path| {
let Ok(new_model) = receiver.try_recv() else {
return;
};
for loaded_bundle in loaded_bundles.iter() {
commands.entity(loaded_bundle).despawn();
}
commands.spawn(SceneRoot(
//asset_server.load(GltfAssetLabel::Scene(0).from_asset("DamagedHelmet.glb")),
asset_server.load(
GltfAssetLabel::Scene(0).from_asset(format!("{}{}", base_path.0, new_model.path)),
),
));
camera.0 = new_model.center;
}
}
fn animate_camera(
mut cameras: Query<&mut Transform, With<Camera3d>>,
time: Res<Time>,
camera: Res<CameraPos>,
) {
let now = time.elapsed_secs();
for mut transform in cameras.iter_mut() {
// transform.translation = vec3(ops::cos(now), 0.0, ops::sin(now)) * vec3(3.0, 4.0, 4.0);
transform.translation = vec3(ops::cos(now), 0.0, ops::sin(now)) * camera.0;
transform.look_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y);
}
}