From 2cfb72135985240576deb2991a238444d680cf90 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Wed, 25 Mar 2026 15:23:31 +0900 Subject: [PATCH] feat(editor): add editor_demo example with IMGUI widgets Interactive demo showing panel, text, button, slider, and checkbox widgets rendered via the new UiRenderer pipeline. Uses winit event loop with mouse input forwarded to UiContext. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 23 +++ Cargo.toml | 1 + examples/editor_demo/Cargo.toml | 15 ++ examples/editor_demo/src/main.rs | 233 +++++++++++++++++++++++++++++++ 4 files changed, 272 insertions(+) create mode 100644 examples/editor_demo/Cargo.toml create mode 100644 examples/editor_demo/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 9b682ee..fefb089 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -483,6 +483,21 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "editor_demo" +version = "0.1.0" +dependencies = [ + "bytemuck", + "env_logger", + "log", + "pollster", + "voltex_editor", + "voltex_platform", + "voltex_renderer", + "wgpu", + "winit", +] + [[package]] name = "env_filter" version = "1.0.1" @@ -2069,6 +2084,14 @@ dependencies = [ "voltex_math", ] +[[package]] +name = "voltex_editor" +version = "0.1.0" +dependencies = [ + "bytemuck", + "wgpu", +] + [[package]] name = "voltex_math" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 34f9e60..4b455c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "crates/voltex_net", "crates/voltex_script", "crates/voltex_editor", + "examples/editor_demo", ] [workspace.dependencies] diff --git a/examples/editor_demo/Cargo.toml b/examples/editor_demo/Cargo.toml new file mode 100644 index 0000000..9f9ddcd --- /dev/null +++ b/examples/editor_demo/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "editor_demo" +version = "0.1.0" +edition = "2021" + +[dependencies] +voltex_platform.workspace = true +voltex_renderer.workspace = true +voltex_editor.workspace = true +wgpu.workspace = true +winit.workspace = true +bytemuck.workspace = true +pollster.workspace = true +env_logger.workspace = true +log.workspace = true diff --git a/examples/editor_demo/src/main.rs b/examples/editor_demo/src/main.rs new file mode 100644 index 0000000..0785df2 --- /dev/null +++ b/examples/editor_demo/src/main.rs @@ -0,0 +1,233 @@ +use winit::{ + application::ApplicationHandler, + event::WindowEvent, + event_loop::{ActiveEventLoop, EventLoop}, + keyboard::{KeyCode, PhysicalKey}, + window::WindowId, +}; +use voltex_platform::{VoltexWindow, WindowConfig, InputState, GameTimer}; +use voltex_renderer::GpuContext; +use voltex_editor::{UiContext, UiRenderer}; + +struct EditorDemoApp { + state: Option, +} + +struct AppState { + window: VoltexWindow, + gpu: GpuContext, + input: InputState, + timer: GameTimer, + ui: UiContext, + ui_renderer: UiRenderer, + // Widget state + counter: u32, + speed: f32, + show_grid: bool, +} + +impl ApplicationHandler for EditorDemoApp { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let config = WindowConfig { + title: "Voltex - Editor Demo".to_string(), + width: 1280, + height: 720, + ..Default::default() + }; + let window = VoltexWindow::new(event_loop, &config); + let gpu = GpuContext::new(window.handle.clone()); + + let ui = UiContext::new( + gpu.config.width as f32, + gpu.config.height as f32, + ); + let ui_renderer = UiRenderer::new( + &gpu.device, + &gpu.queue, + gpu.surface_format, + &ui.font, + ); + + self.state = Some(AppState { + window, + gpu, + input: InputState::new(), + timer: GameTimer::new(60), + ui, + ui_renderer, + counter: 0, + speed: 5.0, + show_grid: true, + }); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + let state = match &mut self.state { + Some(s) => s, + None => return, + }; + + match event { + WindowEvent::CloseRequested => event_loop.exit(), + + WindowEvent::KeyboardInput { + event: winit::event::KeyEvent { + physical_key: PhysicalKey::Code(key_code), + state: key_state, + .. + }, + .. + } => { + let pressed = key_state == winit::event::ElementState::Pressed; + state.input.process_key(key_code, pressed); + if key_code == KeyCode::Escape && pressed { + event_loop.exit(); + } + } + + WindowEvent::Resized(size) => { + state.gpu.resize(size.width, size.height); + } + + WindowEvent::CursorMoved { position, .. } => { + state.input.process_mouse_move(position.x, position.y); + } + + WindowEvent::MouseInput { state: btn_state, button, .. } => { + let pressed = btn_state == winit::event::ElementState::Pressed; + state.input.process_mouse_button(button, pressed); + } + + WindowEvent::MouseWheel { delta, .. } => { + let y = match delta { + winit::event::MouseScrollDelta::LineDelta(_, y) => y, + winit::event::MouseScrollDelta::PixelDelta(pos) => pos.y as f32, + }; + state.input.process_scroll(y); + } + + WindowEvent::RedrawRequested => { + state.timer.tick(); + state.input.begin_frame(); + + // Gather mouse state + let (mx, my) = state.input.mouse_position(); + let mouse_down = state.input.is_mouse_button_pressed( + winit::event::MouseButton::Left, + ); + + let dt = state.timer.frame_dt(); + let fps = if dt > 0.0 { 1.0 / dt } else { 0.0 }; + + // Begin UI frame + state.ui.begin_frame(mx as f32, my as f32, mouse_down); + + // Draw widgets inside a panel + state.ui.begin_panel("Debug", 10.0, 10.0, 250.0, 400.0); + state.ui.text("Voltex Editor Demo"); + state.ui.text(&format!("FPS: {:.0}", fps)); + + if state.ui.button("Click Me") { + state.counter += 1; + } + state.ui.text(&format!("Clicked: {}", state.counter)); + + state.speed = state.ui.slider("Speed", state.speed, 0.0, 10.0); + state.show_grid = state.ui.checkbox("Show Grid", state.show_grid); + + state.ui.end_panel(); + state.ui.end_frame(); + + // Acquire surface texture + let output = match state.gpu.surface.get_current_texture() { + Ok(t) => t, + Err(wgpu::SurfaceError::Lost) => { + let (w, h) = state.window.inner_size(); + state.gpu.resize(w, h); + return; + } + Err(wgpu::SurfaceError::OutOfMemory) => { + event_loop.exit(); + return; + } + Err(_) => return, + }; + + let view = output.texture.create_view( + &wgpu::TextureViewDescriptor::default(), + ); + let mut encoder = state.gpu.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("Editor Demo Encoder"), + }, + ); + + // Clear the background + { + let _clear_pass = encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("Clear Pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.12, + g: 0.12, + b: 0.15, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }, + ); + // Pass drops here, clearing the surface + } + + // Render UI overlay + let screen_w = state.gpu.config.width as f32; + let screen_h = state.gpu.config.height as f32; + state.ui_renderer.render( + &state.gpu.device, + &state.gpu.queue, + &mut encoder, + &view, + &state.ui.draw_list, + screen_w, + screen_h, + ); + + state.gpu.queue.submit(std::iter::once(encoder.finish())); + output.present(); + } + + _ => {} + } + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + if let Some(state) = &self.state { + state.window.request_redraw(); + } + } +} + +fn main() { + env_logger::init(); + let event_loop = EventLoop::new().unwrap(); + let mut app = EditorDemoApp { state: None }; + event_loop.run_app(&mut app).unwrap(); +}