From c6895db45abe614bd849aa6ef6d6db5058556d83 Mon Sep 17 00:00:00 2001 From: Vladyslav Batyrenko Date: Sun, 18 Aug 2024 14:41:04 +0300 Subject: [PATCH] Initial world space UI support (#304) * implement worldspace ui Signed-off-by: Schmarni * clean render to texture logic from EguiNode Signed-off-by: Schmarni * fix warnings Signed-off-by: Schmarni * require render feature for rtt example Signed-off-by: Schmarni * Implement paint callbacks for rendering to a texture * Fix compilation of not(feature = render) flag --------- Signed-off-by: Schmarni Co-authored-by: Schmarni Co-authored-by: Schmarni <51007916+Schmarni-Dev@users.noreply.github.com> --- Cargo.toml | 4 + examples/paint_callback.rs | 70 ++++- examples/render_egui_to_texture.rs | 69 +++++ src/egui_node.rs | 90 +++--- src/egui_render_to_texture_node.rs | 445 +++++++++++++++++++++++++++++ src/lib.rs | 134 ++++++--- src/render_systems.rs | 67 +++-- src/systems.rs | 78 +++-- 8 files changed, 831 insertions(+), 126 deletions(-) create mode 100644 examples/render_egui_to_texture.rs create mode 100644 src/egui_render_to_texture_node.rs diff --git a/Cargo.toml b/Cargo.toml index e0acbc071..7d27c0df7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,9 @@ required-features = ["render"] [[example]] name = "ui" required-features = ["render"] +[[example]] +name = "render_egui_to_texture" +required-features = ["render"] [dependencies] bevy = { version = "0.14.0", default-features = false, features = [ @@ -48,6 +51,7 @@ bevy = { version = "0.14.0", default-features = false, features = [ egui = { version = "0.28", default-features = false, features = ["bytemuck"] } bytemuck = "1" webbrowser = { version = "1.0.1", optional = true } +wgpu-types = "0.20" [target.'cfg(not(any(target_arch = "wasm32", target_os = "android")))'.dependencies] arboard = { version = "3.2.0", optional = true } diff --git a/examples/paint_callback.rs b/examples/paint_callback.rs index 990495d72..8bb2b217a 100644 --- a/examples/paint_callback.rs +++ b/examples/paint_callback.rs @@ -13,14 +13,19 @@ use bevy::{ }; use bevy_egui::{ egui_node::{EguiBevyPaintCallback, EguiBevyPaintCallbackImpl, EguiPipelineKey}, - EguiContexts, EguiPlugin, + EguiContexts, EguiPlugin, EguiRenderToTextureHandle, }; use std::path::Path; +use wgpu_types::{Extent3d, TextureUsages}; fn main() { App::new() .add_plugins((DefaultPlugins, EguiPlugin, CustomPipelinePlugin)) - .add_systems(Update, ui_example_system) + .add_systems(Startup, setup_worldspace) + .add_systems( + Update, + (ui_example_system, ui_render_to_texture_example_system), + ) .run(); } @@ -170,3 +175,64 @@ fn ui_example_system(mut ctx: EguiContexts) { }); } } + +// The following systems are used to render UI in world space to demonstrate that paint callbacks +// work for them as well (they aren't needed to set up pain callbacks for regular screen-space UI, +// so feel free to skip them): + +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(Cuboid::new(1.0, 1.0, 1.0).mesh()), + material: materials.add(StandardMaterial { + base_color: Color::WHITE, + base_color_texture: Some(Handle::clone(&output_texture)), + alpha_mode: AlphaMode::Blend, + // Remove this if you want it to use the world's lighting. + unlit: true, + ..default() + }), + ..default() + }); + commands.spawn(EguiRenderToTextureHandle(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 ui_render_to_texture_example_system( + 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| { + let (resp, painter) = + ui.allocate_painter(egui::Vec2 { x: 200., y: 200. }, egui::Sense::hover()); + + painter.add(EguiBevyPaintCallback::new_paint_callback( + resp.rect, + CustomPaintCallback, + )); + }); + } +} diff --git a/examples/render_egui_to_texture.rs b/examples/render_egui_to_texture.rs new file mode 100644 index 000000000..953ee5844 --- /dev/null +++ b/examples/render_egui_to_texture.rs @@ -0,0 +1,69 @@ +use bevy::prelude::*; +use bevy_egui::{EguiContexts, EguiPlugin, EguiRenderToTextureHandle}; +use wgpu_types::{Extent3d, TextureUsages}; + +fn main() { + let mut app = App::new(); + app.add_plugins(DefaultPlugins); + app.add_plugins(EguiPlugin); + app.add_systems(Startup, setup_worldspace); + app.add_systems(Update, (update_screenspace, update_worldspace)); + app.run(); +} + +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!"); + }); + } +} + +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(Cuboid::new(1.0, 1.0, 1.0).mesh()), + material: materials.add(StandardMaterial { + base_color: Color::WHITE, + base_color_texture: Some(Handle::clone(&output_texture)), + alpha_mode: AlphaMode::Blend, + // Remove this if you want it to use the world's lighting. + unlit: true, + ..default() + }), + ..default() + }); + commands.spawn(EguiRenderToTextureHandle(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() + }); +} diff --git a/src/egui_node.rs b/src/egui_node.rs index 0669ece0e..0966e35e5 100644 --- a/src/egui_node.rs +++ b/src/egui_node.rs @@ -2,7 +2,7 @@ use crate::{ render_systems::{ EguiPipelines, EguiTextureBindGroups, EguiTextureId, EguiTransform, EguiTransforms, }, - EguiRenderOutput, EguiSettings, WindowSize, + EguiRenderOutput, EguiSettings, RenderTargetSize, }; use bevy::{ ecs::world::{FromWorld, World}, @@ -22,7 +22,10 @@ use bevy::{ VertexBufferLayout, VertexFormat, VertexState, VertexStepMode, }, renderer::{RenderContext, RenderDevice, RenderQueue}, - texture::{Image, ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor}, + texture::{ + GpuImage, Image, ImageAddressMode, ImageFilterMode, ImageSampler, + ImageSamplerDescriptor, + }, view::{ExtractedWindow, ExtractedWindows}, }, }; @@ -96,12 +99,19 @@ pub struct EguiPipelineKey { } impl EguiPipelineKey { - /// Extracts target texture format in egui renderpass + /// Constructs a pipeline key from a window. pub fn from_extracted_window(window: &ExtractedWindow) -> Option { Some(Self { texture_format: window.swap_chain_texture_format?.add_srgb_suffix(), }) } + + /// Constructs a pipeline key from a gpu image. + pub fn from_gpu_image(image: &GpuImage) -> Self { + EguiPipelineKey { + texture_format: image.texture_format.add_srgb_suffix(), + } + } } impl SpecializedRenderPipeline for EguiPipeline { @@ -160,25 +170,24 @@ impl SpecializedRenderPipeline for EguiPipeline { } } -struct DrawCommand { - clip_rect: egui::Rect, - primitive: DrawPrimitive, +pub(crate) struct DrawCommand { + pub(crate) clip_rect: egui::Rect, + pub(crate) primitive: DrawPrimitive, } -enum DrawPrimitive { +pub(crate) enum DrawPrimitive { Egui(EguiDraw), PaintCallback(PaintCallbackDraw), } -struct PaintCallbackDraw { - callback: std::sync::Arc, - rect: egui::Rect, +pub(crate) struct PaintCallbackDraw { + pub(crate) callback: std::sync::Arc, + pub(crate) rect: egui::Rect, } -#[derive(Debug)] -struct EguiDraw { - vertices_count: usize, - egui_texture: EguiTextureId, +pub(crate) struct EguiDraw { + pub(crate) vertices_count: usize, + pub(crate) egui_texture: EguiTextureId, } /// Egui render node. @@ -223,9 +232,10 @@ impl Node for EguiNode { return; }; - let mut window_sizes = world.query::<(&WindowSize, &mut EguiRenderOutput)>(); + let mut render_target_size = world.query::<(&RenderTargetSize, &mut EguiRenderOutput)>(); - let Ok((window_size, mut render_output)) = window_sizes.get_mut(world, self.window_entity) + let Ok((window_size, mut render_output)) = + render_target_size.get_mut(world, self.window_entity) else { return; }; @@ -382,21 +392,13 @@ 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 extracted_window = extracted_windows.get(&self.window_entity); + let swap_chain_texture_view = + match extracted_window.and_then(|v| v.swap_chain_texture_view.as_ref()) { + None => return Ok(()), + Some(window) => 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 render_queue = world.get_resource::().unwrap(); let (vertex_buffer, index_buffer) = match (&self.vertex_buffer, &self.index_buffer) { @@ -432,13 +434,19 @@ impl Node for EguiNode { }); let mut render_pass = TrackedRenderPass::new(device, render_pass); - let Some(key) = EguiPipelineKey::from_extracted_window(extracted_window) else { - return Ok(()); + let (physical_width, physical_height, pipeline_key) = match extracted_window { + Some(window) => ( + window.physical_width, + window.physical_height, + EguiPipelineKey::from_extracted_window(window), + ), + None => unreachable!(), }; - - let Some(pipeline_id) = egui_pipelines.get(&extracted_window.entity) else { + let Some(key) = pipeline_key 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(()); }; @@ -454,8 +462,8 @@ impl Node for EguiNode { render_pass.set_viewport( 0., 0., - extracted_window.physical_width as f32, - extracted_window.physical_height as f32, + physical_width as f32, + physical_height as f32, 0., 1., ); @@ -479,11 +487,12 @@ impl Node for EguiNode { y: (draw_command.clip_rect.max.y * self.pixels_per_point).round() as u32, }, }; + let scrissor_rect = clip_urect.intersect(bevy::math::URect::new( 0, 0, - extracted_window.physical_width, - extracted_window.physical_width, + physical_width, + physical_height, )); if scrissor_rect.is_empty() { continue; @@ -529,10 +538,7 @@ impl Node for EguiNode { viewport: command.rect, clip_rect: draw_command.clip_rect, pixels_per_point: self.pixels_per_point, - screen_size_px: [ - extracted_window.physical_width, - extracted_window.physical_height, - ], + screen_size_px: [physical_width, physical_height], }; let viewport = info.viewport_in_pixels(); @@ -649,7 +655,7 @@ impl EguiBevyPaintCallback { } } - fn cb(&self) -> &dyn EguiBevyPaintCallbackImpl { + pub(crate) fn cb(&self) -> &dyn EguiBevyPaintCallbackImpl { self.0.as_ref() } } diff --git a/src/egui_render_to_texture_node.rs b/src/egui_render_to_texture_node.rs new file mode 100644 index 000000000..594c1da60 --- /dev/null +++ b/src/egui_render_to_texture_node.rs @@ -0,0 +1,445 @@ +use crate::{ + egui_node::{ + DrawCommand, DrawPrimitive, EguiBevyPaintCallback, EguiDraw, EguiPipelineKey, + PaintCallbackDraw, + }, + render_systems::{EguiPipelines, EguiTextureBindGroups, EguiTextureId, EguiTransforms}, + EguiRenderOutput, EguiRenderToTextureHandle, EguiSettings, RenderTargetSize, +}; +use bevy::{ + ecs::world::World, + prelude::Entity, + render::{ + render_asset::RenderAssets, + render_graph::{Node, NodeRunError, RenderGraphContext, RenderLabel}, + render_phase::TrackedRenderPass, + render_resource::{ + Buffer, BufferAddress, BufferDescriptor, BufferUsages, IndexFormat, LoadOp, Operations, + PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, StoreOp, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::GpuImage, + }, +}; +use bytemuck::cast_slice; + +/// [`RenderLabel`] type for the Egui Render to Texture pass. +#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] +pub struct EguiRenderToTexturePass { + /// Index of the window entity. + pub entity_index: u32, + /// Generation of the window entity. + pub entity_generation: u32, +} + +/// Egui render to texture node. +pub struct EguiRenderToTextureNode { + render_to_texture_target: Entity, + vertex_data: Vec, + vertex_buffer_capacity: usize, + vertex_buffer: Option, + index_data: Vec, + index_buffer_capacity: usize, + index_buffer: Option, + draw_commands: Vec, + postponed_updates: Vec<(egui::Rect, PaintCallbackDraw)>, + pixels_per_point: f32, +} +impl EguiRenderToTextureNode { + /// Constructs Egui render node. + pub fn new(render_to_texture_target: Entity) -> Self { + EguiRenderToTextureNode { + render_to_texture_target, + draw_commands: Vec::new(), + vertex_data: Vec::new(), + vertex_buffer_capacity: 0, + vertex_buffer: None, + index_data: Vec::new(), + index_buffer_capacity: 0, + index_buffer: None, + postponed_updates: Vec::new(), + pixels_per_point: 1., + } + } +} +impl Node for EguiRenderToTextureNode { + fn update(&mut self, world: &mut World) { + let Ok(image_handle) = world + .query::<&EguiRenderToTextureHandle>() + .get(world, self.render_to_texture_target) + .map(|handle| handle.0.clone_weak()) + else { + return; + }; + let Some(key) = world + .get_resource::>() + .and_then(|render_assets| render_assets.get(&image_handle)) + .map(EguiPipelineKey::from_gpu_image) + else { + return; + }; + + let mut render_target_sizes = world.query::<(&RenderTargetSize, &mut EguiRenderOutput)>(); + let Ok((render_target_size, mut render_output)) = + render_target_sizes.get_mut(world, self.render_to_texture_target) + else { + return; + }; + + let render_target_size = *render_target_size; + let paint_jobs = std::mem::take(&mut render_output.paint_jobs); + + let egui_settings = &world.get_resource::().unwrap(); + + let render_device = world.get_resource::().unwrap(); + + self.pixels_per_point = render_target_size.scale_factor * egui_settings.scale_factor; + if render_target_size.physical_width == 0.0 || render_target_size.physical_height == 0.0 { + return; + } + + let mut index_offset = 0; + + self.draw_commands.clear(); + self.vertex_data.clear(); + self.index_data.clear(); + self.postponed_updates.clear(); + + for egui::epaint::ClippedPrimitive { + clip_rect, + primitive, + } in paint_jobs + { + let clip_urect = bevy::math::URect { + min: bevy::math::UVec2 { + x: (clip_rect.min.x * self.pixels_per_point).round() as u32, + y: (clip_rect.min.y * self.pixels_per_point).round() as u32, + }, + max: bevy::math::UVec2 { + x: (clip_rect.max.x * self.pixels_per_point).round() as u32, + y: (clip_rect.max.y * self.pixels_per_point).round() as u32, + }, + }; + + if clip_urect + .intersect(bevy::math::URect::new( + 0, + 0, + render_target_size.physical_width as u32, + render_target_size.physical_height as u32, + )) + .is_empty() + { + continue; + } + + let mesh = match primitive { + egui::epaint::Primitive::Mesh(mesh) => mesh, + egui::epaint::Primitive::Callback(paint_callback) => { + let Ok(callback) = paint_callback.callback.downcast::() + else { + unimplemented!("Unsupported egui paint callback type"); + }; + + self.postponed_updates.push(( + clip_rect, + PaintCallbackDraw { + callback: callback.clone(), + rect: paint_callback.rect, + }, + )); + + self.draw_commands.push(DrawCommand { + primitive: DrawPrimitive::PaintCallback(PaintCallbackDraw { + callback, + rect: paint_callback.rect, + }), + clip_rect, + }); + continue; + } + }; + + self.vertex_data + .extend_from_slice(cast_slice::<_, u8>(mesh.vertices.as_slice())); + let indices_with_offset = mesh + .indices + .iter() + .map(|i| i + index_offset) + .collect::>(); + self.index_data + .extend_from_slice(cast_slice(indices_with_offset.as_slice())); + index_offset += mesh.vertices.len() as u32; + + let texture_handle = match mesh.texture_id { + egui::TextureId::Managed(id) => { + EguiTextureId::Managed(self.render_to_texture_target, id) + } + egui::TextureId::User(id) => EguiTextureId::User(id), + }; + + self.draw_commands.push(DrawCommand { + primitive: DrawPrimitive::Egui(EguiDraw { + vertices_count: mesh.indices.len(), + egui_texture: texture_handle, + }), + clip_rect, + }); + } + + if self.vertex_data.len() > self.vertex_buffer_capacity { + self.vertex_buffer_capacity = if self.vertex_data.len().is_power_of_two() { + self.vertex_data.len() + } else { + self.vertex_data.len().next_power_of_two() + }; + self.vertex_buffer = Some(render_device.create_buffer(&BufferDescriptor { + label: Some("egui vertex buffer"), + size: self.vertex_buffer_capacity as BufferAddress, + usage: BufferUsages::COPY_DST | BufferUsages::VERTEX, + mapped_at_creation: false, + })); + } + if self.index_data.len() > self.index_buffer_capacity { + self.index_buffer_capacity = if self.index_data.len().is_power_of_two() { + self.index_data.len() + } else { + self.index_data.len().next_power_of_two() + }; + self.index_buffer = Some(render_device.create_buffer(&BufferDescriptor { + label: Some("egui index buffer"), + size: self.index_buffer_capacity as BufferAddress, + usage: BufferUsages::COPY_DST | BufferUsages::INDEX, + mapped_at_creation: false, + })); + } + + for (clip_rect, command) in self.postponed_updates.drain(..) { + let info = egui::PaintCallbackInfo { + viewport: command.rect, + clip_rect, + pixels_per_point: self.pixels_per_point, + screen_size_px: [ + render_target_size.physical_width as u32, + render_target_size.physical_height as u32, + ], + }; + command + .callback + .cb() + .update(info, self.render_to_texture_target, key, world); + } + } + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let egui_pipelines = &world.get_resource::().unwrap().0; + let pipeline_cache = world.get_resource::().unwrap(); + + let extracted_render_to_texture: Option<&EguiRenderToTextureHandle> = + world.get(self.render_to_texture_target); + let Some(render_to_texture_gpu_image) = extracted_render_to_texture else { + return Ok(()); + }; + + let gpu_images = world.get_resource::>().unwrap(); + let gpu_image = gpu_images.get(&render_to_texture_gpu_image.0).unwrap(); + let key = EguiPipelineKey::from_gpu_image(gpu_image); + + let render_queue = world.get_resource::().unwrap(); + + let (vertex_buffer, index_buffer) = match (&self.vertex_buffer, &self.index_buffer) { + (Some(vertex), Some(index)) => (vertex, index), + _ => return Ok(()), + }; + + render_queue.write_buffer(vertex_buffer, 0, &self.vertex_data); + render_queue.write_buffer(index_buffer, 0, &self.index_data); + + let bind_groups = &world.get_resource::().unwrap(); + + let egui_transforms = world.get_resource::().unwrap(); + + let device = world.get_resource::().unwrap(); + + let render_pass = + render_context + .command_encoder() + .begin_render_pass(&RenderPassDescriptor { + label: Some("egui render to texture render pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: &gpu_image.texture_view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(wgpu_types::Color::TRANSPARENT), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + let mut render_pass = TrackedRenderPass::new(device, render_pass); + + let Some(pipeline_id) = egui_pipelines.get(&self.render_to_texture_target) else { + bevy::log::error!("no egui_pipeline"); + return Ok(()); + }; + let Some(pipeline) = pipeline_cache.get_render_pipeline(*pipeline_id) else { + return Ok(()); + }; + + let transform_buffer_offset = egui_transforms.offsets[&self.render_to_texture_target]; + let transform_buffer_bind_group = &egui_transforms.bind_group.as_ref().unwrap().1; + + let mut requires_reset = true; + + let mut vertex_offset: u32 = 0; + for draw_command in &self.draw_commands { + if requires_reset { + render_pass.set_viewport( + 0., + 0., + gpu_image.size.x as f32, + gpu_image.size.y as f32, + 0., + 1., + ); + + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group( + 0, + transform_buffer_bind_group, + &[transform_buffer_offset], + ); + + requires_reset = false; + } + + let clip_urect = bevy::math::URect { + min: bevy::math::UVec2 { + x: (draw_command.clip_rect.min.x * self.pixels_per_point).round() as u32, + y: (draw_command.clip_rect.min.y * self.pixels_per_point).round() as u32, + }, + max: bevy::math::UVec2 { + x: (draw_command.clip_rect.max.x * self.pixels_per_point).round() as u32, + y: (draw_command.clip_rect.max.y * self.pixels_per_point).round() as u32, + }, + }; + let scrissor_rect = clip_urect.intersect(bevy::math::URect::from_corners( + bevy::math::UVec2::ZERO, + gpu_image.size, + )); + if scrissor_rect.is_empty() { + continue; + } + + render_pass.set_scissor_rect( + scrissor_rect.min.x, + scrissor_rect.min.y, + scrissor_rect.width(), + scrissor_rect.height(), + ); + + match &draw_command.primitive { + DrawPrimitive::Egui(command) => { + let texture_bind_group = match bind_groups.get(&command.egui_texture) { + Some(texture_resource) => texture_resource, + None => { + vertex_offset += command.vertices_count as u32; + continue; + } + }; + + render_pass.set_bind_group(1, texture_bind_group, &[]); + + render_pass + .set_vertex_buffer(0, self.vertex_buffer.as_ref().unwrap().slice(..)); + render_pass.set_index_buffer( + self.index_buffer.as_ref().unwrap().slice(..), + 0, + IndexFormat::Uint32, + ); + + render_pass.draw_indexed( + vertex_offset..(vertex_offset + command.vertices_count as u32), + 0, + 0..1, + ); + + vertex_offset += command.vertices_count as u32; + } + DrawPrimitive::PaintCallback(command) => { + let info = egui::PaintCallbackInfo { + viewport: command.rect, + clip_rect: draw_command.clip_rect, + pixels_per_point: self.pixels_per_point, + screen_size_px: [gpu_image.size.x, gpu_image.size.y], + }; + + let viewport = info.viewport_in_pixels(); + if viewport.width_px > 0 && viewport.height_px > 0 { + requires_reset = true; + render_pass.set_viewport( + viewport.left_px as f32, + viewport.top_px as f32, + viewport.width_px as f32, + viewport.height_px as f32, + 0., + 1., + ); + + command.callback.cb().render( + info, + &mut render_pass, + self.render_to_texture_target, + key, + world, + ); + } + } + } + + // if (draw_command.clip_rect.min.x as u32) < physical_width + // && (draw_command.clip_rect.min.y as u32) < physical_height + // { + // let draw_primitive = match &draw_command.primitive { + // DrawPrimitive::Egui(draw_primitive) => draw_primitive, + // DrawPrimitive::PaintCallback(_) => unimplemented!(), + // }; + // let texture_bind_group = match bind_groups.get(&draw_primitive.egui_texture) { + // Some(texture_resource) => texture_resource, + // None => { + // vertex_offset += draw_primitive.vertices_count as u32; + // continue; + // } + // }; + // + // render_pass.set_bind_group(1, texture_bind_group, &[]); + // + // render_pass.set_scissor_rect( + // draw_command.clip_rect.min.x as u32, + // draw_command.clip_rect.min.y as u32, + // (draw_command.clip_rect.width() as u32) + // .min(physical_width.saturating_sub(draw_command.clip_rect.min.x as u32)), + // (draw_command.clip_rect.height() as u32) + // .min(physical_height.saturating_sub(draw_command.clip_rect.min.y as u32)), + // ); + // + // render_pass.draw_indexed( + // vertex_offset..(vertex_offset + draw_primitive.vertices_count as u32), + // 0, + // 0..1, + // ); + // vertex_offset += draw_primitive.vertices_count as u32; + // } + } + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 65cd1048f..60c7c9cdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,12 +60,15 @@ compile_error!(include_str!("../static/error_web_sys_unstable_apis.txt")); /// Egui render node. #[cfg(feature = "render")] pub mod egui_node; +/// Egui render node for rendering to a texture. +#[cfg(feature = "render")] +pub mod egui_render_to_texture_node; /// Plugin systems for the render app. #[cfg(feature = "render")] pub mod render_systems; /// Plugin systems. pub mod systems; -/// Clipboard management for web +/// Clipboard management for web. #[cfg(all( feature = "manage_clipboard", target_arch = "wasm32", @@ -86,6 +89,8 @@ use crate::{ not(any(target_arch = "wasm32", target_os = "android")) ))] use arboard::Clipboard; +#[cfg(feature = "render")] +use bevy::ecs::query::Or; #[allow(unused_imports)] use bevy::log; #[cfg(feature = "render")] @@ -172,7 +177,7 @@ impl Default for EguiSettings { } } -/// Is used for storing Egui context input.. +/// Is used for storing Egui context input. /// /// It gets reset during the [`EguiSet::ProcessInput`] system. #[derive(Component, Clone, Debug, Default, Deref, DerefMut)] @@ -342,6 +347,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. @@ -354,7 +365,7 @@ pub struct EguiContexts<'w, 's> { &'static mut EguiContext, Option<&'static PrimaryWindow>, ), - With, + EguiContextsFilter, >, #[cfg(feature = "render")] user_textures: ResMut<'w, EguiUserTextures>, @@ -382,21 +393,21 @@ impl<'w, 's> EguiContexts<'w, 's> { }) } - /// Egui context of a specific window. + /// Egui context of a specific entity. #[must_use] - pub fn ctx_for_window_mut(&mut self, window: Entity) -> &mut egui::Context { - self.try_ctx_for_window_mut(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)")) + pub fn ctx_for_entity_mut(&mut self, enity: Entity) -> &mut egui::Context { + self.try_ctx_for_entity_mut(enity) + .unwrap_or_else(|| panic!("`EguiContexts::ctx_for_window_mut` was called for an uninitialized context (entity {enity:?}), make sure your system is run after [`EguiSet::InitContexts`] (or [`EguiStartupSet::InitContexts`] for startup systems)")) } - /// 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> { + pub fn try_ctx_for_entity_mut(&mut self, entity: Entity) -> Option<&mut egui::Context> { self.q .iter_mut() .find_map(|(window_entity, ctx, _primary_window)| { - if window_entity == window { + if window_entity == entity { Some(ctx.into_inner().get_mut()) } else { None @@ -407,7 +418,7 @@ impl<'w, 's> EguiContexts<'w, 's> { /// 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. #[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> { @@ -466,12 +477,12 @@ 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 { - self.try_ctx_for_window(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)")) + pub fn ctx_for_entity(&self, entity: Entity) -> &egui::Context { + self.try_ctx_for_entity(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)")) } - /// Fallible variant of [`EguiContexts::ctx_for_window_mut`]. + /// Fallible variant of [`EguiContexts::ctx_for_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 @@ -483,11 +494,11 @@ 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> { + pub fn try_ctx_for_entity(&self, entity: Entity) -> Option<&egui::Context> { self.q .iter() .find_map(|(window_entity, ctx, _primary_window)| { - if window_entity == window { + if window_entity == entity { Some(ctx.get()) } else { None @@ -523,6 +534,11 @@ impl<'w, 's> EguiContexts<'w, 's> { } } +/// Contains the texture [`Image`] to render to. +#[cfg(feature = "render")] +#[derive(Component, Clone, Debug, ExtractComponent)] +pub struct EguiRenderToTextureHandle(pub Handle); + /// A resource for storing `bevy_egui` user textures. #[derive(Clone, Resource, Default, ExtractResource)] #[cfg(feature = "render")] @@ -569,7 +585,7 @@ impl EguiUserTextures { /// Stores physical size and scale factor, is used as a helper to calculate logical size. #[derive(Component, Debug, Default, Clone, Copy, PartialEq)] #[cfg_attr(feature = "render", derive(ExtractComponent))] -pub struct WindowSize { +pub struct RenderTargetSize { /// Physical width pub physical_width: f32, /// Physical height @@ -578,7 +594,7 @@ pub struct WindowSize { pub scale_factor: f32, } -impl WindowSize { +impl RenderTargetSize { fn new(physical_width: f32, physical_height: f32, scale_factor: f32) -> Self { Self { physical_width, @@ -587,13 +603,13 @@ impl WindowSize { } } - /// Returns the width of the window. + /// Returns the width of the render target. #[inline] pub fn width(&self) -> f32 { self.physical_width / self.scale_factor } - /// Returns the height of the window. + /// Returns the height of the render target. #[inline] pub fn height(&self) -> f32 { self.physical_height / self.scale_factor @@ -616,7 +632,7 @@ pub enum EguiStartupSet { /// The `bevy_egui` plugin system sets. #[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)] pub enum EguiSet { - /// Initializes Egui contexts for newly created windows. + /// Initializes Egui contexts for newly created render targets. InitContexts, /// Reads Egui inputs (keyboard, mouse, etc) and writes them into the [`EguiInput`] resource. /// @@ -657,9 +673,11 @@ impl Plugin for EguiPlugin { #[cfg(feature = "render")] app.add_plugins(ExtractComponentPlugin::::default()); #[cfg(feature = "render")] - app.add_plugins(ExtractComponentPlugin::::default()); + app.add_plugins(ExtractComponentPlugin::::default()); #[cfg(feature = "render")] app.add_plugins(ExtractComponentPlugin::::default()); + #[cfg(feature = "render")] + app.add_plugins(ExtractComponentPlugin::::default()); #[cfg(all( feature = "manage_clipboard", @@ -671,8 +689,9 @@ impl Plugin for EguiPlugin { PreStartup, ( setup_new_windows_system, + setup_render_to_texture_handles_system, apply_deferred, - update_window_contexts_system, + update_contexts_system, ) .chain() .in_set(EguiStartupSet::InitContexts), @@ -681,8 +700,9 @@ impl Plugin for EguiPlugin { PreUpdate, ( setup_new_windows_system, + setup_render_to_texture_handles_system, apply_deferred, - update_window_contexts_system, + update_contexts_system, ) .chain() .in_set(EguiSet::InitContexts), @@ -737,7 +757,10 @@ impl Plugin for EguiPlugin { .init_resource::() .add_systems( ExtractSchedule, - render_systems::setup_new_windows_render_system, + ( + render_systems::setup_new_windows_render_system, + render_systems::setup_new_rtt_render_system, + ), ) .add_systems( Render, @@ -758,9 +781,10 @@ impl Plugin for EguiPlugin { /// Queries all the Egui related components. #[derive(QueryData)] #[query_data(mutable)] +#[non_exhaustive] pub struct EguiContextQuery { /// Window entity. - pub window_entity: Entity, + pub render_target: Entity, /// Egui context associated with the window. pub ctx: &'static mut EguiContext, /// Encapsulates [`egui::RawInput`]. @@ -770,9 +794,12 @@ pub struct EguiContextQuery { /// Encapsulates [`egui::PlatformOutput`]. 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, + pub render_target_size: &'static mut RenderTargetSize, + /// [`Window`] component, when rendering to a window. + pub window: Option<&'static mut Window>, + /// [`EguiRenderToTextureHandle`] component, when rendering to a texture. + #[cfg(feature = "render")] + pub render_to_texture: Option<&'static mut EguiRenderToTextureHandle>, } /// Contains textures allocated and painted by Egui. @@ -800,19 +827,45 @@ pub fn setup_new_windows_system( EguiRenderOutput::default(), EguiInput::default(), EguiOutput::default(), - WindowSize::default(), + RenderTargetSize::default(), + )); + } +} +/// Adds bevy_egui components to newly created windows. +pub fn setup_render_to_texture_handles_system( + mut commands: Commands, + #[cfg(feature = "render")] new_render_to_texture_targets: Query< + Entity, + (Added, Without), + >, + #[cfg(not(feature = "render"))] new_render_to_texture_targets: Query< + Entity, + Without, + >, +) { + for render_to_texture_target in new_render_to_texture_targets.iter() { + commands.entity(render_to_texture_target).insert(( + EguiContext::default(), + EguiRenderOutput::default(), + EguiInput::default(), + EguiOutput::default(), + RenderTargetSize::default(), )); } } /// Updates textures painted by Egui. #[cfg(feature = "render")] +#[allow(clippy::type_complexity)] 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>, ) { - 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 { @@ -828,8 +881,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); @@ -844,7 +896,7 @@ pub fn update_egui_textures_system( let image = egui_node::color_image_as_bevy_image(&color_image, sampler); let handle = image_assets.add(image); egui_managed_textures.insert( - (window_id, texture_id), + (entity, texture_id), EguiManagedTexture { handle, color_image, @@ -864,18 +916,22 @@ pub fn update_egui_textures_system( } #[cfg(feature = "render")] +#[allow(clippy::type_complexity)] 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); } diff --git a/src/render_systems.rs b/src/render_systems.rs index 4211040ee..bae941bc5 100644 --- a/src/render_systems.rs +++ b/src/render_systems.rs @@ -1,6 +1,8 @@ use crate::{ egui_node::{EguiNode, EguiPipeline, EguiPipelineKey}, - EguiManagedTextures, EguiSettings, EguiUserTextures, WindowSize, + egui_render_to_texture_node::{EguiRenderToTextureNode, EguiRenderToTexturePass}, + EguiManagedTextures, EguiRenderToTextureHandle, EguiSettings, EguiUserTextures, + RenderTargetSize, }; use bevy::{ ecs::system::SystemParam, @@ -58,9 +60,9 @@ pub struct ExtractedEguiTextures<'w> { #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] pub struct EguiPass { /// Index of the window entity. - pub window_index: u32, + pub entity_index: u32, /// Generation of the window entity. - pub window_generation: u32, + pub entity_generation: u32, } impl ExtractedEguiTextures<'_> { @@ -88,8 +90,8 @@ pub fn setup_new_windows_render_system( ) { for window in windows.iter() { let egui_pass = EguiPass { - window_index: window.index(), - window_generation: window.generation(), + entity_index: window.index(), + entity_generation: window.generation(), }; let new_node = EguiNode::new(window); @@ -99,6 +101,24 @@ pub fn setup_new_windows_render_system( render_graph.add_node_edge(bevy::render::graph::CameraDriverLabel, egui_pass); } } +/// Sets up the pipeline for newly created Render to texture entities. +pub fn setup_new_rtt_render_system( + render_to_texture_targets: Extract>>, + mut render_graph: ResMut, +) { + for render_to_texture_target in render_to_texture_targets.iter() { + let egui_rtt_pass = EguiRenderToTexturePass { + entity_index: render_to_texture_target.index(), + entity_generation: render_to_texture_target.generation(), + }; + + let new_node = EguiRenderToTextureNode::new(render_to_texture_target); + + render_graph.add_node(egui_rtt_pass.clone(), new_node); + + render_graph.add_node_edge(egui_rtt_pass, bevy::render::graph::CameraDriverLabel); + } +} /// Describes the transform buffer. #[derive(Resource, Default)] @@ -123,11 +143,14 @@ pub struct EguiTransform { impl EguiTransform { /// Calculates the transform from window size and scale factor. - pub fn from_window_size(window_size: WindowSize, scale_factor: f32) -> Self { + pub fn from_render_target_size( + render_target_size: RenderTargetSize, + scale_factor: f32, + ) -> Self { EguiTransform { scale: Vec2::new( - 2.0 / (window_size.width() / scale_factor), - -2.0 / (window_size.height() / scale_factor), + 2.0 / (render_target_size.width() / scale_factor), + -2.0 / (render_target_size.height() / scale_factor), ), translation: Vec2::new(-1.0, 1.0), } @@ -137,7 +160,7 @@ impl EguiTransform { /// Prepares Egui transforms. pub fn prepare_egui_transforms_system( mut egui_transforms: ResMut, - window_sizes: Query<(Entity, &WindowSize)>, + render_target_sizes: Query<(Entity, &RenderTargetSize)>, egui_settings: Res, render_device: Res, @@ -148,10 +171,10 @@ pub fn prepare_egui_transforms_system( egui_transforms.buffer.clear(); egui_transforms.offsets.clear(); - for (window, size) in window_sizes.iter() { + for (window, size) in render_target_sizes.iter() { let offset = egui_transforms .buffer - .push(&EguiTransform::from_window_size( + .push(&EguiTransform::from_render_target_size( *size, egui_settings.scale_factor, )); @@ -217,27 +240,37 @@ pub fn queue_bind_groups_system( commands.insert_resource(EguiTextureBindGroups(bind_groups)) } -/// Cached Pipeline IDs for the specialized `EguiPipeline`s +/// Cached Pipeline IDs for the specialized instances of `EguiPipeline`. #[derive(Resource)] pub struct EguiPipelines(pub HashMap); -/// Queue [`EguiPipeline`]s specialized on each window's swap chain texture format. +/// Queue [`EguiPipeline`] instances specialized on each window's swap chain texture format. pub fn queue_pipelines_system( mut commands: Commands, pipeline_cache: Res, - mut pipelines: ResMut>, + mut specialized_pipelines: ResMut>, egui_pipeline: Res, windows: Res, + render_to_texture: Query<(Entity, &EguiRenderToTextureHandle)>, + images: Res>, ) { - let pipelines = windows + let mut pipelines: HashMap = windows .iter() .filter_map(|(window_id, window)| { let key = EguiPipelineKey::from_extracted_window(window)?; - let pipeline_id = pipelines.specialize(&pipeline_cache, &egui_pipeline, key); - + let pipeline_id = + specialized_pipelines.specialize(&pipeline_cache, &egui_pipeline, key); Some((*window_id, pipeline_id)) }) .collect(); + pipelines.extend(render_to_texture.iter().filter_map(|(entity_id, handle)| { + let img = images.get(&handle.0)?; + let key = EguiPipelineKey::from_gpu_image(img); + let pipeline_id = specialized_pipelines.specialize(&pipeline_cache, &egui_pipeline, key); + + Some((entity_id, pipeline_id)) + })); + commands.insert_resource(EguiPipelines(pipelines)); } diff --git a/src/systems.rs b/src/systems.rs index abd500e1e..07e1d85c0 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,6 +1,11 @@ +#[cfg(feature = "render")] +use crate::EguiRenderToTextureHandle; use crate::{ - EguiContext, EguiContextQuery, EguiContextQueryItem, EguiInput, EguiSettings, WindowSize, + EguiContext, EguiContextQuery, EguiContextQueryItem, EguiInput, EguiSettings, RenderTargetSize, }; + +#[cfg(feature = "render")] +use bevy::{asset::Assets, render::texture::Image}; use bevy::{ ecs::{ event::EventWriter, @@ -13,7 +18,7 @@ use bevy::{ touch::TouchInput, ButtonState, }, - log, + log::{self, error}, prelude::{Entity, EventReader, NonSend, Query, Resource, Time}, time::Real, window::{CursorMoved, RequestRedraw}, @@ -431,21 +436,40 @@ 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(), - ); - let width = new_window_size.physical_width - / new_window_size.scale_factor + let mut render_target_size = None; + if let Some(window) = context.window { + render_target_size = Some(RenderTargetSize::new( + window.physical_width() as f32, + window.physical_height() as f32, + window.scale_factor(), + )); + } + #[cfg(feature = "render")] + if let Some(EguiRenderToTextureHandle(handle)) = context.render_to_texture.as_deref() { + let image = images.get(handle).expect("rtt handle should be valid"); + let size = image.size_f32(); + render_target_size = Some(RenderTargetSize { + physical_width: size.x, + physical_height: size.y, + scale_factor: 1.0, + }) + } + + let Some(new_render_target_size) = render_target_size else { + error!("bevy_egui context without window or render to texture!"); + continue; + }; + let width = new_render_target_size.physical_width + / new_render_target_size.scale_factor / egui_settings.scale_factor; - let height = new_window_size.physical_height - / new_window_size.scale_factor + let height = new_render_target_size.physical_height + / new_render_target_size.scale_factor / egui_settings.scale_factor; if width < 1.0 || height < 1.0 { @@ -460,9 +484,9 @@ pub fn update_window_contexts_system( context .ctx .get_mut() - .set_pixels_per_point(new_window_size.scale_factor * egui_settings.scale_factor); + .set_pixels_per_point(new_render_target_size.scale_factor * egui_settings.scale_factor); - *context.window_size = new_window_size; + *context.render_target_size = new_render_target_size; } } @@ -513,21 +537,23 @@ pub fn process_output_system( egui_clipboard.set_contents(&platform_output.copied_text); } - 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(mut window) = context.window { + let mut set_icon = || { + window.cursor.icon = egui_to_winit_cursor_icon(platform_output.cursor_icon) + .unwrap_or(bevy::window::CursorIcon::Default); + }; - #[cfg(windows)] - { - let last_cursor_icon = last_cursor_icon.entry(context.window_entity).or_default(); - if *last_cursor_icon != platform_output.cursor_icon { - set_icon(); - *last_cursor_icon = platform_output.cursor_icon; + #[cfg(windows)] + { + let last_cursor_icon = last_cursor_icon.entry(context.render_target).or_default(); + if *last_cursor_icon != platform_output.cursor_icon { + set_icon(); + *last_cursor_icon = platform_output.cursor_icon; + } } + #[cfg(not(windows))] + set_icon(); } - #[cfg(not(windows))] - set_icon(); let needs_repaint = !context.render_output.is_empty(); should_request_redraw |= ctx.has_requested_repaint() && needs_repaint;