diff --git a/Cargo.toml b/Cargo.toml index fce483f3f..8e86b0c3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ bevy = { version = "0.12", default-features = false, features = [ ] } egui = { version = "0.24.0", default-features = false, features = ["bytemuck"] } webbrowser = { version = "0.8.2", optional = true } +wgpu-types = "0.17" [target.'cfg(not(any(target_arch = "wasm32", target_os = "android")))'.dependencies] arboard = { version = "3.2.0", optional = true } diff --git a/examples/worldspace.rs b/examples/worldspace.rs new file mode 100644 index 000000000..e4a8e0726 --- /dev/null +++ b/examples/worldspace.rs @@ -0,0 +1,74 @@ +use bevy::{ + prelude::*, + render::render_resource::{Extent3d, TextureUsages}, +}; +use bevy_egui::{egui, EguiContexts, EguiPlugin, EguiRenderToTexture}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(EguiPlugin) + .insert_resource(AmbientLight { + color: Color::WHITE, + brightness: 1., + }) + .add_systems(Startup, setup_worldspace) + // Systems that create Egui widgets should be run during the `CoreSet::Update` set, + // or after the `EguiSet::BeginFrame` system (which belongs to the `CoreSet::PreUpdate` set). + .add_systems(Update, (update_screenspace, update_worldspace)) + .run(); +} + +fn setup_worldspace( + mut images: ResMut>, + mut meshes: ResMut>, + mut materials: ResMut>, + mut commands: Commands, +) { + let output_texture = images.add({ + let size = Extent3d { + width: 256, + height: 256, + depth_or_array_layers: 1, + }; + let mut output_texture = Image { + // You should use `0` so that the pixels are transparent. + data: vec![0; (size.width * size.height * 4) as usize], + ..default() + }; + output_texture.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT; + output_texture.texture_descriptor.size = size; + output_texture + }); + + commands.spawn(PbrBundle { + mesh: meshes.add(shape::Cube::default().into()), + material: materials.add(StandardMaterial { + base_color: Color::WHITE, + base_color_texture: Some(Handle::clone(&output_texture)), + // Remove this if you want it to use the world's lighting. + unlit: true, + ..default() + }), + ..default() + }); + commands.spawn(EguiRenderToTexture(output_texture)); + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(1.5, 1.5, 1.5).looking_at(Vec3::new(0., 0., 0.), Vec3::Y), + ..default() + }); +} + +fn update_screenspace(mut contexts: EguiContexts) { + egui::Window::new("Screenspace UI").show(contexts.ctx_mut(), |ui| { + ui.label("I'm rendering to screenspace!"); + }); +} + +fn update_worldspace(mut contexts: Query<&mut bevy_egui::EguiContext, With>) { + for mut ctx in contexts.iter_mut() { + egui::Window::new("Worldspace UI").show(ctx.get_mut(), |ui| { + ui.label("I'm rendering to a texture in worldspace!"); + }); + } +} diff --git a/src/egui_node.rs b/src/egui_node.rs index ff6f76550..ee10bcfbb 100644 --- a/src/egui_node.rs +++ b/src/egui_node.rs @@ -2,13 +2,14 @@ use crate::{ render_systems::{ EguiPipelines, EguiTextureBindGroups, EguiTextureId, EguiTransform, EguiTransforms, }, - EguiRenderOutput, EguiSettings, WindowSize, + EguiRenderOutput, EguiRenderToTexture, EguiSettings, WindowSize, }; use bevy::{ core::cast_slice, ecs::world::{FromWorld, World}, prelude::{Entity, Handle, Resource}, render::{ + render_asset::RenderAssets, render_graph::{Node, NodeRunError, RenderGraphContext}, render_resource::{ BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, @@ -306,19 +307,29 @@ impl Node for EguiNode { let pipeline_cache = world.get_resource::().unwrap(); let extracted_windows = &world.get_resource::().unwrap().windows; - let extracted_window = - if let Some(extracted_window) = extracted_windows.get(&self.window_entity) { - extracted_window - } else { - return Ok(()); // No window - }; - - let swap_chain_texture_view = if let Some(swap_chain_texture_view) = - extracted_window.swap_chain_texture_view.as_ref() - { - swap_chain_texture_view - } else { - return Ok(()); // No swapchain texture + let extracted_window = extracted_windows.get(&self.window_entity); + let extracted_render_to_texture: Option<&EguiRenderToTexture> = + world.get(self.window_entity); + let gpu_image = extracted_render_to_texture.map(|r| { + let gpu_images = world.get_resource::>().unwrap(); + gpu_images.get(&r.0).unwrap() + }); + + let swap_chain_texture_view = match (extracted_window, gpu_image) { + (Some(w), None) => { + if let Some(tv) = w.swap_chain_texture_view.as_ref() { + tv + } else { + // Nothing to do + return Ok(()); + } + } + (None, Some(g)) => { + &g.texture_view + } + // Nothing to do + (None, None) => return Ok(()), + (Some(_), Some(_)) => panic!("RenderWorld should not contain entities with both Window and EguiRenderToTexture components. This restriction might be relaxed in the future"), }; let render_queue = world.get_resource::().unwrap(); @@ -351,9 +362,7 @@ impl Node for EguiNode { depth_stencil_attachment: None, }); - let Some(pipeline_id) = egui_pipelines.get(&extracted_window.entity) else { - return Ok(()); - }; + let pipeline_id = egui_pipelines.get(&self.window_entity).unwrap(); let Some(pipeline) = pipeline_cache.get_render_pipeline(*pipeline_id) else { return Ok(()); }; @@ -369,10 +378,18 @@ impl Node for EguiNode { let transform_buffer_bind_group = &egui_transforms.bind_group.as_ref().unwrap().1; render_pass.set_bind_group(0, transform_buffer_bind_group, &[transform_buffer_offset]); + let (physical_width, physical_height) = match (extracted_window, gpu_image) { + (Some(w), None) => (w.physical_width, w.physical_height), + (None, Some(g)) => (g.size.x as u32, g.size.y as u32), + (None, None) | (Some(_), Some(_)) => { + unreachable!("At this point, we already checked that this should be impossible") + } + }; + let mut vertex_offset: u32 = 0; for draw_command in &self.draw_commands { - if draw_command.clipping_zone.0 < extracted_window.physical_width - && draw_command.clipping_zone.1 < extracted_window.physical_height + if draw_command.clipping_zone.0 < physical_width + && draw_command.clipping_zone.1 < physical_height { let texture_bind_group = match bind_groups.get(&draw_command.egui_texture) { Some(texture_resource) => texture_resource, @@ -387,16 +404,14 @@ impl Node for EguiNode { render_pass.set_scissor_rect( draw_command.clipping_zone.0, draw_command.clipping_zone.1, - draw_command.clipping_zone.2.min( - extracted_window - .physical_width - .saturating_sub(draw_command.clipping_zone.0), - ), - draw_command.clipping_zone.3.min( - extracted_window - .physical_height - .saturating_sub(draw_command.clipping_zone.1), - ), + draw_command + .clipping_zone + .2 + .min(physical_width.saturating_sub(draw_command.clipping_zone.0)), + draw_command + .clipping_zone + .3 + .min(physical_height.saturating_sub(draw_command.clipping_zone.1)), ); render_pass.draw_indexed( diff --git a/src/lib.rs b/src/lib.rs index 4a7a3948e..5297bf8d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #![warn(missing_docs)] +#![allow(clippy::type_complexity)] //! This crate provides an [Egui](https://github.com/emilk/egui) integration for the [Bevy](https://github.com/bevyengine/bevy) game engine. //! @@ -81,6 +82,7 @@ use bevy::{ asset::{load_internal_asset, AssetEvent, Assets, Handle}, ecs::{ event::EventReader, + query::Or, system::{Res, ResMut}, }, prelude::Shader, @@ -102,8 +104,8 @@ use bevy::{ }, input::InputSystem, prelude::{ - Added, Commands, Component, Deref, DerefMut, Entity, IntoSystemConfigs, Query, Resource, - SystemSet, With, Without, + Added, Bundle, Commands, Component, Deref, DerefMut, Entity, IntoSystemConfigs, Query, + Resource, SystemSet, With, Without, }, reflect::Reflect, window::{PrimaryWindow, Window}, @@ -210,6 +212,11 @@ impl EguiSettings { } } +/// Contains the texture [`Image`] to render to. +#[cfg(feature = "render")] +#[derive(Component, Clone, Debug, ExtractComponent)] +pub struct EguiRenderToTexture(pub Handle); + /// Is used for storing Egui context input.. /// /// It gets reset during the [`EguiSet::ProcessInput`] system. @@ -309,7 +316,7 @@ pub struct EguiOutput { } /// A component for storing `bevy_egui` context. -#[derive(Clone, Component, Default)] +#[derive(Clone, Component, Debug, Default)] #[cfg_attr(feature = "render", derive(ExtractComponent))] pub struct EguiContext(egui::Context); @@ -344,6 +351,12 @@ impl EguiContext { } } +#[cfg(not(feature = "render"))] +type EguiContextsFilter = With; + +#[cfg(feature = "render")] +type EguiContextsFilter = Or<(With, With)>; + #[derive(SystemParam)] /// A helper SystemParam that provides a way to get `[EguiContext]` with less boilerplate and /// combines a proxy interface to the [`EguiUserTextures`] resource. @@ -356,7 +369,7 @@ pub struct EguiContexts<'w, 's> { &'static mut EguiContext, Option<&'static PrimaryWindow>, ), - With, + EguiContextsFilter, >, #[cfg(feature = "render")] user_textures: ResMut<'w, EguiUserTextures>, @@ -366,7 +379,7 @@ impl<'w, 's> EguiContexts<'w, 's> { /// Egui context of the primary window. #[must_use] pub fn ctx_mut(&mut self) -> &mut egui::Context { - let (_window, ctx, _primary_window) = self + let (_entity, ctx, _primary_window) = self .q .iter_mut() .find(|(_window_entity, _ctx, primary_window)| primary_window.is_some()) @@ -374,42 +387,40 @@ impl<'w, 's> EguiContexts<'w, 's> { ctx.into_inner().get_mut() } - /// Egui context for a specific window. + /// Egui context for a specific entity. #[must_use] - pub fn ctx_for_window_mut(&mut self, window: Entity) -> &mut egui::Context { + pub fn ctx_for_entity_mut(&mut self, entity: Entity) -> &mut egui::Context { let (_window, ctx, _primary_window) = self .q .iter_mut() - .find(|(window_entity, _ctx, _primary_window)| *window_entity == window) - .unwrap_or_else(|| panic!("`EguiContexts::ctx_for_window_mut` was called for an uninitialized context (window {window:?}), make sure your system is run after [`EguiSet::InitContexts`] (or [`EguiStartupSet::InitContexts`] for startup systems)")); + .find(|(window_entity, _ctx, _primary_window)| *window_entity == entity) + .unwrap_or_else(|| panic!("`EguiContexts::ctx_for_window_mut` was called for an uninitialized context (entity {entity:?}), make sure your system is run after [`EguiSet::InitContexts`] (or [`EguiStartupSet::InitContexts`] for startup systems)")); ctx.into_inner().get_mut() } - /// Fallible variant of [`EguiContexts::ctx_for_window_mut`]. + /// Fallible variant of [`EguiContexts::ctx_for_entity_mut`]. #[must_use] #[track_caller] - pub fn try_ctx_for_window_mut(&mut self, window: Entity) -> Option<&mut egui::Context> { - self.q - .iter_mut() - .find_map(|(window_entity, ctx, _primary_window)| { - if window_entity == window { - Some(ctx.into_inner().get_mut()) - } else { - None - } - }) + pub fn try_ctx_for_entity_mut(&mut self, entity: Entity) -> Option<&mut egui::Context> { + self.q.iter_mut().find_map(|(e, ctx, _primary_window)| { + if e == entity { + Some(ctx.into_inner().get_mut()) + } else { + None + } + }) } /// Allows to get multiple contexts at the same time. This function is useful when you want - /// to get multiple window contexts without using the `immutable_ctx` feature. + /// to get multiple contexts without using the `immutable_ctx` feature. #[track_caller] - pub fn ctx_for_windows_mut( + pub fn ctx_for_entities_mut( &mut self, ids: [Entity; N], ) -> Result<[&mut egui::Context; N], QueryEntityError> { self.q .get_many_mut(ids) - .map(|arr| arr.map(|(_window_entity, ctx, _primary_window)| ctx.into_inner().get_mut())) + .map(|arr| arr.map(|(_entity, ctx, _primary_window)| ctx.into_inner().get_mut())) } /// Egui context of the primary window. @@ -424,15 +435,15 @@ impl<'w, 's> EguiContexts<'w, 's> { #[cfg(feature = "immutable_ctx")] #[must_use] pub fn ctx(&self) -> &egui::Context { - let (_window, ctx, _primary_window) = self + let (_entity, ctx, _primary_window) = self .q .iter() - .find(|(_window_entity, _ctx, primary_window)| primary_window.is_some()) + .find(|(_entity, _ctx, primary_window)| primary_window.is_some()) .expect("`EguiContexts::ctx` was called for an uninitialized context (primary window), make sure your system is run after [`EguiSet::InitContexts`] (or [`EguiStartupSet::InitContexts`] for startup systems)"); ctx.get() } - /// Egui context for a specific window. + /// Egui context for a specific entity. /// /// Even though the mutable borrow isn't necessary, as the context is wrapped into `RwLock`, /// using the immutable getter is gated with the `immutable_ctx` feature. Using the immutable @@ -443,16 +454,16 @@ impl<'w, 's> EguiContexts<'w, 's> { /// instead of busy-waiting. #[must_use] #[cfg(feature = "immutable_ctx")] - pub fn ctx_for_window(&self, window: Entity) -> &egui::Context { - let (_window, ctx, _primary_window) = self + pub fn ctx_for_entity(&self, entity: Entity) -> &egui::Context { + let (_entity, ctx, _primary_window) = self .q .iter() - .find(|(window_entity, _ctx, _primary_window)| *window_entity == window) - .unwrap_or_else(|| panic!("`EguiContexts::ctx_for_window` was called for an uninitialized context (window {window:?}), make sure your system is run after [`EguiSet::InitContexts`] (or [`EguiStartupSet::InitContexts`] for startup systems)")); + .find(|(e, _ctx, _primary_window)| *e == entity) + .unwrap_or_else(|| panic!("`EguiContexts::ctx_for_entity` was called for an uninitialized context (entity {entity:?}), make sure your system is run after [`EguiSet::InitContexts`] (or [`EguiStartupSet::InitContexts`] for startup systems)")); ctx.get() } - /// Fallible variant of [`EguiContexts::ctx_for_window_mut`]. + /// Fallible variant of [`EguiContexts::ctx_for_entity_mut`]. /// /// Even though the mutable borrow isn't necessary, as the context is wrapped into `RwLock`, /// using the immutable getter is gated with the `immutable_ctx` feature. Using the immutable @@ -464,16 +475,16 @@ impl<'w, 's> EguiContexts<'w, 's> { #[must_use] #[track_caller] #[cfg(feature = "immutable_ctx")] - pub fn try_ctx_for_window(&self, window: Entity) -> Option<&egui::Context> { - self.q - .iter() - .find_map(|(window_entity, ctx, _primary_window)| { - if window_entity == window { + pub fn try_ctx_for_entity(&self, entity: Entity) -> Option<&egui::Context> { + self.q.iter().find_map( + |(e, ctx, _primary_window)| { + if e == entity { Some(ctx.get()) } else { None } - }) + }, + ) } /// Can accept either a strong or a weak handle. @@ -633,6 +644,8 @@ impl Plugin for EguiPlugin { #[cfg(feature = "render")] app.add_plugins(ExtractResourcePlugin::::default()); #[cfg(feature = "render")] + app.add_plugins(ExtractComponentPlugin::::default()); + #[cfg(feature = "render")] app.add_plugins(ExtractComponentPlugin::::default()); #[cfg(feature = "render")] app.add_plugins(ExtractComponentPlugin::::default()); @@ -642,9 +655,13 @@ impl Plugin for EguiPlugin { app.add_systems( PreStartup, ( - setup_new_windows_system, + ( + #[cfg(feature = "render")] + setup_new_render_to_texture_system, + setup_new_windows_system, + ), apply_deferred, - update_window_contexts_system, + update_contexts_system, ) .chain() .in_set(EguiStartupSet::InitContexts), @@ -652,9 +669,13 @@ impl Plugin for EguiPlugin { app.add_systems( PreUpdate, ( - setup_new_windows_system, + ( + #[cfg(feature = "render")] + setup_new_render_to_texture_system, + setup_new_windows_system, + ), apply_deferred, - update_window_contexts_system, + update_contexts_system, ) .chain() .in_set(EguiSet::InitContexts), @@ -730,10 +751,11 @@ impl Plugin for EguiPlugin { /// Queries all the Egui related components. #[derive(WorldQuery)] #[world_query(mutable)] +#[non_exhaustive] pub struct EguiContextQuery { - /// Window entity. - pub window_entity: Entity, - /// Egui context associated with the window. + /// Context entity. + pub entity: Entity, + /// Egui context pub ctx: &'static mut EguiContext, /// Encapsulates [`egui::RawInput`]. pub egui_input: &'static mut EguiInput, @@ -743,8 +765,11 @@ pub struct EguiContextQuery { pub egui_output: &'static mut EguiOutput, /// Stores physical size of the window and its scale factor. pub window_size: &'static mut WindowSize, - /// [`Window`] component. - pub window: &'static mut Window, + /// [`Window`] component, if egui is rendering to the window. + pub window: Option<&'static mut Window>, + /// [`EguiRenderToTexture`] component, if egui is rendering to a texture. + #[cfg(feature = "render")] + pub render_to_tex: Option<&'static mut EguiRenderToTexture>, } /// Contains textures allocated and painted by Egui. @@ -761,6 +786,29 @@ pub struct EguiManagedTexture { pub color_image: egui::ColorImage, } +/// All required components of bevy_egui +#[derive(Bundle, Default)] +pub struct EguiRequiredComponentsBundle { + ctx: EguiContext, + input: EguiInput, + output: EguiOutput, + render_output: EguiRenderOutput, + window_size: WindowSize, +} + +/// Adds bevy_egui components to newly created [`EguiRenderToTexture`]s. +#[cfg(feature = "render")] +pub fn setup_new_render_to_texture_system( + mut commands: Commands, + new_contexts: Query, Without)>, +) { + for entity in new_contexts.iter() { + commands + .entity(entity) + .insert(EguiRequiredComponentsBundle::default()); + } +} + /// Adds bevy_egui components to newly created windows. pub fn setup_new_windows_system( mut commands: Commands, @@ -768,12 +816,8 @@ pub fn setup_new_windows_system( ) { for window in new_windows.iter() { commands.entity(window).insert(( - EguiContext::default(), EguiMousePosition::default(), - EguiRenderOutput::default(), - EguiInput::default(), - EguiOutput::default(), - WindowSize::default(), + EguiRequiredComponentsBundle::default(), )); } } @@ -781,12 +825,15 @@ pub fn setup_new_windows_system( /// Updates textures painted by Egui. #[cfg(feature = "render")] pub fn update_egui_textures_system( - mut egui_render_output: Query<(Entity, &mut EguiRenderOutput), With>, + mut egui_render_output: Query< + (Entity, &mut EguiRenderOutput), + Or<(With, With)>, + >, mut egui_managed_textures: ResMut, mut image_assets: ResMut>, egui_settings: Res, ) { - for (window_id, mut egui_render_output) in egui_render_output.iter_mut() { + for (entity, mut egui_render_output) in egui_render_output.iter_mut() { let set_textures = std::mem::take(&mut egui_render_output.textures_delta.set); for (texture_id, image_delta) in set_textures { @@ -799,8 +846,7 @@ pub fn update_egui_textures_system( if let Some(pos) = image_delta.pos { // Partial update. - if let Some(managed_texture) = - egui_managed_textures.get_mut(&(window_id, texture_id)) + if let Some(managed_texture) = egui_managed_textures.get_mut(&(entity, texture_id)) { // TODO: when bevy supports it, only update the part of the texture that changes. update_image_rect(&mut managed_texture.color_image, pos, &color_image); @@ -820,7 +866,7 @@ pub fn update_egui_textures_system( ); let handle = image_assets.add(image); egui_managed_textures.insert( - (window_id, texture_id), + (entity, texture_id), EguiManagedTexture { handle, color_image, @@ -842,16 +888,19 @@ pub fn update_egui_textures_system( #[cfg(feature = "render")] fn free_egui_textures_system( mut egui_user_textures: ResMut, - mut egui_render_output: Query<(Entity, &mut EguiRenderOutput), With>, + mut egui_render_output: Query< + (Entity, &mut EguiRenderOutput), + Or<(With, With)>, + >, mut egui_managed_textures: ResMut, mut image_assets: ResMut>, mut image_events: EventReader>, ) { - for (window_id, mut egui_render_output) in egui_render_output.iter_mut() { + for (entity, mut egui_render_output) in egui_render_output.iter_mut() { let free_textures = std::mem::take(&mut egui_render_output.textures_delta.free); for texture_id in free_textures { if let egui::TextureId::Managed(texture_id) = texture_id { - let managed_texture = egui_managed_textures.remove(&(window_id, texture_id)); + let managed_texture = egui_managed_textures.remove(&(entity, texture_id)); if let Some(managed_texture) = managed_texture { image_assets.remove(managed_texture.handle); } @@ -868,8 +917,8 @@ fn free_egui_textures_system( /// Egui's render graph config. pub struct RenderGraphConfig { - /// Target window. - pub window: Entity, + /// Target entity. + pub entity: Entity, /// Render pass name. pub egui_pass: Cow<'static, str>, } diff --git a/src/render_systems.rs b/src/render_systems.rs index 742d2efc0..b894cf751 100644 --- a/src/render_systems.rs +++ b/src/render_systems.rs @@ -1,6 +1,6 @@ use crate::{ egui_node::{EguiNode, EguiPipeline, EguiPipelineKey}, - EguiManagedTextures, EguiSettings, EguiUserTextures, WindowSize, + EguiManagedTextures, EguiRenderToTexture, EguiSettings, EguiUserTextures, WindowSize, }; use bevy::{ ecs::system::SystemParam, @@ -74,20 +74,14 @@ impl ExtractedEguiTextures<'_> { /// Sets up the pipeline for newly created windows. pub fn setup_new_windows_render_system( - windows: Extract>>, + windows: Extract, Added)>>>, mut render_graph: ResMut, ) { - for window in windows.iter() { - let egui_pass = format!("egui-{}-{}", window.index(), window.generation()); - - let new_node = EguiNode::new(window); - - render_graph.add_node(egui_pass.clone(), new_node); - - render_graph.add_node_edge( - bevy::render::main_graph::node::CAMERA_DRIVER, - egui_pass.to_string(), - ); + for entity in windows.iter() { + let node_name = format!("bevy_egui-{}-{}", entity.index(), entity.generation()); + let new_node = EguiNode::new(entity); + let node_id = render_graph.add_node(node_name.clone(), new_node); + render_graph.add_node_edge(bevy::render::main_graph::node::CAMERA_DRIVER, node_id); } } @@ -210,25 +204,41 @@ pub fn queue_bind_groups_system( #[derive(Resource)] pub struct EguiPipelines(pub HashMap); -/// Queue [`EguiPipeline`]s specialized on each window's swap chain texture format. +/// Queue [`EguiPipeline`]s specialized on each render target's swap chain texture format. pub fn queue_pipelines_system( mut commands: Commands, pipeline_cache: Res, mut pipelines: ResMut>, egui_pipeline: Res, windows: Res, + render_to_tex: Query<(Entity, &EguiRenderToTexture)>, + imgs: Res>, ) { - let pipelines = windows - .iter() - .filter_map(|(window_id, window)| { - let key = EguiPipelineKey { - texture_format: window.swap_chain_texture_format?.add_srgb_suffix(), - }; - let pipeline_id = pipelines.specialize(&pipeline_cache, &egui_pipeline, key); - - Some((*window_id, pipeline_id)) - }) - .collect(); + // Add pipelines for `Window` + let mut egui_pipelines = EguiPipelines( + windows + .iter() + .filter_map(|(window_id, window)| { + let key = EguiPipelineKey { + texture_format: window.swap_chain_texture_format?.add_srgb_suffix(), + }; + let pipeline_id = pipelines.specialize(&pipeline_cache, &egui_pipeline, key); + + Some((*window_id, pipeline_id)) + }) + .collect(), + ); + + // Add pipelines for `EguiRenderToTexture` + for (entity, t) in render_to_tex.iter() { + let gpu_img = imgs.get(&t.0).unwrap(); + let key = EguiPipelineKey { + texture_format: gpu_img.texture_format, + }; + let pipeline_id = pipelines.specialize(&pipeline_cache, &egui_pipeline, key); + + egui_pipelines.0.insert(entity, pipeline_id); + } - commands.insert_resource(EguiPipelines(pipelines)); + commands.insert_resource(egui_pipelines); } diff --git a/src/systems.rs b/src/systems.rs index 7fa589bdc..1135bd1ef 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,8 +1,12 @@ +#[cfg(feature = "render")] +use crate::EguiRenderToTexture; use crate::{ EguiContext, EguiContextQuery, EguiInput, EguiMousePosition, EguiSettings, WindowSize, }; #[cfg(feature = "open_url")] use bevy::log; +#[cfg(feature = "render")] +use bevy::prelude::{Assets, Image, Or}; use bevy::{ ecs::{ event::EventWriter, @@ -14,11 +18,11 @@ use bevy::{ touch::TouchInput, ButtonState, Input, }, - prelude::{Entity, EventReader, Query, Resource, Time}, + prelude::{Entity, EventReader, Query, Resource, Time, With}, time::Real, window::{ - CursorEntered, CursorLeft, CursorMoved, ReceivedCharacter, RequestRedraw, WindowCreated, - WindowFocused, + CursorEntered, CursorLeft, CursorMoved, ReceivedCharacter, RequestRedraw, Window, + WindowCreated, WindowFocused, }, }; use std::marker::PhantomData; @@ -73,7 +77,10 @@ pub struct InputResources<'w, 's> { pub struct ContextSystemParams<'w, 's> { pub focused_window: Local<'s, Option>, pub pointer_touch_id: Local<'s, TouchId>, - pub contexts: Query<'w, 's, EguiContextQuery>, + #[cfg(feature = "render")] + pub contexts: Query<'w, 's, EguiContextQuery, Or<(With, With)>>, + #[cfg(not(feature = "render"))] + pub contexts: Query<'w, 's, EguiContextQuery, With>, #[system_param(ignore)] _marker: PhantomData<&'s ()>, } @@ -368,16 +375,31 @@ pub fn process_input_system( } /// Initialises Egui contexts (for multiple windows). -pub fn update_window_contexts_system( +pub fn update_contexts_system( mut context_params: ContextSystemParams, egui_settings: Res, + #[cfg(feature = "render")] images: Res>, ) { for mut context in context_params.contexts.iter_mut() { - let new_window_size = WindowSize::new( - context.window.physical_width() as f32, - context.window.physical_height() as f32, - context.window.scale_factor() as f32, - ); + let mut new_window_size = None; + if let Some(window) = context.window { + new_window_size = Some(WindowSize::new( + window.physical_width() as f32, + window.physical_height() as f32, + window.scale_factor() as f32, + )); + } + #[cfg(feature = "render")] + if let Some(EguiRenderToTexture(render_output)) = context.render_to_tex.as_deref() { + let render_output = images.get(render_output).expect( + "should have found an `Image` with the handle stored in `EguiRenderToTexture`", + ); + let (width, height) = render_output.size().into(); + new_window_size = Some(WindowSize::new(width as f32, height as f32, 1.)); + } + let Some(new_window_size) = new_window_size else { + unreachable!("All entities in `context_params` should have either a `Window` or a `EguiRenderToTexture` component") + }; let width = new_window_size.physical_width / new_window_size.scale_factor / egui_settings.scale_factor as f32; @@ -444,8 +466,10 @@ pub fn process_output_system( } let mut set_icon = || { - context.window.cursor.icon = egui_to_winit_cursor_icon(platform_output.cursor_icon) - .unwrap_or(bevy::window::CursorIcon::Default); + if let Some(window) = context.window.as_mut() { + window.cursor.icon = egui_to_winit_cursor_icon(platform_output.cursor_icon) + .unwrap_or(bevy::window::CursorIcon::Default); + } }; #[cfg(windows)]