summaryrefslogtreecommitdiff
path: root/engine/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'engine/src/ui')
-rw-r--r--engine/src/ui/dear_imgui.rs1082
-rw-r--r--engine/src/ui/view.rs2
-rw-r--r--engine/src/ui/view/transform_3d.rs128
-rw-r--r--engine/src/ui/view/world.rs2316
4 files changed, 3528 insertions, 0 deletions
diff --git a/engine/src/ui/dear_imgui.rs b/engine/src/ui/dear_imgui.rs
new file mode 100644
index 0000000..c5f7ce2
--- /dev/null
+++ b/engine/src/ui/dear_imgui.rs
@@ -0,0 +1,1082 @@
+use std::collections::HashMap;
+use std::path::{Path, PathBuf};
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::LazyLock;
+
+use dear_imgui_rs::{
+ DrawCmd as ImguiDrawCmd,
+ Key as ImguiKey,
+ MouseButton as ImguiMouseButton,
+};
+use ecs::component::local::Local;
+use ecs::component::Component;
+use ecs::error::Context as _;
+use ecs::event::component::{Changed, EventMatchExt};
+use ecs::pair::Pair;
+use ecs::query::term::With;
+use ecs::sole::Single;
+use ecs::system::initializable::Initializable;
+use ecs::system::observer::Observe;
+use ecs::system::Into;
+use ecs::time::Time;
+use ecs::{Component, Query, Sole};
+use intmap::IntMap;
+
+use crate::asset::{Assets, Handle as AssetHandle, Label as AssetLabel};
+use crate::data_types::dimens::Dimens;
+use crate::input::keyboard::{Key, Keyboard};
+use crate::input::mouse::{Button as MouseButton, Buttons as MouseButtons, Mouse};
+use crate::mesh::vertex_buffer::{
+ NamedVertexAttr as MeshNamedVertexAttr,
+ VertexAttrInfo as MeshVertexAttrInfo,
+ VertexBuffer as MeshVertexBuffer,
+ VertexLabel,
+};
+use crate::mesh::{Mesh, VertexAttrType as MeshVertexAttrType};
+use crate::projection::{
+ ClipVolume as ProjectionClipVolume,
+ Orthographic as OrthographicProjection,
+};
+use crate::reflection::Reflection;
+use crate::rendering::blending::{
+ Config as RenderingBlendingConfiguration,
+ Equation as RenderingBlendingEquation,
+ Factor as RenderingBlendingFactor,
+};
+use crate::rendering::object::Id as RenderingObjectId;
+use crate::rendering::shader::cursor::{
+ BindingTextureKind as ShaderBindingTextureKind,
+ BindingValue as ShaderBindingValue,
+ Cursor as ShaderCursor,
+};
+use crate::rendering::shader::{
+ Context as ShaderContext,
+ EntrypointFlags as ShaderEntrypointFlags,
+ ModuleSource as ShaderModuleSource,
+};
+use crate::rendering::{
+ AssetOrValue,
+ Command as RenderingCommand,
+ DrawMeshOptions,
+ DrawProperties,
+ DrawPropertiesUpdateFlags,
+ MeshUsage,
+ RenderPass,
+ RenderPasses,
+ ScissorBox,
+ SrgbaTextureDataType,
+ Surface,
+ SurfaceId,
+ TextureCreation,
+ TexturePixelDataFormat,
+ TextureUpdate,
+ PRE_RENDER_PHASE,
+};
+use crate::texture::Properties as TextureProperties;
+use crate::vector::Vec2;
+use crate::windowing::dpi::PhysicalSize;
+use crate::windowing::window::Window;
+
+mod reexports
+{
+ pub use dear_imgui_rs as bindings;
+}
+
+pub use reexports::*;
+
+pub static SHADER_ASSET_LABEL: LazyLock<AssetLabel> = LazyLock::new(|| AssetLabel {
+ path: Path::new("").into(),
+ name: Some("imgui_shader".into()),
+});
+
+/// Dear Imgui context
+#[derive(Sole)]
+pub struct Context
+{
+ pub enabled: bool,
+ ctx: inner_context_wrapper::InnerContextWrapper,
+ texture_lookup: IntMap<TextureLookupId, RenderingObjectId>,
+ texture_id_lookup:
+ HashMap<dear_imgui_rs::SnapshotTextureId, dear_imgui_rs::TextureId>,
+}
+
+impl Context
+{
+ pub fn frame(&mut self) -> Option<&mut bindings::Ui>
+ {
+ if !self.enabled {
+ return None;
+ }
+
+ self.ctx.get_frame()
+ }
+
+ pub fn register_texture(
+ &mut self,
+ texture_data: bindings::OwnedTextureData,
+ ) -> bindings::ManagedTextureId
+ {
+ self.ctx.register_texture(texture_data)
+ }
+}
+
+#[derive(Debug, Default)]
+#[non_exhaustive]
+pub struct Extension
+{
+ start_enabled: bool,
+ settings_ini_file_path: Option<PathBuf>,
+}
+
+impl Extension
+{
+ #[must_use]
+ pub fn with_start_enabled(mut self, start_enabled: bool) -> Self
+ {
+ self.start_enabled = start_enabled;
+ self
+ }
+
+ #[must_use]
+ pub fn with_settings_ini_file(
+ mut self,
+ settings_ini_file_path: Option<PathBuf>,
+ ) -> Self
+ {
+ self.settings_ini_file_path = settings_ini_file_path;
+ self
+ }
+}
+
+impl ecs::extension::Extension for Extension
+{
+ fn collect(self, mut collector: ecs::extension::Collector<'_>)
+ {
+ let mut context = Context {
+ enabled: self.start_enabled,
+ ctx: inner_context_wrapper::InnerContextWrapper::new(),
+ texture_lookup: IntMap::with_capacity(8),
+ texture_id_lookup: HashMap::with_capacity(8),
+ };
+
+ if let Err(err) = context
+ .ctx
+ .set_settings_ini_file_path(self.settings_ini_file_path)
+ {
+ tracing::error!("Failed to set path of imgui settings ini file: {err}");
+ }
+
+ const {
+ assert!(
+ dear_imgui_rs::HAS_FREETYPE,
+ "Freetype font rasterizer support is not enabled"
+ );
+ }
+
+ unsafe {
+ context.ctx.font_atlas().add_font_from_memory_ttf(
+ include_bytes!("../../res/FiraMono-Regular.ttf"),
+ 14.0,
+ Some(
+ &dear_imgui_rs::FontConfig::default()
+ .name("FiraMono-Regular.ttf")
+ .font_loader_flags(
+ dear_imgui_rs::FontLoaderFlags::BITMAP
+ | dear_imgui_rs::FontLoaderFlags::FORCE_AUTOHINT,
+ ),
+ ),
+ None,
+ );
+ }
+
+ collector.add_sole(context).ok();
+
+ collector.add_system(
+ *PRE_RENDER_PHASE,
+ update.into_system().initialize((State::default(),)),
+ );
+
+ collector.add_observer(handle_window_changed);
+ }
+}
+
+#[tracing::instrument(skip_all)]
+fn handle_window_changed(
+ observe: Observe<Pair<Changed, Window>>,
+ mut context: Single<Context>,
+)
+{
+ let Ok(context) = context.get_mut() else {
+ unreachable!();
+ };
+
+ let Some(event_match) = observe
+ .iter()
+ .find(|event_match| event_match.get_entity().has_component(TargetWindow::id()))
+ else {
+ return;
+ };
+
+ let window = event_match.get_ent_target_comp();
+
+ let hidpi_factor = window.scale_factor().round();
+
+ context
+ .ctx
+ .get_io_mut()
+ .set_display_framebuffer_scale([hidpi_factor as f32, hidpi_factor as f32]);
+
+ let window_size_logical = PhysicalSize {
+ width: f64::from(window.inner_size.width),
+ height: f64::from(window.inner_size.height),
+ }
+ .to_logical::<f64>(hidpi_factor);
+
+ context.ctx.get_io_mut().set_display_size([
+ window_size_logical.width as f32,
+ window_size_logical.height as f32,
+ ]);
+}
+
+fn update(
+ target_window_query: Query<(&Window, &Surface, With<TargetWindow>)>,
+ mut context: Single<Context>,
+ time: Single<Time>,
+ mut assets: Single<Assets>,
+ mut render_passes: Single<RenderPasses>,
+ shader_context: Single<ShaderContext>,
+ keyboard: Single<Keyboard>,
+ mouse: Single<Mouse>,
+ mouse_buttons: Single<MouseButtons>,
+ mut state: Local<State>,
+) -> Result<(), crate::Error>
+{
+ let Ok(context) = context.get_mut() else {
+ unreachable!();
+ };
+
+ let assets = assets.get_mut()?;
+ let render_passes = render_passes.get_mut()?;
+ let shader_context = shader_context.get()?;
+ let keyboard = keyboard.get()?;
+ let mouse = mouse.get()?;
+ let mouse_buttons = mouse_buttons.get()?;
+
+ let Some((window, window_surface)) = target_window_query.iter().next() else {
+ return Ok(());
+ };
+
+ let curr_state = std::mem::replace(&mut *state, State::Unreachable);
+
+ match curr_state {
+ State::NotInitialized => {
+ let Some((shader_asset, mesh_obj_id, mesh)) = initialize_context(
+ context,
+ &window,
+ window_surface.id,
+ assets,
+ render_passes,
+ shader_context,
+ ) else {
+ *state = State::NotInitialized;
+ return Ok(());
+ };
+
+ context.ctx.new_frame();
+
+ *state = State::Ready { shader_asset, mesh_obj_id, mesh };
+ }
+ State::Ready { shader_asset, mesh_obj_id, mut mesh } => {
+ if !context.enabled {
+ *state = State::Ready { shader_asset, mesh_obj_id, mesh };
+ return Ok(());
+ }
+
+ context
+ .ctx
+ .get_io_mut()
+ .set_delta_time(time.delta_time.as_secs_f32());
+
+ update_inputs(context, &window, keyboard, mouse, mouse_buttons);
+
+ add_drawing_render_pass(
+ context,
+ render_passes,
+ shader_context,
+ window_surface.id,
+ shader_asset.clone(),
+ mesh_obj_id,
+ &mut mesh,
+ )?;
+
+ context.ctx.new_frame();
+
+ *state = State::Ready { shader_asset, mesh_obj_id, mesh };
+ }
+ State::Unreachable => unreachable!(),
+ }
+
+ Ok(())
+}
+
+#[derive(Debug, Default, Clone, Copy, Component, Reflection)]
+pub struct TargetWindow;
+
+#[derive(Debug, Default, Component)]
+enum State
+{
+ #[default]
+ NotInitialized,
+ Ready
+ {
+ shader_asset: AssetHandle<ShaderModuleSource>,
+ mesh_obj_id: RenderingObjectId,
+ mesh: Mesh,
+ },
+ Unreachable,
+}
+
+fn initialize_context(
+ context: &mut Context,
+ window: &Window,
+ window_surface_id: SurfaceId,
+ assets: &mut Assets,
+ render_passes: &mut RenderPasses,
+ shader_context: &ShaderContext,
+) -> Option<(AssetHandle<ShaderModuleSource>, RenderingObjectId, Mesh)>
+{
+ let Some(shader_asset) =
+ assets.get_handle_to_loaded::<ShaderModuleSource>(SHADER_ASSET_LABEL.clone())
+ else {
+ assets.store_with_label(
+ SHADER_ASSET_LABEL.clone(),
+ ShaderModuleSource {
+ name: "imgui_shader.slang".into(),
+ file_path: Path::new("@engine/imgui_shader").into(),
+ source: include_str!("../../res/imgui_shader.slang").into(),
+ link_entrypoints: ShaderEntrypointFlags::VERTEX
+ | ShaderEntrypointFlags::FRAGMENT,
+ },
+ );
+
+ // We want to wait with initializing until the shader have been processed by the
+ // shader context
+ return None;
+ };
+
+ #[allow(clippy::cast_possible_truncation)]
+ let hidpi_factor = window.scale_factor().round() as f32;
+
+ context
+ .ctx
+ .get_io_mut()
+ .set_display_framebuffer_scale([hidpi_factor, hidpi_factor]);
+
+ let window_size = Dimens {
+ width: window.inner_size.width as f32,
+ height: window.inner_size.height as f32,
+ };
+
+ #[allow(clippy::cast_possible_truncation)]
+ let window_logical_size = window_size / hidpi_factor;
+
+ context
+ .ctx
+ .get_io_mut()
+ .set_display_size([window_logical_size.width, window_logical_size.height]);
+
+ let mesh_obj_id = RenderingObjectId::new_sequential();
+
+ let mesh = Mesh::builder()
+ .vertices(MeshVertexBuffer::with_capacity(
+ &[
+ MeshVertexAttrInfo {
+ label: VertexLabel::Position,
+ ty: MeshVertexAttrType::Float32Array { length: 3 },
+ },
+ MeshVertexAttrInfo {
+ label: VertexLabel::Color,
+ ty: MeshVertexAttrType::Float32Array { length: 4 },
+ },
+ MeshVertexAttrInfo {
+ label: VertexLabel::UvFromTopLeft,
+ ty: MeshVertexAttrType::Float32Array { length: 2 },
+ },
+ ],
+ 128,
+ ))
+ .indices([])
+ .build();
+
+ render_passes.passes.push_back(RenderPass {
+ commands: vec![
+ RenderingCommand::MakeCurrent(window_surface_id),
+ RenderingCommand::CreateShaderProgram(
+ RenderingObjectId::Asset(shader_asset.id()),
+ shader_context
+ .get_program(&shader_asset.id())
+ .expect("Not possible")
+ .clone(),
+ ),
+ RenderingCommand::ActivateShader(RenderingObjectId::Asset(shader_asset.id())),
+ RenderingCommand::CreateMesh {
+ obj_id: mesh_obj_id,
+ mesh: AssetOrValue::Value(mesh.clone()),
+ usage: MeshUsage::Stream,
+ },
+ ],
+ draw_properties: DrawProperties::default(),
+ });
+
+ Some((shader_asset, mesh_obj_id, mesh))
+}
+
+fn update_inputs(
+ context: &mut Context,
+ window: &Window,
+ keyboard: &Keyboard,
+ mouse: &Mouse,
+ mouse_buttons: &MouseButtons,
+)
+{
+ let io = context.ctx.get_io_mut();
+
+ for (key, key_state) in keyboard.new_key_states() {
+ if let Some(key) = key_to_imgui_key(key) {
+ io.add_key_event(key, key_state.is_pressed());
+ }
+ }
+
+ for character in keyboard.text_keys().chars() {
+ if character == '\u{7f}' {
+ continue;
+ }
+
+ io.add_input_character(character);
+ }
+
+ io.add_mouse_source_event(dear_imgui_rs::input::MouseSource::Mouse);
+
+ let mouse_pos = mouse.position.to_logical::<f64>(window.scale_factor());
+
+ #[allow(clippy::cast_possible_truncation)]
+ io.add_mouse_pos_event([mouse_pos.x as f32, mouse_pos.y as f32]);
+
+ if !mouse.curr_tick_scroll_delta.is_zero() {
+ io.add_mouse_wheel_event([
+ mouse.curr_tick_scroll_delta.hor_lines,
+ mouse.curr_tick_scroll_delta.vert_lines,
+ ]);
+ }
+
+ for (mouse_button, mouse_button_state) in mouse_buttons.all_current() {
+ if let Some(mouse_button) = mouse_button_to_imgui_mouse_button(mouse_button) {
+ io.add_mouse_button_event(mouse_button, mouse_button_state.is_pressed());
+ }
+ }
+}
+
+fn add_drawing_render_pass(
+ context: &mut Context,
+ render_passes: &mut RenderPasses,
+ shader_context: &ShaderContext,
+ window_surface_id: SurfaceId,
+ shader_asset: AssetHandle<ShaderModuleSource>,
+ mesh_obj_id: RenderingObjectId,
+ mesh: &mut Mesh,
+) -> Result<(), crate::Error>
+{
+ let render_pass = render_passes.passes.push_back_mut(RenderPass {
+ commands: Vec::new(),
+ draw_properties: DrawProperties {
+ blending_enabled: true,
+ blending_config: RenderingBlendingConfiguration {
+ equation: RenderingBlendingEquation::Add,
+ source_factor: RenderingBlendingFactor::SrcAlpha,
+ destination_factor: RenderingBlendingFactor::OneMinusSrcAlpha,
+ },
+ depth_test_enabled: false,
+ face_culling_enabled: false,
+ scissor_test_enabled: true,
+ ..Default::default()
+ },
+ });
+
+ let shader_program = shader_context
+ .get_program(&shader_asset.id())
+ .expect("Not possible");
+
+ let shader_cursor = ShaderCursor::new(
+ shader_program
+ .reflection(0)
+ .unwrap()
+ .global_params_var_layout()
+ .unwrap(),
+ );
+
+ let Context {
+ enabled: _,
+ ctx,
+ texture_lookup,
+ texture_id_lookup,
+ } = context;
+
+ let mut render_frame = ctx.render();
+
+ let mut tex_feedbacks = Vec::with_capacity(render_frame.texture_requests().len());
+
+ for texture_request in render_frame.texture_requests() {
+ match texture_request.operation() {
+ dear_imgui_rs::TextureOp::Create {
+ format,
+ width,
+ height,
+ row_pitch: _,
+ pixels,
+ } => {
+ if !matches!(format, dear_imgui_rs::TextureFormat::RGBA32) {
+ unimplemented!();
+ }
+
+ let texture_lookup_id = TextureLookupId::new();
+
+ tracing::trace!(
+ snapshot_tex_id = ?texture_request.texture(),
+ tex_lookup_id = ?texture_lookup_id,
+ "Performing texture operation: Create"
+ );
+
+ let texture_object_id = RenderingObjectId::new_sequential();
+
+ render_pass.commands.push(RenderingCommand::CreateTexture {
+ obj_id: texture_object_id,
+ pixel_data_format: TexturePixelDataFormat::Srgba(
+ SrgbaTextureDataType::UnsignedByte,
+ ),
+ creation: TextureCreation::Texture2D {
+ size: Dimens { width: *width, height: *height },
+ image: Some(pixels.clone().into_boxed_slice()),
+ },
+ properties: TextureProperties::default(),
+ });
+
+ texture_lookup.insert(texture_lookup_id, texture_object_id);
+
+ texture_id_lookup
+ .insert(texture_request.texture(), texture_lookup_id.into());
+
+ let texture_feedback =
+ match texture_request.uploaded(texture_lookup_id.into()) {
+ Ok(texture_feedback) => texture_feedback,
+ Err(
+ dear_imgui_rs::TextureFeedbackError::UploadForDestroy
+ | dear_imgui_rs::TextureFeedbackError::DestroyForUpload,
+ ) => unreachable!(),
+ };
+
+ tex_feedbacks.push(texture_feedback);
+ }
+ dear_imgui_rs::TextureOp::Update { format, width: _, height: _, rects } => {
+ if !matches!(format, dear_imgui_rs::TextureFormat::RGBA32) {
+ unimplemented!();
+ }
+
+ let texture_lookup_id =
+ TextureLookupId::from(texture_id_lookup[&texture_request.texture()]);
+
+ tracing::trace!(
+ snapshot_tex_id = ?texture_request.texture(),
+ tex_lookup_id = ?texture_lookup_id,
+ "Performing texture operation: Update"
+ );
+
+ let Some(texture_object_id) = texture_lookup.get(texture_lookup_id)
+ else {
+ tracing::error!(
+ snapshot_tex_id = ?texture_request.texture(),
+ tex_lookup_id = ?texture_lookup_id,
+ "Unknown texture. Skipping update",
+ );
+ continue;
+ };
+
+ render_pass.commands.reserve(rects.len());
+
+ for rect in rects {
+ render_pass.commands.push(RenderingCommand::UpdateTexture {
+ obj_id: *texture_object_id,
+ pixels: rect.data.clone().into_boxed_slice(),
+ pixel_data_format: TexturePixelDataFormat::Srgba(
+ SrgbaTextureDataType::UnsignedByte,
+ ),
+ update: TextureUpdate::Texture2D {
+ size: Dimens::<u32> {
+ width: rect.rect.w.into(),
+ height: rect.rect.h.into(),
+ },
+ offset: Vec2 {
+ x: u32::from(rect.rect.x),
+ y: u32::from(rect.rect.y),
+ },
+ },
+ });
+ }
+
+ let texture_feedback =
+ match texture_request.uploaded(texture_lookup_id.into()) {
+ Ok(texture_feedback) => texture_feedback,
+ Err(
+ dear_imgui_rs::TextureFeedbackError::UploadForDestroy
+ | dear_imgui_rs::TextureFeedbackError::DestroyForUpload,
+ ) => unreachable!(),
+ };
+
+ tex_feedbacks.push(texture_feedback);
+ }
+ dear_imgui_rs::TextureOp::Destroy => {
+ let texture_lookup_id =
+ TextureLookupId::from(texture_id_lookup[&texture_request.texture()]);
+
+ tracing::trace!(
+ snapshot_tex_id = ?texture_request.texture(),
+ tex_lookup_id = ?texture_lookup_id,
+ "Performing texture operation: Destroy"
+ );
+
+ let Some(texture_object_id) = texture_lookup.remove(texture_lookup_id)
+ else {
+ tracing::error!(
+ texture = ?texture_lookup_id,
+ "Unknown texture. Skipping destroyal",
+ );
+ continue;
+ };
+
+ render_pass
+ .commands
+ .push(RenderingCommand::RemoveTexture(texture_object_id));
+
+ let texture_feedback = match texture_request.destroyed() {
+ Ok(texture_feedback) => texture_feedback,
+ Err(
+ dear_imgui_rs::TextureFeedbackError::UploadForDestroy
+ | dear_imgui_rs::TextureFeedbackError::DestroyForUpload,
+ ) => unreachable!(),
+ };
+
+ tex_feedbacks.push(texture_feedback);
+ }
+ }
+ }
+
+ render_frame
+ .reconcile_texture_feedback(tex_feedbacks)
+ .with_context(|| "Failed to reconcile texture feedback")?;
+
+ let draw_data = render_frame.draw_data();
+
+ let [display_width, display_height] = draw_data.display_size();
+
+ // let [scale_width, scale_height] = draw_data.framebuffer_scale;
+
+ let shader_object_id = RenderingObjectId::Asset(shader_asset.id());
+
+ render_pass.commands.extend([
+ RenderingCommand::MakeCurrent(window_surface_id),
+ RenderingCommand::ActivateShader(shader_object_id),
+ RenderingCommand::SetShaderBinding(
+ shader_object_id,
+ shader_cursor
+ .field("Uniforms")
+ .field("projection")
+ .binding(
+ OrthographicProjection::builder()
+ .near(1.0)
+ .far(-1.0)
+ .viewport_origin(Vec2 { x: 0.0, y: 1.0 })
+ .size(crate::projection::OrthographicSize::FixedSize(Dimens {
+ width: display_width,
+ height: -display_height,
+ }))
+ .build()
+ .to_matrix_rh(
+ // Window size can be 0x0 since it will not be used
+ PhysicalSize::default(),
+ ProjectionClipVolume::NegOneToOne,
+ )
+ .into(),
+ )?,
+ ),
+ ]);
+
+ for draw_list in draw_data.draw_lists() {
+ mesh.vertex_buf_mut().clear();
+
+ for vertex in draw_list.vtx_buffer() {
+ mesh.vertex_buf_mut().push((
+ MeshNamedVertexAttr::<[f32; 3]> {
+ label: VertexLabel::Position,
+ value: [vertex.pos[0], vertex.pos[1], 0.0],
+ },
+ MeshNamedVertexAttr {
+ label: VertexLabel::Color,
+ value: vertex.rgba().map(|elem| f32::from(elem) / 255.0),
+ },
+ MeshNamedVertexAttr {
+ label: VertexLabel::UvFromTopLeft,
+ value: vertex.uv,
+ },
+ ));
+ }
+
+ mesh.set_indices(draw_list.idx_buffer().iter().map(|index| (*index).into()));
+
+ render_pass.commands.push(RenderingCommand::UpdateMesh {
+ obj_id: mesh_obj_id,
+ mesh: mesh.clone(),
+ usage: MeshUsage::Stream,
+ });
+
+ for command in draw_list.commands() {
+ if let ImguiDrawCmd::Elements { count, cmd_params } = command {
+ let Some(scissor_box) = calc_draw_cmd_scissor_box(draw_data, &cmd_params)
+ else {
+ continue;
+ };
+
+ let tex_id = cmd_params.texture_id;
+
+ let Some(texture_object_id) =
+ texture_lookup.get(TextureLookupId::from(tex_id))
+ else {
+ tracing::error!(
+ "Unknown texture {}. Skipping skipping draw command",
+ tex_id.id()
+ );
+ continue;
+ };
+
+ render_pass.commands.extend([
+ RenderingCommand::UpdateDrawProperties(
+ DrawProperties { scissor_box, ..Default::default() },
+ DrawPropertiesUpdateFlags::SCISSOR_BOX,
+ ),
+ RenderingCommand::SetShaderBinding(
+ shader_object_id,
+ shader_cursor.field("main_texture").binding(
+ ShaderBindingValue::Texture(
+ *texture_object_id,
+ ShaderBindingTextureKind::Texture2D,
+ ),
+ )?,
+ ),
+ RenderingCommand::DrawMesh(
+ mesh_obj_id,
+ DrawMeshOptions::builder()
+ .element_offset(cmd_params.idx_offset.try_into().unwrap())
+ .vertex_offset(cmd_params.vtx_offset.try_into().unwrap())
+ .element_cnt(count.try_into().unwrap())
+ .build(),
+ ),
+ ]);
+ }
+ }
+ }
+
+ Ok(())
+}
+
+fn calc_draw_cmd_scissor_box(
+ draw_data: &dear_imgui_rs::DrawData,
+ cmd_params: &dear_imgui_rs::DrawCmdParams,
+) -> Option<ScissorBox>
+{
+ let clip_rect = cmd_params.clip_rect;
+
+ let display_size = draw_data.display_size();
+ let display_pos = draw_data.display_pos();
+ let framebuffer_scale = draw_data.framebuffer_scale();
+
+ let clip_min_x = (clip_rect[0] - display_pos[0]) * framebuffer_scale[0];
+
+ let clip_min_y = (clip_rect[1] - display_pos[1]) * framebuffer_scale[1];
+
+ let clip_max_x = (clip_rect[2] - display_pos[0]) * framebuffer_scale[0];
+
+ let clip_max_y = (clip_rect[3] - display_pos[1]) * framebuffer_scale[1];
+
+ if clip_max_x <= clip_min_x || clip_max_y <= clip_min_y {
+ return None;
+ }
+
+ let fb_height = display_size[1] * framebuffer_scale[1];
+
+ Some(ScissorBox {
+ size: Some(Dimens {
+ width: (clip_max_x - clip_min_x) as u16,
+ height: (clip_max_y - clip_min_y) as u16,
+ }),
+ lower_left_corner_pos: Vec2 {
+ x: clip_min_x as u16,
+ y: (fb_height - clip_max_y) as u16,
+ },
+ })
+}
+
+fn key_to_imgui_key(key: Key) -> Option<ImguiKey>
+{
+ match key {
+ Key::Backquote => Some(ImguiKey::GraveAccent),
+ Key::Backslash => Some(ImguiKey::Backslash),
+ Key::BracketLeft => Some(ImguiKey::LeftBracket),
+ Key::BracketRight => Some(ImguiKey::RightBracket),
+ Key::Comma => Some(ImguiKey::Comma),
+ Key::Digit0 => Some(ImguiKey::Key0),
+ Key::Digit1 => Some(ImguiKey::Key1),
+ Key::Digit2 => Some(ImguiKey::Key2),
+ Key::Digit3 => Some(ImguiKey::Key3),
+ Key::Digit4 => Some(ImguiKey::Key4),
+ Key::Digit5 => Some(ImguiKey::Key5),
+ Key::Digit6 => Some(ImguiKey::Key6),
+ Key::Digit7 => Some(ImguiKey::Key7),
+ Key::Digit8 => Some(ImguiKey::Key8),
+ Key::Digit9 => Some(ImguiKey::Key9),
+ Key::Equal => Some(ImguiKey::Equal),
+ Key::A => Some(ImguiKey::A),
+ Key::B => Some(ImguiKey::B),
+ Key::C => Some(ImguiKey::C),
+ Key::D => Some(ImguiKey::D),
+ Key::E => Some(ImguiKey::E),
+ Key::F => Some(ImguiKey::F),
+ Key::G => Some(ImguiKey::G),
+ Key::H => Some(ImguiKey::H),
+ Key::I => Some(ImguiKey::I),
+ Key::J => Some(ImguiKey::J),
+ Key::K => Some(ImguiKey::K),
+ Key::L => Some(ImguiKey::L),
+ Key::M => Some(ImguiKey::M),
+ Key::N => Some(ImguiKey::N),
+ Key::O => Some(ImguiKey::O),
+ Key::P => Some(ImguiKey::P),
+ Key::Q => Some(ImguiKey::Q),
+ Key::R => Some(ImguiKey::R),
+ Key::S => Some(ImguiKey::S),
+ Key::T => Some(ImguiKey::T),
+ Key::U => Some(ImguiKey::U),
+ Key::V => Some(ImguiKey::V),
+ Key::W => Some(ImguiKey::W),
+ Key::X => Some(ImguiKey::X),
+ Key::Y => Some(ImguiKey::Y),
+ Key::Z => Some(ImguiKey::Z),
+ Key::Minus => Some(ImguiKey::Minus),
+ Key::Period => Some(ImguiKey::Period),
+ Key::Quote => Some(ImguiKey::Apostrophe),
+ Key::Semicolon => Some(ImguiKey::Semicolon),
+ Key::Slash => Some(ImguiKey::Slash),
+ Key::AltLeft => Some(ImguiKey::LeftAlt),
+ Key::AltRight => Some(ImguiKey::RightAlt),
+ Key::Backspace => Some(ImguiKey::Backspace),
+ Key::CapsLock => Some(ImguiKey::CapsLock),
+ Key::ControlLeft => Some(ImguiKey::LeftCtrl),
+ Key::ControlRight => Some(ImguiKey::RightCtrl),
+ Key::Enter => Some(ImguiKey::Enter),
+ Key::SuperLeft => Some(ImguiKey::LeftSuper),
+ Key::SuperRight => Some(ImguiKey::RightSuper),
+ Key::ShiftLeft => Some(ImguiKey::LeftShift),
+ Key::ShiftRight => Some(ImguiKey::RightShift),
+ Key::Space => Some(ImguiKey::Space),
+ Key::Tab => Some(ImguiKey::Tab),
+ Key::Delete => Some(ImguiKey::Delete),
+ Key::End => Some(ImguiKey::End),
+ Key::Home => Some(ImguiKey::Home),
+ Key::Insert => Some(ImguiKey::Insert),
+ Key::PageDown => Some(ImguiKey::PageDown),
+ Key::PageUp => Some(ImguiKey::PageUp),
+ Key::ArrowDown => Some(ImguiKey::DownArrow),
+ Key::ArrowLeft => Some(ImguiKey::LeftArrow),
+ Key::ArrowRight => Some(ImguiKey::RightArrow),
+ Key::ArrowUp => Some(ImguiKey::UpArrow),
+ Key::NumLock => Some(ImguiKey::NumLock),
+ Key::Numpad0 => Some(ImguiKey::Keypad0),
+ Key::Numpad1 => Some(ImguiKey::Keypad1),
+ Key::Numpad2 => Some(ImguiKey::Keypad2),
+ Key::Numpad3 => Some(ImguiKey::Keypad3),
+ Key::Numpad4 => Some(ImguiKey::Keypad4),
+ Key::Numpad5 => Some(ImguiKey::Keypad5),
+ Key::Numpad6 => Some(ImguiKey::Keypad6),
+ Key::Numpad7 => Some(ImguiKey::Keypad7),
+ Key::Numpad8 => Some(ImguiKey::Keypad8),
+ Key::Numpad9 => Some(ImguiKey::Keypad9),
+ Key::NumpadAdd => Some(ImguiKey::KeypadAdd),
+ Key::NumpadDecimal => Some(ImguiKey::KeypadDecimal),
+ Key::NumpadDivide => Some(ImguiKey::KeypadDivide),
+ Key::NumpadEnter => Some(ImguiKey::KeypadEnter),
+ Key::NumpadEqual => Some(ImguiKey::KeypadEqual),
+ Key::NumpadMultiply => Some(ImguiKey::KeypadMultiply),
+ Key::NumpadSubtract => Some(ImguiKey::KeypadSubtract),
+ Key::Escape => Some(ImguiKey::Escape),
+ Key::PrintScreen => Some(ImguiKey::PrintScreen),
+ Key::ScrollLock => Some(ImguiKey::ScrollLock),
+ Key::Pause => Some(ImguiKey::Pause),
+ Key::Meta => Some(ImguiKey::ModSuper),
+ Key::F1 => Some(ImguiKey::F1),
+ Key::F2 => Some(ImguiKey::F2),
+ Key::F3 => Some(ImguiKey::F3),
+ Key::F4 => Some(ImguiKey::F4),
+ Key::F5 => Some(ImguiKey::F5),
+ Key::F6 => Some(ImguiKey::F6),
+ Key::F7 => Some(ImguiKey::F7),
+ Key::F8 => Some(ImguiKey::F8),
+ Key::F9 => Some(ImguiKey::F9),
+ Key::F10 => Some(ImguiKey::F10),
+ Key::F11 => Some(ImguiKey::F11),
+ Key::F12 => Some(ImguiKey::F12),
+ _ => None,
+ }
+}
+
+fn mouse_button_to_imgui_mouse_button(
+ mouse_button: MouseButton,
+) -> Option<ImguiMouseButton>
+{
+ match mouse_button {
+ MouseButton::Left | MouseButton::Other(0) => Some(ImguiMouseButton::Left),
+ MouseButton::Right | MouseButton::Other(1) => Some(ImguiMouseButton::Right),
+ MouseButton::Middle | MouseButton::Other(2) => Some(ImguiMouseButton::Middle),
+ MouseButton::Other(3) => Some(ImguiMouseButton::Extra1),
+ MouseButton::Other(4) => Some(ImguiMouseButton::Extra2),
+ _ => None,
+ }
+}
+
+mod inner_context_wrapper
+{
+ use std::path::PathBuf;
+ use std::pin::Pin;
+ use std::ptr::null_mut;
+
+ pub struct InnerContextWrapper
+ {
+ ctx: Pin<Box<dear_imgui_rs::Context>>,
+ frame: *mut dear_imgui_rs::Ui,
+ _renderer_consumer: dear_imgui_rs::RendererConsumer,
+ }
+
+ impl InnerContextWrapper
+ {
+ pub fn new() -> Self
+ {
+ let mut ctx = Box::pin(dear_imgui_rs::Context::create());
+
+ let _ = ctx.set_platform_name(Some("engine-windowing"));
+ let _ = ctx.set_renderer_name(Some("engine-rendering"));
+
+ ctx.io_mut()
+ .set_backend_flags(dear_imgui_rs::BackendFlags::RENDERER_HAS_TEXTURES);
+
+ let renderer_consumer = ctx.create_renderer_consumer().unwrap();
+
+ Self {
+ ctx,
+ frame: null_mut(),
+ _renderer_consumer: renderer_consumer,
+ }
+ }
+
+ pub fn set_settings_ini_file_path(
+ &mut self,
+ path: Option<PathBuf>,
+ ) -> Result<(), dear_imgui_rs::ImGuiError>
+ {
+ self.ctx.set_ini_filename(path)
+ }
+
+ pub fn register_texture(
+ &mut self,
+ texture_data: dear_imgui_rs::texture::OwnedTextureData,
+ ) -> dear_imgui_rs::ManagedTextureId
+ {
+ self.ctx.register_texture(texture_data)
+ }
+
+ pub fn get_io_mut(&mut self) -> &mut dear_imgui_rs::Io
+ {
+ self.ctx.io_mut()
+ }
+
+ pub fn font_atlas(&self) -> &dear_imgui_rs::FontAtlas
+ {
+ self.ctx.font_atlas()
+ }
+
+ // pub fn create_renderer_consumer(&mut self) -> dear_imgui_rs::RendererConsumer
+ // {
+ // self.ctx.create_renderer_consumer().unwrap()
+ // }
+
+ pub fn render(&mut self) -> dear_imgui_rs::RenderedFrame<'_>
+ {
+ self.ctx.render()
+ }
+
+ pub fn new_frame(&mut self)
+ {
+ let frame = &raw mut *self.ctx.frame();
+
+ self.frame = frame;
+ }
+
+ pub fn get_frame(&mut self) -> Option<&mut dear_imgui_rs::Ui>
+ {
+ unsafe { self.frame.as_mut() }
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+struct TextureLookupId
+{
+ inner: u64,
+}
+
+impl TextureLookupId
+{
+ fn new() -> Self
+ {
+ static NEXT: AtomicU64 = AtomicU64::new(1);
+
+ Self {
+ inner: NEXT.fetch_add(1, Ordering::Relaxed),
+ }
+ }
+}
+
+impl intmap::IntKey for TextureLookupId
+{
+ type Int = u64;
+
+ const PRIME: Self::Int = <u64 as intmap::IntKey>::PRIME;
+
+ fn into_int(self) -> Self::Int
+ {
+ self.inner
+ }
+}
+
+impl From<dear_imgui_rs::TextureId> for TextureLookupId
+{
+ fn from(texture_id: dear_imgui_rs::TextureId) -> Self
+ {
+ assert!(!texture_id.is_null(), "Bad texture ID: is null");
+
+ Self { inner: texture_id.id() }
+ }
+}
+
+impl From<TextureLookupId> for dear_imgui_rs::TextureId
+{
+ fn from(texture_id: TextureLookupId) -> Self
+ {
+ Self::new(texture_id.inner)
+ }
+}
diff --git a/engine/src/ui/view.rs b/engine/src/ui/view.rs
new file mode 100644
index 0000000..42601a3
--- /dev/null
+++ b/engine/src/ui/view.rs
@@ -0,0 +1,2 @@
+pub mod transform_3d;
+pub mod world;
diff --git a/engine/src/ui/view/transform_3d.rs b/engine/src/ui/view/transform_3d.rs
new file mode 100644
index 0000000..9c3dbf1
--- /dev/null
+++ b/engine/src/ui/view/transform_3d.rs
@@ -0,0 +1,128 @@
+use dear_imguizmo::GuizmoExt;
+
+use crate::camera::{Active as ActiveCamera, Camera};
+use crate::data_types::dimens::Dimens3;
+use crate::ecs::pair::ChildOf;
+use crate::ecs::query::term::{Traverse, TraverseUp, With, Without};
+use crate::ecs::sole::Single;
+use crate::ecs::Query;
+use crate::projection::ClipVolume as ProjectionClipVolume;
+use crate::scene::{Active as ActiveScene, Scene};
+use crate::transform::Transform;
+use crate::ui::dear_imgui::{
+ Context as DearImguiContext,
+ TargetWindow as DearImguiTargetWindow,
+};
+use crate::vector::Vec3;
+use crate::windowing::window::{Closed as WindowClosed, Window};
+
+pub fn show(
+ subject_query: Query<(
+ &mut Transform,
+ Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>,
+ Without<ActiveCamera>,
+ )>,
+ camera_query: Query<(
+ &Camera,
+ &Transform,
+ Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>,
+ With<ActiveCamera>,
+ )>,
+ target_window_query: Query<(
+ &Window,
+ With<DearImguiTargetWindow>,
+ Without<WindowClosed>,
+ )>,
+ mut dear_imgui_context: Single<DearImguiContext>,
+) -> Result<(), crate::Error>
+{
+ let dear_imgui_context = dear_imgui_context.get_mut()?;
+
+ let Some(frame) = dear_imgui_context.frame() else {
+ return Ok(());
+ };
+
+ let Some((camera, camera_world_pos)) = camera_query.iter().next() else {
+ return Ok(());
+ };
+
+ let Some((target_window,)) = target_window_query.iter().next() else {
+ return Ok(());
+ };
+
+ let gizmo = frame.guizmo();
+
+ let [display_width, display_height] = frame.io().display_size();
+
+ gizmo.set_rect(0.0, 0.0, display_width, display_height);
+
+ // The widgets should appear under dear imgui windows
+ gizmo.set_drawlist_background();
+
+ let col_major_view = flatten_matrix(
+ camera
+ .to_view_matrix(camera_world_pos.position)
+ .to_column_major(),
+ );
+
+ let col_major_projection = flatten_matrix(
+ camera
+ .projection
+ .to_matrix_rh(target_window.inner_size, ProjectionClipVolume::NegOneToOne)
+ .to_column_major(),
+ );
+
+ for (subject_index, (mut subject_transform,)) in subject_query.iter().enumerate() {
+ let subject_index = match i32::try_from(subject_index) {
+ Ok(subject_index) => subject_index,
+ Err(err) => {
+ tracing::error!("Invalid subject index {subject_index}: {err}");
+ break;
+ }
+ };
+
+ // No rotation
+ let rotation = [0.0, 0.0, 0.0];
+
+ let mut col_major_model_mat = dear_imguizmo::recompose_matrix(
+ &subject_transform.position.into(),
+ &rotation,
+ &subject_transform.scale.into(),
+ );
+
+ let _id_token = gizmo.push_id(subject_index);
+
+ let used = gizmo
+ .manipulate_config(
+ &col_major_view,
+ &col_major_projection,
+ &mut col_major_model_mat,
+ )
+ .operation(
+ dear_imguizmo::Operation::TRANSLATE | dear_imguizmo::Operation::SCALE,
+ )
+ .mode(dear_imguizmo::Mode::World)
+ .build();
+
+ if used {
+ let (translation, _rotation, scale) =
+ dear_imguizmo::decompose_matrix(&col_major_model_mat);
+
+ subject_transform.position = Vec3::from(translation);
+ subject_transform.scale = Dimens3::from(scale);
+ }
+ }
+
+ Ok(())
+}
+
+fn flatten_matrix(mat: [[f32; 4]; 4]) -> [f32; 4 * 4]
+{
+ let flattened = mat.as_flattened();
+
+ let Some(flattened) = flattened.as_array::<{ 4 * 4 }>() else {
+ unreachable!();
+ };
+
+ *flattened
+}
diff --git a/engine/src/ui/view/world.rs b/engine/src/ui/view/world.rs
new file mode 100644
index 0000000..95f29d0
--- /dev/null
+++ b/engine/src/ui/view/world.rs
@@ -0,0 +1,2316 @@
+use std::any::{Any, TypeId};
+use std::borrow::Cow;
+use std::io::Cursor;
+use std::ops::{Deref, DerefMut};
+
+use crate::color::{Rgb, Rgba};
+use crate::ecs::actions::Actions;
+use crate::ecs::component::local::Local;
+use crate::ecs::component::{
+ Handle as ComponentHandle,
+ Info as ComponentInfo,
+ Parts as ComponentParts,
+};
+use crate::ecs::entity::{Handle as EntityHandle, Name as EntityName};
+use crate::ecs::sole::Single;
+use crate::ecs::uid::Uid;
+use crate::ecs::util::Either;
+use crate::ecs::{Component, Query, World};
+use crate::image::{Format as ImageFormat, Image};
+use crate::reflection::{
+ Enum as EnumReflection,
+ EnumVariant as EnumVariantReflection,
+ GetError as ReflectionGetError,
+ LiteralType,
+ Struct as StructReflection,
+ Type as TypeReflection,
+ Visibility,
+};
+use crate::ui::dear_imgui::{
+ bindings as dear_imgui_bindings,
+ Context as DearImguiContext,
+};
+
+const BUTTON_RED_NORMAL: [f32; 4] = [124.0, 52.0, 39.0, 255.0];
+const BUTTON_RED_HOVERED: [f32; 4] = [181.0, 76.0, 57.0, 255.0];
+const BUTTON_RED_ACTIVE: [f32; 4] = [162.0, 68.0, 51.0, 255.0];
+
+#[derive(Default, Component)]
+pub struct State
+{
+ spawning_entity: Option<Uid>,
+ popup: Option<PopupState>,
+ icon_textures: Option<IconTextures>,
+}
+
+pub fn show(
+ query: Query<()>,
+ mut dear_imgui_context: Single<DearImguiContext>,
+ mut state: Local<State>,
+ world: &World,
+ mut actions: Actions,
+) -> Result<(), crate::Error>
+{
+ let dear_imgui_context = dear_imgui_context.get_mut()?;
+
+ let State {
+ spawning_entity,
+ popup: popup_state,
+ icon_textures,
+ } = &mut *state;
+
+ let icon_textures =
+ icon_textures.get_or_insert_with(|| IconTextures::new(dear_imgui_context));
+
+ let Some(frame) = dear_imgui_context.frame() else {
+ return Ok(());
+ };
+
+ frame
+ .window("World")
+ .size([600.0, 400.0], dear_imgui_bindings::Condition::Once)
+ .build(|| {
+ create_spawn_button_widgets(
+ frame,
+ spawning_entity,
+ popup_state,
+ world,
+ &mut actions,
+ );
+
+ frame.spacing();
+ frame.spacing();
+
+ for ent_handle in &query.into_flexible_query() {
+ create_entity_widgets(
+ frame,
+ popup_state,
+ icon_textures,
+ &ent_handle,
+ world,
+ &mut actions,
+ );
+ }
+
+ let popup_closed = match popup_state {
+ Some(PopupState::AddComponent(popup_state)) => {
+ show_add_component_popup(frame, popup_state, &mut actions)
+ }
+ Some(PopupState::RenameEntity(popup_state)) => {
+ show_rename_entity_popup(frame, popup_state, &mut actions, world)
+ }
+ _ => false,
+ };
+
+ if popup_closed {
+ *popup_state = None;
+ }
+ });
+
+ Ok(())
+}
+
+fn create_texture(
+ dear_imgui_context: &mut DearImguiContext,
+ image: Image,
+) -> dear_imgui_bindings::ManagedTextureId
+{
+ let image = image.into_rgba8();
+
+ let mut texture_data = dear_imgui_bindings::OwnedTextureData::new();
+
+ texture_data.create(
+ dear_imgui_bindings::TextureFormat::RGBA32,
+ image.dimensions().width,
+ image.dimensions().height,
+ );
+
+ texture_data.set_data(image.as_bytes());
+
+ dear_imgui_context.register_texture(texture_data)
+}
+
+fn create_spawn_button_widgets(
+ frame: &dear_imgui_bindings::Ui,
+ spawning_entity: &mut Option<Uid>,
+ popup_state: &mut Option<PopupState>,
+ world: &World,
+ actions: &mut Actions,
+)
+{
+ let _disabled_token = frame.begin_disabled_with_cond(spawning_entity.is_some());
+
+ if frame.button("Spawn") {
+ let new_ent_id = actions.spawn(());
+
+ *spawning_entity = Some(new_ent_id);
+ }
+
+ if let Some(spawning_ent) = &spawning_entity {
+ if let Some(spawned_ent) = world.get_entity(*spawning_ent) {
+ let spawned_ent_title = create_entity_title(&spawned_ent);
+
+ if enter_add_component_popup_state(
+ popup_state,
+ &spawned_ent,
+ &spawned_ent_title,
+ world,
+ ) {
+ *spawning_entity = None;
+ }
+ }
+ }
+}
+
+fn create_entity_widgets(
+ frame: &dear_imgui_bindings::Ui,
+ popup_state: &mut Option<PopupState>,
+ icon_textures: &IconTextures,
+ ent_handle: &EntityHandle,
+ world: &World,
+ actions: &mut Actions,
+)
+{
+ if ent_handle.component_ids().next().is_some()
+ && !ent_handle.component_ids().any(|comp_id| {
+ world.get_entity(comp_id).is_some_and(|comp_ent| {
+ comp_ent
+ .get::<ComponentInfo>()
+ .is_some_and(|comp_info| comp_info.type_reflection.is_some())
+ })
+ })
+ {
+ return;
+ }
+
+ let ent_title = create_entity_title(ent_handle);
+
+ let mut is_open = false;
+
+ frame
+ .table(format!("ent_{}_table", ent_handle.uid()))
+ .headers(false)
+ .sizing_policy(dear_imgui_bindings::TableSizingPolicy::FixedFit)
+ .columns([
+ dear_imgui_bindings::TableColumnSetup::new("a")
+ .fixed_width(frame.content_region_avail_width() * 0.8),
+ dear_imgui_bindings::TableColumnSetup::new("b"),
+ dear_imgui_bindings::TableColumnSetup::new("c"),
+ dear_imgui_bindings::TableColumnSetup::new("d"),
+ ])
+ .build(|frame| {
+ frame.table_next_row();
+
+ frame.table_next_column();
+
+ is_open = frame
+ .collapsing_header(&ent_title, dear_imgui_bindings::TreeNodeFlags::NONE);
+
+ frame.table_next_column();
+
+ create_add_component_button_widget(
+ frame,
+ ent_handle,
+ &ent_title,
+ popup_state,
+ world,
+ );
+
+ frame.table_next_column();
+
+ create_despawn_button_widget(frame, icon_textures, ent_handle, actions);
+
+ frame.table_next_column();
+
+ create_rename_entity_button_widget(
+ frame,
+ icon_textures,
+ ent_handle,
+ &ent_title,
+ popup_state,
+ );
+ });
+
+ if is_open {
+ for component_id in ent_handle.component_ids() {
+ if component_id.is_pair() {
+ create_pair_component_widgets(
+ frame,
+ icon_textures,
+ ent_handle,
+ component_id,
+ world,
+ actions,
+ );
+
+ frame.spacing();
+
+ continue;
+ }
+
+ create_component_widgets(frame, ent_handle, component_id, world, actions);
+ }
+ }
+}
+
+fn create_component_widgets(
+ frame: &dear_imgui_bindings::Ui,
+ ent_handle: &EntityHandle,
+ component_id: Uid,
+ world: &World,
+ actions: &mut Actions,
+)
+{
+ let Some(component_info) = world
+ .get_entity(component_id)
+ .and_then(|comp_ent| comp_ent.get::<ComponentInfo>())
+ else {
+ return;
+ };
+
+ let Some(component_type) = component_info.type_reflection else {
+ return;
+ };
+
+ let Some(mut component) = ent_handle.get_any_by_id_mut(component_id) else {
+ unreachable!();
+ };
+
+ {
+ let _color_token = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::Button,
+ BUTTON_RED_NORMAL.map(|num| num / 255.0),
+ );
+
+ let _color_token_b = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonHovered,
+ BUTTON_RED_HOVERED.map(|num| num / 255.0),
+ );
+
+ let _color_token_c = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonActive,
+ BUTTON_RED_ACTIVE.map(|num| num / 255.0),
+ );
+
+ if frame.small_button(format!(
+ "X##world_view_entity_{}_remove_component_button_{}",
+ ent_handle.uid(),
+ component_info.name
+ )) {
+ actions.remove_components(ent_handle.uid(), [component_id]);
+ }
+ }
+
+ frame.same_line();
+
+ let mut component_changed = false;
+
+ create_item_title_widget(
+ frame,
+ component_info.name,
+ &ItemType::Reflected(component_type),
+ None,
+ );
+
+ let item_type = ItemType::Reflected(component_type);
+
+ let _token = item_type.indent_or_same_line(frame);
+
+ add_item_to_frame(
+ frame,
+ ItemRef::Mutable(&mut *component),
+ &mut component_changed,
+ ItemInfo {
+ title: component_info.name,
+ tag: &format!(
+ "world_view_entity_{}_component_{}",
+ ent_handle.uid(),
+ component_info.name,
+ ),
+ type_name: None,
+ ty: item_type,
+ is_read_only: false,
+ },
+ &[],
+ );
+
+ frame.spacing();
+
+ if component_changed {
+ component.set_changed();
+ }
+}
+
+fn create_pair_component_widgets(
+ frame: &dear_imgui_bindings::Ui,
+ icon_textures: &IconTextures,
+ ent_handle: &EntityHandle,
+ pair_id: Uid,
+ world: &World,
+ actions: &mut Actions,
+)
+{
+ {
+ let _color_token = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::Button,
+ BUTTON_RED_NORMAL.map(|num| num / 255.0),
+ );
+
+ let _color_token_b = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonHovered,
+ BUTTON_RED_HOVERED.map(|num| num / 255.0),
+ );
+
+ let _color_token_c = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonActive,
+ BUTTON_RED_ACTIVE.map(|num| num / 255.0),
+ );
+
+ if frame.small_button(format!(
+ "X##world_view_entity_{}_remove_component_button_{}",
+ ent_handle.uid(),
+ pair_id
+ )) {
+ actions.remove_components(ent_handle.uid(), [pair_id]);
+ }
+ }
+
+ frame.same_line();
+
+ let relation_comp_info_or_ent_name =
+ get_comp_info_or_ent_name(world, pair_id.relation());
+
+ let relation_name: Cow<'_, str> = match &relation_comp_info_or_ent_name {
+ Some(Either::A(comp_info)) => comp_info.name.into(),
+ Some(Either::B(ent_name)) => format!("🌐 {}", ent_name.name).into(),
+ None => format!("<unnamed> {}", pair_id.relation()).into(),
+ };
+
+ let target_comp_info_or_ent_name = get_comp_info_or_ent_name(world, pair_id.target());
+
+ let target_name: Cow<'_, str> = match &target_comp_info_or_ent_name {
+ Some(Either::A(comp_info)) => comp_info.name.into(),
+ Some(Either::B(ent_name)) => format!("🌐 {}", ent_name.name).into(),
+ None => format!("<unnamed> {}", pair_id.target()).into(),
+ };
+
+ // TODO: Make relation & target ids editable
+
+ frame.text(format!("({relation_name}, {target_name})"));
+
+ let Some(mut pair_data) = ent_handle.get_any_by_id_mut(pair_id) else {
+ unreachable!();
+ };
+
+ let pair_data_ty_id = (*pair_data).type_id();
+
+ let Some(pair_data_comp_info) = [
+ &relation_comp_info_or_ent_name,
+ &target_comp_info_or_ent_name,
+ ]
+ .map(Option::as_ref)
+ .map(|item| item.and_then(Either::as_a))
+ .into_iter()
+ .flatten()
+ .find(|comp_info| comp_info.ty_id == pair_data_ty_id) else {
+ return;
+ };
+
+ let Some(pair_data_ty) = pair_data_comp_info.type_reflection else {
+ frame.same_line();
+
+ frame.image(icon_textures.warning, [16.0, 16.0]);
+
+ if frame.is_item_hovered() {
+ frame.tooltip_text(format!(
+ "Pair data type {} does not have reflection",
+ pair_data_comp_info.name
+ ));
+ }
+
+ return;
+ };
+
+ let _indent = frame.begin_indent();
+
+ let mut component_changed = false;
+
+ create_item_title_widget(
+ frame,
+ pair_data_comp_info.name,
+ &ItemType::Reflected(pair_data_ty),
+ None,
+ );
+
+ let item_type = ItemType::Reflected(pair_data_ty);
+
+ let _token = item_type.indent_or_same_line(frame);
+
+ add_item_to_frame(
+ frame,
+ ItemRef::Mutable(&mut *pair_data),
+ &mut component_changed,
+ ItemInfo {
+ title: pair_data_comp_info.name,
+ tag: &format!(
+ "world_view_entity_{}_pair_component_{pair_id}",
+ ent_handle.uid(),
+ ),
+ type_name: None,
+ ty: item_type,
+ is_read_only: false,
+ },
+ &[],
+ );
+
+ // The pair component is not set as changed here since events do not support
+ // pairs as event targets
+}
+
+fn get_comp_info_or_ent_name(
+ world: &World,
+ id: Uid,
+) -> Option<Either<ComponentHandle<'_, ComponentInfo>, ComponentHandle<'_, EntityName>>>
+{
+ let ent = world.get_entity(id)?;
+
+ if let Some(component_info) = ent.get::<ComponentInfo>() {
+ return Some(Either::A(component_info));
+ }
+
+ if let Some(entity_name) = ent.get::<EntityName>() {
+ return Some(Either::B(entity_name));
+ }
+
+ None
+}
+
+fn create_add_component_button_widget(
+ frame: &dear_imgui_bindings::Ui,
+ ent_handle: &EntityHandle,
+ ent_title: &str,
+ popup_state: &mut Option<PopupState>,
+ world: &World,
+)
+{
+ let _color_token = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::Button,
+ [49.0, 89.0, 28.0, 255.0].map(|num| num / 255.0),
+ );
+
+ let _color_token_b = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonHovered,
+ [100.0, 181.0, 57.0, 255.0].map(|num| num / 255.0),
+ );
+
+ let _color_token_c = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonActive,
+ [90.0, 162.0, 51.0, 255.0].map(|num| num / 255.0),
+ );
+
+ let is_button_clicked = frame.button(format!(
+ "+##add_component_to_entity_button_{}",
+ ent_handle.uid()
+ ));
+
+ if is_button_clicked {
+ enter_add_component_popup_state(popup_state, ent_handle, ent_title, world);
+ }
+}
+
+fn create_despawn_button_widget(
+ frame: &dear_imgui_bindings::Ui,
+ icon_textures: &IconTextures,
+ ent_handle: &EntityHandle,
+ actions: &mut Actions,
+)
+{
+ let _color_token = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::Button,
+ BUTTON_RED_NORMAL.map(|num| num / 255.0),
+ );
+
+ let _color_token_b = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonHovered,
+ BUTTON_RED_HOVERED.map(|num| num / 255.0),
+ );
+
+ let _color_token_c = frame.push_style_color(
+ dear_imgui_bindings::StyleColor::ButtonActive,
+ BUTTON_RED_ACTIVE.map(|num| num / 255.0),
+ );
+
+ if frame
+ .image_button_config(
+ format!("ent_{}_despawn_button", ent_handle.uid()),
+ icon_textures.despawn,
+ [14.0, 14.0],
+ )
+ .build()
+ {
+ actions.despawn(ent_handle.uid());
+ }
+}
+
+fn create_rename_entity_button_widget(
+ frame: &dear_imgui_bindings::Ui,
+ icon_textures: &IconTextures,
+ ent_handle: &EntityHandle,
+ ent_title: &str,
+ popup_state: &mut Option<PopupState>,
+)
+{
+ if frame
+ .image_button_config(
+ format!("ent_{}_rename_button", ent_handle.uid()),
+ icon_textures.edit,
+ [16.0, 16.0],
+ )
+ .build()
+ {
+ enter_rename_entity_popup_state(popup_state, ent_handle, ent_title);
+ }
+}
+
+fn enter_rename_entity_popup_state(
+ popup_state: &mut Option<PopupState>,
+ ent_handle: &EntityHandle,
+ ent_title: &str,
+) -> bool
+{
+ if popup_state.is_some() {
+ return false;
+ }
+
+ *popup_state = Some(PopupState::RenameEntity(RenameEntityPopupState {
+ popup_name: format!("Rename entity {ent_title}"),
+ target_entity_id: ent_handle.uid(),
+ new_entity_name: String::with_capacity(32),
+ }));
+
+ true
+}
+
+fn show_rename_entity_popup(
+ frame: &dear_imgui_bindings::Ui,
+ popup_state: &mut RenameEntityPopupState,
+ actions: &mut Actions,
+ world: &World,
+) -> bool
+{
+ frame.open_popup(&popup_state.popup_name);
+
+ let mut is_opened = true;
+
+ let should_close =
+ frame.modal_popup_with_opened(&popup_state.popup_name, &mut is_opened, || {
+ frame.set_window_size([
+ frame.calc_text_size(&popup_state.popup_name)[0] + 32.0,
+ 120.0,
+ ]);
+
+ frame.spacing();
+ frame.spacing();
+
+ frame.text("Name:");
+ frame.same_line();
+
+ frame
+ .input_text(
+ "##rename_entity_popup_new_name_input",
+ &mut popup_state.new_entity_name,
+ )
+ .build();
+
+ frame.spacing();
+ frame.spacing();
+
+ let rename_text_size = frame.calc_text_size("Rename");
+
+ if frame.button_with_size(
+ "Rename##rename_entity_popup_rename_button",
+ [rename_text_size[0] * 2.0, rename_text_size[1] * 2.0],
+ ) {
+ let Some(target_ent) = world.get_entity(popup_state.target_entity_id)
+ else {
+ tracing::error!(
+ entity = ?popup_state.target_entity_id,
+ "Cannot rename entity: entity no longer exists"
+ );
+ return true;
+ };
+
+ if let Some(mut target_ent_name) = target_ent.get_mut::<EntityName>() {
+ target_ent_name.name = popup_state.new_entity_name.clone().into();
+ } else {
+ actions.add_components(
+ popup_state.target_entity_id,
+ (EntityName {
+ name: popup_state.new_entity_name.clone().into(),
+ },),
+ );
+ }
+
+ return true;
+ }
+
+ false
+ });
+
+ if let Some(true) = should_close {
+ return true;
+ }
+
+ if !is_opened {
+ return true;
+ }
+
+ false
+}
+
+fn enter_add_component_popup_state(
+ popup_state: &mut Option<PopupState>,
+ ent_handle: &EntityHandle,
+ ent_title: &str,
+ world: &World,
+) -> bool
+{
+ if popup_state.is_some() {
+ return false;
+ }
+
+ let popup_name = format!("Add component to {ent_title}");
+
+ let mut searchable_components = world
+ .query::<(&ComponentInfo,)>()
+ .iter_with_euids()
+ .map(|(id, (component_info,))| {
+ let user_creatable = if component_info.is_sole {
+ ComponentUserCreatable::No { reason: "is a sole" }
+ } else if component_info.type_reflection.is_none() {
+ ComponentUserCreatable::No { reason: "no type reflection" }
+ } else if component_info
+ .type_reflection
+ .is_some_and(|ty| !ty.has_default_value())
+ {
+ ComponentUserCreatable::No { reason: "no default value" }
+ } else if !get_whole_type_is_user_editable(
+ component_info.type_reflection,
+ component_info.ty_id,
+ ) {
+ ComponentUserCreatable::No {
+ reason: "whole component is not user editable",
+ }
+ } else {
+ ComponentUserCreatable::Yes
+ };
+
+ (id, component_info.clone(), user_creatable)
+ })
+ .collect::<Vec<_>>();
+
+ searchable_components.sort_by_key(|(_, _, user_creatable)| *user_creatable);
+
+ *popup_state = Some(PopupState::AddComponent(AddComponentPopupState {
+ popup_name: popup_name.clone(),
+ target_entity_id: ent_handle.uid(),
+ search_text: String::with_capacity(20),
+ searched_components: searchable_components.clone(),
+ searchable_components,
+ selected_search_result: None,
+ new_component: None,
+ }));
+
+ true
+}
+
+fn show_add_component_popup(
+ frame: &dear_imgui_bindings::Ui,
+ add_component_popup_state: &mut AddComponentPopupState,
+ actions: &mut Actions,
+) -> bool
+{
+ frame.open_popup(&add_component_popup_state.popup_name);
+
+ let mut is_opened = true;
+
+ let should_close = frame.modal_popup_with_opened(
+ &add_component_popup_state.popup_name,
+ &mut is_opened,
+ || {
+ frame.set_window_size([700.0, 600.0]);
+
+ frame.spacing();
+ frame.spacing();
+
+ frame.text("Search:");
+ frame.same_line();
+
+ if frame
+ .input_text(
+ "##add_component_popup_search_input",
+ &mut add_component_popup_state.search_text,
+ )
+ .build()
+ {
+ add_component_popup_state.searched_components = add_component_popup_state
+ .searchable_components
+ .iter()
+ .filter(|&(_, searchable_comp_info, _)| {
+ searchable_comp_info
+ .name
+ .contains(&add_component_popup_state.search_text)
+ })
+ .cloned()
+ .collect();
+
+ add_component_popup_state.selected_search_result = None;
+ }
+
+ frame.spacing();
+ frame.spacing();
+
+ let _item_width_token = frame.push_item_width(-1.0);
+
+ if let Some(selected_search_result_index) = frame
+ .list_box_config("##add_component_popup_search_results")
+ .build_extended(
+ frame,
+ &mut add_component_popup_state.selected_search_result,
+ &add_component_popup_state.searched_components,
+ &|(_, comp_info, comp_user_creatable)| ListBoxItem {
+ label: comp_info.name.into(),
+ disabled: matches!(
+ comp_user_creatable,
+ ComponentUserCreatable::No { .. }
+ ),
+ hover_tooltip: match comp_user_creatable {
+ ComponentUserCreatable::Yes => None,
+ ComponentUserCreatable::No { reason } => Some(
+ format!("Component cannot be selected: {reason}").into(),
+ ),
+ },
+ },
+ )
+ {
+ let (selected_comp_id, selected_comp_info, _) = add_component_popup_state
+ .searched_components[selected_search_result_index]
+ .clone();
+
+ let Some(selected_component_type) = selected_comp_info.type_reflection
+ else {
+ unreachable!();
+ };
+
+ let Some(selected_comp_default_value) =
+ selected_component_type.default_value()
+ else {
+ unreachable!(); // Only components with default values are selectable
+ };
+
+ add_component_popup_state.new_component = Some((
+ selected_comp_default_value,
+ selected_comp_info,
+ selected_comp_id,
+ ));
+ }
+
+ frame.spacing();
+ frame.spacing();
+ frame.spacing();
+
+ if let Some((new_component, new_component_info, _)) =
+ add_component_popup_state.new_component.as_mut()
+ {
+ let mut component_changed = false;
+
+ let Some(new_component_type) = new_component_info.type_reflection else {
+ unreachable!();
+ };
+
+ create_item_title_widget(
+ frame,
+ new_component_info.name,
+ &ItemType::Reflected(new_component_type),
+ None,
+ );
+
+ let item_type = ItemType::Reflected(new_component_type);
+
+ let _token = item_type.indent_or_same_line(frame);
+
+ add_item_to_frame(
+ frame,
+ ItemRef::Mutable(&mut **new_component),
+ &mut component_changed,
+ ItemInfo {
+ title: new_component_info.name,
+ tag: &format!(
+ "add_component_popup_component_{}",
+ new_component_info.name,
+ ),
+ type_name: None,
+ ty: item_type,
+ is_read_only: false,
+ },
+ &[],
+ );
+
+ frame.spacing();
+ frame.spacing();
+ frame.spacing();
+ }
+
+ let _disabled = frame.begin_disabled_with_cond(
+ add_component_popup_state.new_component.is_none(),
+ );
+
+ let add_text_size = frame.calc_text_size("Add");
+
+ if frame.button_with_size(
+ "Add##add_component_popup_add_button",
+ [add_text_size[0] * 2.0, add_text_size[1] * 2.0],
+ ) {
+ if let Some((new_component_data, new_component_info, new_component_id)) =
+ add_component_popup_state.new_component.take()
+ {
+ actions.add_components(
+ add_component_popup_state.target_entity_id,
+ (ComponentParts::from_component_info(
+ &new_component_info,
+ new_component_id,
+ new_component_data,
+ ),),
+ );
+
+ return true;
+ }
+ }
+
+ false
+ },
+ );
+
+ if let Some(true) = should_close {
+ return true;
+ }
+
+ if !is_opened {
+ return true;
+ }
+
+ false
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum ComponentUserCreatable
+{
+ Yes,
+ No
+ {
+ reason: &'static str,
+ },
+}
+
+struct ItemInfo<'a>
+{
+ title: &'a str,
+ tag: &'a str,
+ type_name: Option<&'static str>,
+ ty: ItemType,
+ is_read_only: bool,
+}
+
+enum ItemType
+{
+ Reflected(&'static TypeReflection),
+ String,
+ CowStr,
+ Color(ColorItemType, LiteralType),
+}
+
+impl ItemType
+{
+ fn spans_multiple_rows(&self) -> bool
+ {
+ match self {
+ Self::Reflected(
+ TypeReflection::Struct(_)
+ | TypeReflection::Array(_)
+ | TypeReflection::Slice(_),
+ ) => true,
+ Self::Reflected(TypeReflection::Enum(enum_ty)) => !enum_ty.is_unit_only,
+ Self::Reflected(TypeReflection::Reference(ref_ty)) => {
+ ItemType::Reflected(ref_ty.ty).spans_multiple_rows()
+ }
+ Self::Reflected(TypeReflection::Literal(_))
+ | Self::String
+ | Self::CowStr
+ | Self::Color(..) => false,
+ Self::Reflected(_) => unimplemented!(),
+ }
+ }
+
+ fn indent_or_same_line<'frame>(
+ &self,
+ frame: &'frame dear_imgui_bindings::Ui,
+ ) -> Option<dear_imgui_bindings::IndentToken<'frame>>
+ {
+ if self.spans_multiple_rows() {
+ return Some(frame.begin_indent());
+ }
+
+ frame.same_line();
+
+ None
+ }
+}
+
+#[derive(Debug)]
+enum ColorItemType
+{
+ Rgb,
+ Rgba,
+}
+
+fn get_item_type(ty: Option<&'static TypeReflection>, type_id: TypeId)
+ -> Option<ItemType>
+{
+ if let Some(TypeReflection::Struct(_)) = ty {
+ if type_id == TypeId::of::<Rgb<f32>>() {
+ return Some(ItemType::Color(ColorItemType::Rgb, LiteralType::F32));
+ }
+ if type_id == TypeId::of::<Rgb<u8>>() {
+ return Some(ItemType::Color(ColorItemType::Rgb, LiteralType::U8));
+ }
+
+ if type_id == TypeId::of::<Rgba<f32>>() {
+ return Some(ItemType::Color(ColorItemType::Rgba, LiteralType::F32));
+ }
+ if type_id == TypeId::of::<Rgba<u8>>() {
+ return Some(ItemType::Color(ColorItemType::Rgba, LiteralType::U8));
+ }
+ }
+
+ if let Some(ty) = ty {
+ Some(ItemType::Reflected(ty))
+ } else if type_id == TypeId::of::<String>() {
+ Some(ItemType::String)
+ } else if type_id == TypeId::of::<Cow<'static, str>>() {
+ Some(ItemType::CowStr)
+ } else {
+ None
+ }
+}
+
+enum ItemRef<'item>
+{
+ Mutable(&'item mut dyn Any),
+ Immutable(&'item dyn Any),
+}
+
+impl<'item> ItemRef<'item>
+{
+ fn get_struct_field(
+ &mut self,
+ ty: &StructReflection,
+ field_index: usize,
+ ) -> Result<ItemRef<'_>, ReflectionGetError>
+ {
+ match self {
+ ItemRef::Mutable(item) => {
+ (ty.try_get_field_mut)(*item, field_index).map(ItemRef::Mutable)
+ }
+ ItemRef::Immutable(item) => {
+ (ty.try_get_field)(*item, field_index).map(ItemRef::Immutable)
+ }
+ }
+ }
+
+ fn get_enum_variant_field(
+ &mut self,
+ variant: &EnumVariantReflection,
+ field_index: usize,
+ ) -> Result<ItemRef<'_>, ReflectionGetError>
+ {
+ match self {
+ ItemRef::Mutable(item) => {
+ (variant.try_get_field_mut)(*item, field_index).map(ItemRef::Mutable)
+ }
+ ItemRef::Immutable(item) => {
+ (variant.try_get_field)(*item, field_index).map(ItemRef::Immutable)
+ }
+ }
+ }
+
+ fn into_writable<T: Clone + 'static>(self) -> OwnedOrRefMut<'item, T>
+ {
+ match self {
+ ItemRef::Mutable(item) => OwnedOrRefMut::Ref(
+ (*item)
+ .downcast_mut::<T>()
+ .expect("Cannot downcast: wrong item type"),
+ ),
+ ItemRef::Immutable(item) => OwnedOrRefMut::Owned(
+ (*item)
+ .downcast_ref::<T>()
+ .expect("Cannot downcast: wrong item type")
+ .clone(),
+ ),
+ }
+ }
+}
+
+impl AsRef<dyn Any> for ItemRef<'_>
+{
+ fn as_ref(&self) -> &dyn Any
+ {
+ match self {
+ Self::Mutable(item) => &**item,
+ Self::Immutable(item) => *item,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum OwnedOrRefMut<'a, Value>
+{
+ Ref(&'a mut Value),
+ Owned(Value),
+}
+
+impl<Value> Deref for OwnedOrRefMut<'_, Value>
+{
+ type Target = Value;
+
+ fn deref(&self) -> &Self::Target
+ {
+ match self {
+ Self::Ref(value) => value,
+ Self::Owned(value) => value,
+ }
+ }
+}
+
+impl<Value> DerefMut for OwnedOrRefMut<'_, Value>
+{
+ fn deref_mut(&mut self) -> &mut Self::Target
+ {
+ match self {
+ Self::Ref(value) => value,
+ Self::Owned(value) => value,
+ }
+ }
+}
+
+fn create_item_title_widget(
+ frame: &dear_imgui_bindings::Ui,
+ item_title: &str,
+ item_type: &ItemType,
+ item_type_name: Option<&str>,
+)
+{
+ if !matches!(item_type, ItemType::Reflected(TypeReflection::Reference(_))) {
+ frame.text(item_title);
+
+ if let Some(item_type_name) = item_type_name {
+ if frame.is_item_hovered() {
+ frame.tooltip_text(item_type_name);
+ }
+ }
+ }
+}
+
+fn add_item_to_frame<'a>(
+ frame: &dear_imgui_bindings::Ui,
+ mut item: ItemRef<'_>,
+ data_changed: &mut bool,
+ ItemInfo {
+ title: item_title,
+ tag: item_tag,
+ type_name: item_type_name,
+ ty: item_type,
+ is_read_only: item_is_read_only,
+ }: ItemInfo<'a>,
+ prev_item_tags: &[&'a str],
+)
+{
+ let _disabled = frame.begin_disabled_with_cond(
+ !matches!(
+ item_type,
+ ItemType::Reflected(
+ TypeReflection::Struct(_)
+ | TypeReflection::Enum(EnumReflection { is_unit_only: false, .. })
+ | TypeReflection::Array(_)
+ | TypeReflection::Slice(_)
+ )
+ ) && item_is_read_only,
+ );
+
+ match item_type {
+ ItemType::Reflected(TypeReflection::Struct(struct_ty)) => {
+ create_struct_widgets(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ item_is_read_only,
+ struct_ty,
+ prev_item_tags,
+ );
+ }
+ ItemType::Reflected(TypeReflection::Enum(enum_type)) => {
+ create_enum_widgets(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ item_is_read_only,
+ enum_type,
+ prev_item_tags,
+ );
+ }
+ ItemType::Reflected(TypeReflection::Literal(literal_reflection)) => {
+ match literal_reflection.ty {
+ LiteralType::U8 => create_scalar_item_input::<u8>(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ prev_item_tags,
+ ),
+ LiteralType::I8 => create_scalar_item_input::<i8>(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ prev_item_tags,
+ ),
+ LiteralType::U16 => create_scalar_item_input::<u16>(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ prev_item_tags,
+ ),
+ LiteralType::I16 => create_scalar_item_input::<i16>(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ prev_item_tags,
+ ),
+ LiteralType::U32 => create_scalar_item_input::<u32>(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ prev_item_tags,
+ ),
+ LiteralType::I32 => create_scalar_item_input::<i32>(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ prev_item_tags,
+ ),
+ LiteralType::U64 => create_scalar_item_input::<u64>(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ prev_item_tags,
+ ),
+ LiteralType::I64 => create_scalar_item_input::<i64>(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ prev_item_tags,
+ ),
+ LiteralType::F32 => create_scalar_item_input::<f32>(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ prev_item_tags,
+ ),
+ LiteralType::F64 => create_scalar_item_input::<f64>(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ prev_item_tags,
+ ),
+ LiteralType::Usize => create_scalar_item_input::<usize>(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ prev_item_tags,
+ ),
+ LiteralType::Isize => create_scalar_item_input::<isize>(
+ frame,
+ &mut item,
+ data_changed,
+ item_tag,
+ prev_item_tags,
+ ),
+ LiteralType::U128 => {
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_mut::<u128>() else {
+ unreachable!();
+ };
+
+ item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<u128>() else {
+ unreachable!();
+ };
+
+ &mut item.clone()
+ }
+ };
+
+ frame.text(item.to_string());
+ }
+ LiteralType::I128 => {
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_mut::<i128>() else {
+ unreachable!();
+ };
+
+ item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<i128>() else {
+ unreachable!();
+ };
+
+ &mut item.clone()
+ }
+ };
+
+ frame.text(item.to_string());
+ }
+ LiteralType::Bool => {
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_mut::<bool>() else {
+ unreachable!();
+ };
+
+ item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<bool>() else {
+ unreachable!();
+ };
+
+ &mut item.clone()
+ }
+ };
+
+ if frame.checkbox(
+ create_item_label(item_tag, "value_input", prev_item_tags),
+ item,
+ ) {
+ *data_changed = true;
+ }
+ }
+ LiteralType::Str => {
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_ref::<&'static str>()
+ else {
+ unreachable!();
+ };
+
+ *item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<&'static str>()
+ else {
+ unreachable!();
+ };
+
+ *item
+ }
+ };
+
+ frame.text(item);
+ }
+ _ => unimplemented!(),
+ }
+ }
+ ItemType::Reflected(TypeReflection::Array(array_type)) => {
+ let mut prev_item_tags = prev_item_tags.to_vec();
+
+ prev_item_tags.push(item_tag);
+
+ for array_item_index in 0..array_type.length {
+ let array_item_name = array_item_index.to_string();
+
+ let Ok(item_item) = (match &mut item {
+ ItemRef::Mutable(item) => {
+ (array_type.try_get_item_mut)(*item, array_item_index)
+ .map(ItemRef::Mutable)
+ }
+ ItemRef::Immutable(item) => {
+ (array_type.try_get_item)(*item, array_item_index)
+ .map(ItemRef::Immutable)
+ }
+ }) else {
+ unreachable!();
+ };
+
+ create_item_title_widget(
+ frame,
+ &array_item_name,
+ &ItemType::Reflected(array_type.item_type),
+ Some(array_type.item_type_name()),
+ );
+
+ let array_item_type = ItemType::Reflected(array_type.item_type);
+
+ let _token = array_item_type.indent_or_same_line(frame);
+
+ add_item_to_frame(
+ frame,
+ item_item,
+ data_changed,
+ ItemInfo {
+ title: array_item_name.as_ref(),
+ tag: array_item_name.as_ref(),
+ type_name: Some(array_type.item_type_name()),
+ ty: array_item_type,
+ is_read_only: item_is_read_only,
+ },
+ &prev_item_tags,
+ );
+ }
+ }
+ ItemType::Reflected(TypeReflection::Slice(slice_type)) => {
+ let mut prev_item_tags = prev_item_tags.to_vec();
+
+ prev_item_tags.push(item_tag);
+
+ let Some(len) = (slice_type.try_get_len)(item.as_ref()) else {
+ unreachable!();
+ };
+
+ for index in 0..len {
+ let item_name = index.to_string();
+
+ let Ok(item_item) = (slice_type.try_get_item)(item.as_ref(), index)
+ else {
+ unreachable!();
+ };
+
+ let mut slice_item_changed = false;
+
+ create_item_title_widget(
+ frame,
+ &item_name,
+ &ItemType::Reflected(slice_type.item_type),
+ Some(slice_type.item_type_name()),
+ );
+
+ let slice_item_type = ItemType::Reflected(slice_type.item_type);
+
+ let _token = slice_item_type.indent_or_same_line(frame);
+
+ add_item_to_frame(
+ frame,
+ ItemRef::Immutable(item_item),
+ &mut slice_item_changed,
+ ItemInfo {
+ title: &item_name,
+ tag: &item_name,
+ type_name: Some(slice_type.item_type_name()),
+ ty: slice_item_type,
+ is_read_only: true,
+ },
+ &prev_item_tags,
+ );
+
+ assert!(!slice_item_changed);
+ }
+ }
+ ItemType::Reflected(TypeReflection::Reference(ref_type)) => {
+ let mut derefed_changed = false;
+
+ let Some(derefed_item) = (ref_type.try_deref)(item.as_ref()) else {
+ unreachable!();
+ };
+
+ create_item_title_widget(
+ frame,
+ item_title,
+ &ItemType::Reflected(ref_type.ty),
+ item_type_name,
+ );
+
+ let ref_item_type = ItemType::Reflected(ref_type.ty);
+
+ let _token = ref_item_type.indent_or_same_line(frame);
+
+ add_item_to_frame(
+ frame,
+ ItemRef::Immutable(derefed_item),
+ &mut derefed_changed,
+ ItemInfo {
+ title: item_title,
+ tag: "derefed",
+ type_name: item_type_name,
+ ty: ref_item_type,
+ is_read_only: true,
+ },
+ prev_item_tags,
+ );
+
+ assert!(!derefed_changed);
+ }
+ ItemType::String => {
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_mut::<String>() else {
+ unreachable!();
+ };
+
+ item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<String>() else {
+ unreachable!();
+ };
+
+ &mut item.clone()
+ }
+ };
+
+ if frame
+ .input_text(
+ create_item_label(item_tag, "value_input", prev_item_tags),
+ item,
+ )
+ .build()
+ {
+ *data_changed = true;
+ }
+ }
+ ItemType::CowStr => {
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_mut::<Cow<'static, str>>() else {
+ unreachable!();
+ };
+
+ item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<Cow<'static, str>>() else {
+ unreachable!();
+ };
+
+ &mut item.clone()
+ }
+ };
+
+ if frame
+ .input_text(
+ create_item_label(item_tag, "value_input", prev_item_tags),
+ item.to_mut(),
+ )
+ .build()
+ {
+ *data_changed = true;
+ }
+ }
+ ItemType::Color(ColorItemType::Rgb, LiteralType::F32) => {
+ let mut item = item.into_writable::<Rgb<f32>>();
+
+ let mut scratch = [item.r, item.g, item.b];
+
+ if frame
+ .color_edit3_config(
+ create_item_label(item_tag, "value_input", prev_item_tags),
+ &mut scratch,
+ )
+ .display_mode(dear_imgui_bindings::ColorDisplayMode::Hex)
+ .build()
+ {
+ let [new_r, new_g, new_b] = scratch;
+
+ *item = Rgb { r: new_r, g: new_g, b: new_b };
+
+ *data_changed = true;
+ }
+ }
+ ItemType::Color(ColorItemType::Rgb, LiteralType::U8) => {
+ let mut item = item.into_writable::<Rgb<u8>>();
+
+ let mut scratch = [
+ f32::from(item.r) / 255.0,
+ f32::from(item.g) / 255.0,
+ f32::from(item.b) / 255.0,
+ ];
+
+ if frame
+ .color_edit3_config(
+ create_item_label(item_tag, "value_input", prev_item_tags),
+ &mut scratch,
+ )
+ .display_mode(dear_imgui_bindings::ColorDisplayMode::Hex)
+ .build()
+ {
+ let [new_r, new_g, new_b] = scratch;
+
+ *item = Rgb {
+ r: float_to_u8_checked(new_r * 255.0).unwrap_or(0),
+ g: float_to_u8_checked(new_g * 255.0).unwrap_or(0),
+ b: float_to_u8_checked(new_b * 255.0).unwrap_or(0),
+ };
+
+ *data_changed = true;
+ }
+ }
+ ItemType::Color(ColorItemType::Rgba, LiteralType::F32) => {
+ let mut item = item.into_writable::<Rgba<f32>>();
+
+ let mut scratch = [item.r, item.g, item.b, item.a];
+
+ if frame
+ .color_edit4_config(
+ create_item_label(item_tag, "value_input", prev_item_tags),
+ &mut scratch,
+ )
+ .display_mode(dear_imgui_bindings::ColorDisplayMode::Hex)
+ .build()
+ {
+ let [new_r, new_g, new_b, new_a] = scratch;
+
+ *item = Rgba {
+ r: new_r,
+ g: new_g,
+ b: new_b,
+ a: new_a,
+ };
+
+ *data_changed = true;
+ }
+ }
+ ItemType::Color(ColorItemType::Rgba, LiteralType::U8) => {
+ let mut item = item.into_writable::<Rgba<u8>>();
+
+ let mut scratch = [
+ f32::from(item.r) / 255.0,
+ f32::from(item.g) / 255.0,
+ f32::from(item.b) / 255.0,
+ f32::from(item.a) / 255.0,
+ ];
+
+ if frame
+ .color_edit4_config(
+ create_item_label(item_tag, "value_input", prev_item_tags),
+ &mut scratch,
+ )
+ .display_mode(dear_imgui_bindings::ColorDisplayMode::Hex)
+ .build()
+ {
+ let [new_r, new_g, new_b, new_a] = scratch;
+
+ *item = Rgba {
+ r: float_to_u8_checked(new_r * 255.0).unwrap_or(0),
+ g: float_to_u8_checked(new_g * 255.0).unwrap_or(0),
+ b: float_to_u8_checked(new_b * 255.0).unwrap_or(0),
+ a: float_to_u8_checked(new_a * 255.0).unwrap_or(0),
+ };
+
+ *data_changed = true;
+ }
+ }
+ ItemType::Color(ColorItemType::Rgb | ColorItemType::Rgba, _) => unreachable!(),
+ ItemType::Reflected(_) => unimplemented!(),
+ }
+}
+
+fn create_scalar_item_input<Scalar>(
+ frame: &dear_imgui_bindings::Ui,
+ item: &mut ItemRef<'_>,
+ data_changed: &mut bool,
+ item_tag: &str,
+ prev_item_tags: &[&str],
+) where
+ Scalar: dear_imgui_bindings::internal::DataTypeKind + Clone + 'static,
+{
+ let item = match item {
+ ItemRef::Mutable(item) => {
+ let Some(item) = (*item).downcast_mut::<Scalar>() else {
+ unreachable!();
+ };
+
+ item
+ }
+ ItemRef::Immutable(item) => {
+ let Some(item) = (*item).downcast_ref::<Scalar>() else {
+ unreachable!();
+ };
+
+ &mut item.clone()
+ }
+ };
+
+ if frame
+ .input_scalar(
+ create_item_label(item_tag, "value_input", prev_item_tags),
+ item,
+ )
+ .build()
+ {
+ *data_changed = true;
+ }
+}
+
+fn item_layout_table<'frame>(
+ frame: &'frame dear_imgui_bindings::Ui,
+ prev_item_tags: &[&str],
+) -> dear_imgui_bindings::TableBuilder<'frame>
+{
+ frame
+ .table(create_item_label("item_layout", "table", prev_item_tags))
+ .headers(false)
+ .sizing_policy(dear_imgui_bindings::TableSizingPolicy::StretchSame)
+ .columns([
+ dear_imgui_bindings::TableColumnSetup::new("a"),
+ dear_imgui_bindings::TableColumnSetup::new("b").stretch_weight(2.0),
+ ])
+}
+
+fn create_struct_widgets(
+ frame: &dear_imgui_bindings::Ui,
+ item: &mut ItemRef<'_>,
+ data_changed: &mut bool,
+ item_tag: &str,
+ item_is_read_only: bool,
+ struct_ty: &StructReflection,
+ prev_item_tags: &[&str],
+)
+{
+ let mut prev_item_tags = prev_item_tags.to_vec();
+
+ prev_item_tags.push(item_tag.as_ref());
+
+ let struct_fields =
+ struct_ty
+ .fields
+ .iter()
+ .enumerate()
+ .filter_map(|(field_index, field)| {
+ let field_item_type =
+ get_item_type(field.type_reflection(), field.type_id)?;
+
+ Some((field_index, field, field_item_type))
+ });
+
+ item_layout_table(frame, &prev_item_tags).build(|frame| {
+ for (field_index, field, field_item_type) in struct_fields
+ .clone()
+ .filter(|(.., field_item_type)| !field_item_type.spans_multiple_rows())
+ {
+ frame.table_next_row();
+ frame.table_next_column();
+
+ let field_name = field
+ .name
+ .map_or_else(|| field_index.to_string().into(), Cow::Borrowed);
+
+ let Ok(field_item) = item.get_struct_field(struct_ty, field_index) else {
+ unreachable!();
+ };
+
+ create_item_title_widget(
+ frame,
+ &field_name,
+ &field_item_type,
+ Some(field.type_name()),
+ );
+
+ frame.table_next_column();
+
+ add_item_to_frame(
+ frame,
+ field_item,
+ data_changed,
+ ItemInfo {
+ title: field_name.as_ref(),
+ tag: field_name.as_ref(),
+ type_name: Some(field.type_name()),
+ ty: field_item_type,
+ is_read_only: item_is_read_only
+ || matches!(
+ field.visibility,
+ Visibility::Private | Visibility::PubScoped(_)
+ ),
+ },
+ &prev_item_tags,
+ );
+ }
+ });
+
+ for (field_index, field, field_item_type) in struct_fields
+ .filter(|(.., field_item_type)| field_item_type.spans_multiple_rows())
+ {
+ let field_name = field
+ .name
+ .map_or_else(|| field_index.to_string().into(), Cow::Borrowed);
+
+ let Ok(field_item) = item.get_struct_field(struct_ty, field_index) else {
+ unreachable!();
+ };
+
+ create_item_title_widget(
+ frame,
+ &field_name,
+ &field_item_type,
+ Some(field.type_name()),
+ );
+
+ let _token = frame.begin_indent();
+
+ add_item_to_frame(
+ frame,
+ field_item,
+ data_changed,
+ ItemInfo {
+ title: field_name.as_ref(),
+ tag: field_name.as_ref(),
+ type_name: Some(field.type_name()),
+ ty: field_item_type,
+ is_read_only: item_is_read_only
+ || matches!(
+ field.visibility,
+ Visibility::Private | Visibility::PubScoped(_)
+ ),
+ },
+ &prev_item_tags,
+ );
+ }
+}
+
+fn create_enum_widgets(
+ frame: &dear_imgui_bindings::Ui,
+ item: &mut ItemRef<'_>,
+ data_changed: &mut bool,
+ item_tag: &str,
+ item_is_read_only: bool,
+ enum_type: &EnumReflection,
+ prev_item_tags: &[&str],
+)
+{
+ let Some(mut curr_variant_index) = (enum_type.get_variant_index)(item.as_ref())
+ else {
+ unreachable!();
+ };
+
+ if create_combo_box(
+ frame,
+ create_item_label(item_tag, "variant_select", prev_item_tags),
+ &mut curr_variant_index,
+ enum_type.variants,
+ |variant| {
+ let disabled = variant.fields.as_ref().and_then(|fields| {
+ fields.fields().iter().find_map(|field| {
+ let field_name = match field.name {
+ Some(field_name) => Either::A(field_name),
+ None => Either::B(field.index),
+ };
+
+ let Some(field_type) = field.type_reflection() else {
+ return Some((field_name, "has no type reflection"));
+ };
+
+ if !field_type.has_default_value() {
+ return Some((field_name, "has no default value"));
+ }
+
+ None
+ })
+ });
+
+ ComboBoxItem {
+ label: variant.name.into(),
+ disabled: disabled.is_some(),
+ hover_tooltip: disabled.map(|(bad_field_name, bad_field_reason)| {
+ format!(
+ "Variant cannot be selected since field {bad_field_name} {bad_field_reason}"
+ )
+ .into()
+ }),
+ }
+ },
+ ) {
+ 'used: {
+ let ItemRef::Mutable(item) = item else {
+ break 'used;
+ };
+
+ let new_variant = &enum_type.variants[curr_variant_index];
+
+ let Ok(()) = (new_variant.try_write_new_to)(
+ *item,
+ &mut new_variant
+ .fields
+ .iter()
+ .flat_map(engine_reflection::EnumVariantFields::fields)
+ .map(|field| {
+ let Some(field_type) = field.type_reflection() else {
+ // Variants with any field that is missing type reflection is
+ // disabled so that the user cannot select it
+ unreachable!();
+ };
+
+ let Some(default_value) = field_type.default_value() else {
+ unreachable!();
+ };
+
+ default_value
+ }),
+ ) else {
+ unreachable!();
+ };
+
+ *data_changed = true;
+ }
+ }
+
+ let mut prev_item_tags = prev_item_tags.to_vec();
+
+ prev_item_tags.push(item_tag);
+
+ let curr_variant = &enum_type.variants[curr_variant_index];
+
+ let Some(curr_variant_fields) = &curr_variant.fields else {
+ return;
+ };
+
+ let variant_fields = curr_variant_fields.fields().iter().enumerate().filter_map(
+ |(field_index, field)| {
+ let field_item_type = get_item_type(field.type_reflection(), field.type_id)?;
+
+ Some((field_index, field, field_item_type))
+ },
+ );
+
+ let variant_field_cnt = variant_fields.clone().count();
+
+ item_layout_table(frame, &prev_item_tags).build(|_| {
+ for (field_index, field, field_item_type) in variant_fields
+ .clone()
+ .filter(|(.., field_item_type)| !field_item_type.spans_multiple_rows())
+ {
+ frame.table_next_row();
+ frame.table_next_column();
+
+ let field_name: Cow<str> = match field.name {
+ Some(field_name) => field_name.into(),
+ None => field_index.to_string().into(),
+ };
+
+ let Ok(field_item) = item.get_enum_variant_field(curr_variant, field_index)
+ else {
+ unreachable!();
+ };
+
+ if variant_field_cnt != 1 || field.name.is_some() {
+ create_item_title_widget(
+ frame,
+ &field_name,
+ &field_item_type,
+ Some(field.type_name()),
+ );
+
+ frame.table_next_column();
+ }
+
+ add_item_to_frame(
+ frame,
+ field_item,
+ data_changed,
+ ItemInfo {
+ title: field_name.as_ref(),
+ tag: field_name.as_ref(),
+ type_name: Some(field.type_name()),
+ ty: field_item_type,
+ is_read_only: item_is_read_only,
+ },
+ &prev_item_tags,
+ );
+ }
+ });
+
+ for (field_index, field, field_item_type) in variant_fields
+ .filter(|(.., field_item_type)| field_item_type.spans_multiple_rows())
+ {
+ let field_name: Cow<str> = match field.name {
+ Some(field_name) => field_name.into(),
+ None => field_index.to_string().into(),
+ };
+
+ let Ok(field_item) = item.get_enum_variant_field(curr_variant, field_index)
+ else {
+ unreachable!();
+ };
+
+ if variant_field_cnt != 1 || field.name.is_some() {
+ create_item_title_widget(
+ frame,
+ &field_name,
+ &field_item_type,
+ Some(field.type_name()),
+ );
+
+ let _token = frame.begin_indent();
+ }
+
+ add_item_to_frame(
+ frame,
+ field_item,
+ data_changed,
+ ItemInfo {
+ title: field_name.as_ref(),
+ tag: field_name.as_ref(),
+ type_name: Some(field.type_name()),
+ ty: field_item_type,
+ is_read_only: item_is_read_only,
+ },
+ &prev_item_tags,
+ );
+ }
+}
+
+fn create_item_label(item_tag: &str, value_name: &str, prev_item_tags: &[&str])
+ -> String
+{
+ let mut item_label = String::with_capacity(
+ 2 + item_tag.len()
+ + prev_item_tags.len()
+ + prev_item_tags.iter().max().unwrap_or(&"").len(),
+ );
+
+ item_label.push_str("##");
+
+ for prev_item_tag in prev_item_tags {
+ item_label.push('/');
+ item_label.push_str(prev_item_tag);
+ }
+
+ item_label.push('/');
+ item_label.push_str(item_tag);
+ item_label.push(':');
+ item_label.push_str(value_name);
+
+ item_label
+}
+
+fn get_whole_type_is_user_editable(
+ ty: Option<&'static TypeReflection>,
+ type_id: TypeId,
+) -> bool
+{
+ let Some(item_type) = get_item_type(ty, type_id) else {
+ return false;
+ };
+
+ match item_type {
+ ItemType::Reflected(TypeReflection::Struct(struct_ty)) => {
+ for field in struct_ty.fields {
+ if matches!(
+ field.visibility,
+ Visibility::Private | Visibility::PubScoped(_)
+ ) {
+ continue;
+ }
+
+ if !get_whole_type_is_user_editable(
+ field.type_reflection(),
+ field.type_id,
+ ) {
+ return false;
+ }
+ }
+
+ true
+ }
+ ItemType::Reflected(TypeReflection::Enum(enum_ty)) => {
+ enum_ty.variants.iter().any(|variant| {
+ for field in variant
+ .fields
+ .iter()
+ .flat_map(engine_reflection::EnumVariantFields::fields)
+ {
+ if !get_whole_type_is_user_editable(
+ field.type_reflection(),
+ field.type_id,
+ ) {
+ return false;
+ }
+ }
+
+ true
+ })
+ }
+ ItemType::Reflected(
+ TypeReflection::Slice(_)
+ | TypeReflection::Array(_)
+ | TypeReflection::Literal(_),
+ )
+ | ItemType::String
+ | ItemType::CowStr
+ | ItemType::Color(..) => true,
+ ItemType::Reflected(_) => unimplemented!(),
+ }
+}
+
+struct ComboBoxItem<'b>
+{
+ label: Cow<'b, str>,
+ disabled: bool,
+ hover_tooltip: Option<Cow<'b, str>>,
+}
+
+fn create_combo_box<Item, ItemFn>(
+ frame: &dear_imgui_bindings::Ui,
+ label: impl AsRef<str>,
+ current_item: &mut usize,
+ items: &[Item],
+ item_fn: ItemFn,
+) -> bool
+where
+ for<'b> ItemFn: Fn(&'b Item) -> ComboBoxItem<'b>,
+{
+ let item_fn = &item_fn;
+
+ let mut result = false;
+
+ let preview_label = items.get(*current_item).map(|item| item_fn(item).label);
+
+ if let Some(combo_token) = frame.begin_combo(
+ label,
+ preview_label
+ .as_ref()
+ .map_or("", std::convert::AsRef::as_ref),
+ ) {
+ for (idx, item) in items.iter().enumerate() {
+ let is_selected = idx == *current_item;
+
+ if is_selected {
+ frame.set_item_default_focus();
+ }
+
+ let ComboBoxItem {
+ label: item_label,
+ disabled: item_disabled,
+ hover_tooltip: item_hover_tooltip,
+ } = item_fn(item);
+
+ let disabled_token = frame.begin_disabled_with_cond(item_disabled);
+
+ let clicked = frame.selectable(item_label.as_ref());
+
+ if clicked {
+ *current_item = idx;
+ result = true;
+ }
+
+ disabled_token.end();
+
+ if let Some(item_hover_tooltip) = item_hover_tooltip {
+ if frame.is_item_hovered_with_flags(
+ dear_imgui_bindings::ItemHoveredFlags::ALLOW_WHEN_DISABLED,
+ ) {
+ frame.tooltip_text(item_hover_tooltip.as_ref());
+ }
+ }
+ }
+
+ combo_token.end();
+ }
+
+ result
+}
+
+struct ListBoxItem<'b>
+{
+ label: Cow<'b, str>,
+ disabled: bool,
+ hover_tooltip: Option<Cow<'b, str>>,
+}
+
+trait ListBoxExt
+{
+ fn build_extended<Item, ItemFn>(
+ self,
+ ui: &dear_imgui_bindings::Ui,
+ current_item: &mut Option<usize>,
+ items: &[Item],
+ item_fn: &ItemFn,
+ ) -> Option<usize>
+ where
+ for<'b> ItemFn: Fn(&'b Item) -> ListBoxItem<'b>;
+}
+
+impl<Label> ListBoxExt for dear_imgui_bindings::ListBox<Label>
+where
+ Label: AsRef<str>,
+{
+ fn build_extended<Item, ItemFn>(
+ self,
+ ui: &dear_imgui_bindings::Ui,
+ current_item: &mut Option<usize>,
+ items: &[Item],
+ item_fn: &ItemFn,
+ ) -> Option<usize>
+ where
+ for<'b> ItemFn: Fn(&'b Item) -> ListBoxItem<'b>,
+ {
+ let mut result = None;
+
+ if let Some(_lb_token) = self.begin(ui) {
+ for (index, item) in items.iter().enumerate() {
+ let ListBoxItem {
+ label: item_label,
+ disabled: item_disabled,
+ hover_tooltip: item_hover_tooltip,
+ } = item_fn(item);
+
+ let is_selected = Some(index) == *current_item;
+
+ let disabled_token = ui.begin_disabled_with_cond(item_disabled);
+
+ if ui
+ .selectable_config(&item_label)
+ .selected(is_selected)
+ .build()
+ {
+ *current_item = Some(index);
+ result = Some(index);
+ }
+
+ disabled_token.end();
+
+ if let Some(item_hover_tooltip) = item_hover_tooltip {
+ if ui.is_item_hovered_with_flags(
+ dear_imgui_bindings::ItemHoveredFlags::ALLOW_WHEN_DISABLED,
+ ) {
+ ui.tooltip_text(item_hover_tooltip.as_ref());
+ }
+ }
+ }
+ }
+
+ result
+ }
+}
+
+fn create_entity_title(ent_handle: &EntityHandle) -> String
+{
+ let ent_name = ent_handle.get::<EntityName>();
+
+ format!(
+ "{} {}",
+ ent_name
+ .as_ref()
+ .map_or("<unnamed>", |ent_name| ent_name.name.as_ref()),
+ ent_handle.uid()
+ )
+}
+
+struct IconTextures
+{
+ despawn: dear_imgui_bindings::ManagedTextureId,
+ warning: dear_imgui_bindings::ManagedTextureId,
+ edit: dear_imgui_bindings::ManagedTextureId,
+}
+
+impl IconTextures
+{
+ fn new(dear_imgui_context: &mut DearImguiContext) -> Self
+ {
+ let despawn = create_texture(
+ dear_imgui_context,
+ Image::from_reader(
+ Cursor::new(include_bytes!("../../../res/ui/delete.png")),
+ ImageFormat::Png,
+ )
+ .unwrap(),
+ );
+
+ let warning = create_texture(
+ dear_imgui_context,
+ Image::from_reader(
+ Cursor::new(include_bytes!("../../../res/ui/warning.png")),
+ ImageFormat::Png,
+ )
+ .unwrap(),
+ );
+
+ let edit = create_texture(
+ dear_imgui_context,
+ Image::from_reader(
+ Cursor::new(include_bytes!("../../../res/ui/edit.png")),
+ ImageFormat::Png,
+ )
+ .unwrap(),
+ );
+
+ Self { despawn, warning, edit }
+ }
+}
+
+#[derive(Debug)]
+struct AddComponentPopupState
+{
+ popup_name: String,
+ target_entity_id: Uid,
+ search_text: String,
+ searched_components: Vec<(Uid, ComponentInfo, ComponentUserCreatable)>,
+ searchable_components: Vec<(Uid, ComponentInfo, ComponentUserCreatable)>,
+ selected_search_result: Option<usize>,
+ new_component: Option<(Box<dyn Any>, ComponentInfo, Uid)>,
+}
+
+#[derive(Debug)]
+struct RenameEntityPopupState
+{
+ popup_name: String,
+ target_entity_id: Uid,
+ new_entity_name: String,
+}
+
+#[derive(Debug)]
+enum PopupState
+{
+ AddComponent(AddComponentPopupState),
+ RenameEntity(RenameEntityPopupState),
+}
+
+fn float_to_u8_checked(value: f32) -> Option<u8>
+{
+ if !value.is_finite() {
+ return None;
+ }
+
+ if value > f32::from(u8::MAX) {
+ return Some(u8::MAX);
+ }
+
+ if value < f32::from(u8::MIN) {
+ return Some(u8::MIN);
+ }
+
+ Some(unsafe { value.to_int_unchecked::<u8>() })
+}