diff options
Diffstat (limited to 'engine/src/ui')
| -rw-r--r-- | engine/src/ui/dear_imgui.rs | 1082 | ||||
| -rw-r--r-- | engine/src/ui/view.rs | 2 | ||||
| -rw-r--r-- | engine/src/ui/view/transform_3d.rs | 128 | ||||
| -rw-r--r-- | engine/src/ui/view/world.rs | 2316 |
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>() }) +} |
