use std::path::{Path, PathBuf}; use std::sync::LazyLock; use dear_imgui_rs::texture::TextureFormat as ImguiTextureFormat; use dear_imgui_rs::{ DrawCmd as ImguiDrawCmd, Key as ImguiKey, MouseButton as ImguiMouseButton, TextureId as ImguiTextureId, }; use ecs::component::local::Local; use ecs::component::Component; 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::{Component, Query, Sole}; use crate::asset::{Assets, Handle as AssetHandle, Label as AssetLabel}; use crate::data_types::dimens::Dimens; use crate::delta_time::DeltaTime; use crate::image::{ ColorType as ImageColorType, FromBytesError as ImageFromBytesError, Image, }; 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, }; use crate::mesh::{ Mesh, VertexAttrType as MeshVertexAttrType, POSITION_VERTEX_ATTRIB_NAME, }; use crate::projection::{ ClipVolume as ProjectionClipVolume, Orthographic as OrthographicProjection, }; use crate::rendering::blending::{ Config as RenderingBlendingConfiguration, Equation as RenderingBlendingEquation, Factor as RenderingBlendingFactor, }; use crate::rendering::object::{Id as RenderingObjectId, Store as RenderingObjectStore}; use crate::rendering::{ Command as RenderingCommand, DrawMeshOptions, DrawProperties, MeshUsage, RenderPass, RenderPasses, SurfaceId, SurfaceSpec, PRE_RENDER_PHASE, }; use crate::shader::cursor::{BindingValue as ShaderBindingValue, Cursor as ShaderCursor}; use crate::shader::{ Context as ShaderContext, EntrypointFlags as ShaderEntrypointFlags, ModuleSource as ShaderModuleSource, }; use crate::texture::{ Filtering as TextureFiltering, Properties as TextureProperties, Texture, Wrapping as TextureWrapping, }; use crate::vector::Vec2; use crate::windowing::window::Window; mod reexports { pub use dear_imgui_rs as dear_imgui; } pub use reexports::*; pub static SHADER_ASSET_LABEL: LazyLock = LazyLock::new(|| AssetLabel { path: Path::new("").into(), name: Some("imgui_shader".into()), }); /// Imgui context #[derive(Sole)] pub struct Context { pub enabled: bool, ctx: inner_context_wrapper::InnerContextWrapper, } impl Context { pub fn frame(&mut self) -> Option<&mut dear_imgui_rs::Ui> { if !self.enabled { return None; } self.ctx.get_frame() } } #[derive(Debug, Default)] #[non_exhaustive] pub struct Extension { start_enabled: bool, settings_ini_file_path: Option, } impl Extension { pub fn with_start_enabled(mut self, start_enabled: bool) -> Self { self.start_enabled = start_enabled; self } pub fn with_settings_ini_file( mut self, settings_ini_file_path: Option, ) -> 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(), }; 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}"); } 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>, mut context: Single, ) { 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 = window.inner_size(); let window_size = Dimens { width: window_size.width as f32, height: window_size.height as f32, }; let window_logical_size = window_size / (hidpi_factor as f32); context .ctx .get_io_mut() .set_display_size([window_logical_size.width, window_logical_size.height]); } fn update( target_window_query: Query<(&Window, &SurfaceSpec), (With,)>, mut context: Single, delta_time: Single, mut assets: Single, mut render_passes: Single, renderer_object_store: Single, shader_context: Single, keyboard: Single, mouse: Single, mouse_buttons: Single, mut state: Local, ) { let Some((window, window_surface_spec)) = target_window_query.iter().next() else { return; }; let curr_state = std::mem::replace(&mut *state, State::Unreachable); match curr_state { State::NotInitialized => { let Some((font_texture_asset, shader_asset, mesh_obj_id, mesh)) = initialize_context( &mut context, &window, window_surface_spec.id, &mut assets, &mut render_passes, &shader_context, ) else { *state = State::NotInitialized; return; }; *state = State::Initializing { font_texture_asset, shader_asset, mesh_obj_id, mesh, }; } State::Initializing { font_texture_asset, shader_asset, mesh_obj_id, mesh, } => { let Some(font_texture_obj) = renderer_object_store .get_obj(&RenderingObjectId::Asset(font_texture_asset.id())) else { *state = State::Initializing { font_texture_asset, shader_asset, mesh_obj_id, mesh, }; return; }; context .ctx .get_font_atlas_mut() .set_texture_id(ImguiTextureId::new(font_texture_obj.as_raw().into())); context .ctx .get_io_mut() .set_delta_time(delta_time.duration.as_secs() as f32); context.ctx.new_frame(); *state = State::Ready { font_texture_asset, shader_asset, mesh_obj_id, mesh, }; } State::Ready { font_texture_asset, shader_asset, mesh_obj_id, mut mesh, } => { if !context.enabled { *state = State::Ready { font_texture_asset, shader_asset, mesh_obj_id, mesh, }; return; } context .ctx .get_io_mut() .set_delta_time(delta_time.duration.as_secs_f32()); update_inputs(&mut context, &window, &keyboard, &mouse, &mouse_buttons); add_drawing_render_pass( &mut context, &mut render_passes, &shader_context, window_surface_spec.id, font_texture_asset.clone(), shader_asset.clone(), mesh_obj_id, &mut mesh, ); context.ctx.new_frame(); *state = State::Ready { font_texture_asset, shader_asset, mesh_obj_id, mesh, }; } State::Unreachable => unreachable!(), } } #[derive(Debug, Component)] pub struct TargetWindow; #[derive(Debug, Default, Component)] enum State { #[default] NotInitialized, Initializing { font_texture_asset: AssetHandle, shader_asset: AssetHandle, mesh_obj_id: RenderingObjectId, mesh: Mesh, }, Ready { font_texture_asset: AssetHandle, shader_asset: AssetHandle, 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, AssetHandle, RenderingObjectId, Mesh, )> { let shader_asset = if let Some(shader_asset) = assets.get_handle_to_loaded::(SHADER_ASSET_LABEL.clone()) { shader_asset } 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; }; 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 = window.inner_size(); let window_size = Dimens { width: window_size.width as f32, height: window_size.height as f32, }; let window_logical_size = window_size / (hidpi_factor as f32); context .ctx .get_io_mut() .set_display_size([window_logical_size.width, window_logical_size.height]); let mut fonts = context.ctx.get_font_atlas_mut(); if !fonts.is_built() { fonts.build(); } let Some(font_texture_data) = fonts.tex_data_mut() else { tracing::error!("No texture data exists for font"); return None; }; let font_texture = match create_font_texture(font_texture_data) { Ok(font_texture) => font_texture, Err(err) => { tracing::error!("Failed to create font texture: {err}"); return None; } }; let font_texture_asset = assets.store_with_name("imgui_font_texture", font_texture); let mesh_obj_id = RenderingObjectId::new_sequential(); let mesh = Mesh::builder() .vertices(MeshVertexBuffer::with_capacity( &[ MeshVertexAttrInfo { name: POSITION_VERTEX_ATTRIB_NAME.into(), ty: MeshVertexAttrType::Float32Array { length: 3 }, }, MeshVertexAttrInfo { name: "color".into(), ty: MeshVertexAttrType::Float32Array { length: 4 }, }, MeshVertexAttrInfo { name: "texture_coords".into(), ty: MeshVertexAttrType::Float32Array { length: 2 }, }, ], 128, )) .indices([]) .build(); render_passes.passes.push_back(RenderPass { commands: vec![ RenderingCommand::MakeCurrent(window_surface_id), RenderingCommand::CreateTexture(font_texture_asset.clone()), 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: Some(mesh.clone()), usage: MeshUsage::Stream, }, ], draw_properties: DrawProperties::default(), }); Some((font_texture_asset, shader_asset, mesh_obj_id, mesh)) } fn update_inputs( context: &mut Context, window: &Window, keyboard: &Keyboard, mouse: &Mouse, mouse_buttons: &MouseButtons, ) { for (key, key_state) in keyboard.new_key_states() { if let Some(key) = key_to_imgui_key(key) { context .ctx .get_io_mut() .add_key_event(key, key_state.is_pressed()); } } for character in keyboard.text_keys().chars() { if character == '\u{7f}' { continue; } context.ctx.get_io_mut().add_input_character(character); } let mouse_pos = mouse.position / window.scale_factor(); context .ctx .get_io_mut() .add_mouse_pos_event([mouse_pos.x as f32, mouse_pos.y as f32]); for (mouse_button, mouse_button_state) in mouse_buttons.all_current() { if let Some(mouse_button) = mouse_button_to_imgui_mouse_button(mouse_button) { context .ctx .get_io_mut() .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, font_texture_asset: AssetHandle, shader_asset: AssetHandle, mesh_obj_id: RenderingObjectId, mesh: &mut Mesh, ) { 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, ..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 draw_data = context.ctx.render(); let [display_width, display_height] = draw_data.display_size; // let [scale_width, scale_height] = draw_data.framebuffer_scale; render_pass.commands.extend([ RenderingCommand::MakeCurrent(window_surface_id), RenderingCommand::ActivateShader(RenderingObjectId::Asset(shader_asset.id())), RenderingCommand::SetShaderBinding( shader_cursor .field("Uniforms") .field("projection") .into_binding_location(), ShaderBindingValue::FMat4x4( OrthographicProjection::builder() .near(1.0) .far(-1.0) .viewport_origin(Vec2 { x: 0.0, y: 1.0 }) .size(crate::projection::OrthographicSize::WindowSize) .build() .to_matrix_rh( Dimens { width: display_width, height: -display_height, }, ProjectionClipVolume::NegOneToOne, ), ), ), RenderingCommand::SetShaderBinding( shader_cursor.field("main_texture").into_binding_location(), ShaderBindingValue::Texture(font_texture_asset), ), ]); 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]> { name: POSITION_VERTEX_ATTRIB_NAME, value: [vertex.pos[0], vertex.pos[1], 0.0], }, MeshNamedVertexAttr { name: "color", value: vertex.rgba().map(|elem| (elem as f32) / 255.0), }, MeshNamedVertexAttr { name: "texture_coords", 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() { match command { ImguiDrawCmd::Elements { count, cmd_params, raw_cmd: _ } => { render_pass.commands.push(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(), )); } _ => {} } } } } fn create_font_texture( font_texture_data: &dear_imgui_rs::texture::TextureData, ) -> Result { Ok(Texture { image: Image::try_from_bytes( font_texture_data .pixels() .expect("Font texture data does not contain any pixels"), Dimens { width: font_texture_data.width().try_into().expect( "Font texture width value does not fit in 32-bit unsigned int", ), height: font_texture_data.height().try_into().expect( "Font texture height value does not fit in 32-bit unsigned int", ), }, match font_texture_data.format() { ImguiTextureFormat::RGBA32 => ImageColorType::Rgba8, ImguiTextureFormat::Alpha8 => unimplemented!(), }, )?, properties: TextureProperties::builder() .minifying_filter(TextureFiltering::Linear) .magnifying_filter(TextureFiltering::Linear) .wrap(TextureWrapping::ClampToBorder) .build(), }) } fn key_to_imgui_key(key: Key) -> Option { 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 { 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>, frame: *mut dear_imgui_rs::Ui, } 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")); Self { ctx, frame: null_mut() } } pub fn set_settings_ini_file_path( &mut self, path: Option, ) -> Result<(), dear_imgui_rs::ImGuiError> { self.ctx.set_ini_filename(path) } pub fn get_io_mut(&mut self) -> &mut dear_imgui_rs::Io { self.ctx.io_mut() } pub fn get_font_atlas_mut(&mut self) -> dear_imgui_rs::fonts::FontAtlas { self.ctx.font_atlas_mut() } pub fn render(&mut self) -> &dear_imgui_rs::DrawData { 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() } } } }