mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-27 22:04:08 +00:00

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.
284 lines
9.7 KiB
Rust
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);
|
|
}
|
|
}
|