// Copyright © SixtyFPS GmbH // 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 texture <=> i.source; out property requested-texture-width: i.width; out property requested-texture-height: i.height; in property show-loading-screen: false; in property download-url; in property 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> { let (model_selector_sender, model_selector_receiver) = smol::channel::bounded::(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) { 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, ) -> impl FnMut( Commands, Res, Query>, ResMut, Res, ) { 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>, time: Res