Skip to main content

khora_infra/ui/egui/
overlay.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Concrete [`EditorOverlay`] implementation backed by egui + custom wgpu renderer.
16//!
17//! This struct manages:
18//! - An [`egui::Context`] for UI building (shared with `khora-editor`)
19//! - An [`egui_winit::State`] for input event translation
20//! - A custom [`EguiWgpuRenderer`] for rendering egui output with wgpu 28
21
22use super::renderer::{EguiRenderState, EguiWgpuRenderer};
23use crate::graphics::wgpu::context::WgpuGraphicsContext;
24use egui::ViewportId;
25use khora_core::ui::editor_overlay::{EditorOverlay, OverlayError, OverlayScreenDescriptor};
26use std::any::Any;
27use std::sync::{Arc, Mutex};
28
29/// Render state passed through `end_frame_and_render` as `&mut dyn Any`.
30///
31/// The caller (`WgpuRenderSystem`) constructs this with the current frame's GPU resources.
32/// All fields are owned (`'static`), so this type can be passed through `dyn Any`.
33pub struct EguiFrameRenderState {
34    /// The graphics context (device + queue), shared via Arc.
35    pub graphics_context: Arc<Mutex<WgpuGraphicsContext>>,
36    /// The wgpu command encoder for this overlay pass (owned, moved back after render).
37    pub encoder: Option<wgpu::CommandEncoder>,
38    /// The swapchain texture view to render onto (owned).
39    pub target_view: wgpu::TextureView,
40    /// Physical width in pixels.
41    pub width_px: u32,
42    /// Physical height in pixels.
43    pub height_px: u32,
44}
45
46/// Egui-based editor overlay implementation.
47///
48/// Created once during engine initialization and stored in the `ServiceRegistry`.
49/// The editor application accesses the shared `egui::Context` to build its UI.
50pub struct EguiOverlay {
51    ctx: egui::Context,
52    winit_state: egui_winit::State,
53    renderer: EguiWgpuRenderer,
54    /// Current screen descriptor.
55    screen: OverlayScreenDescriptor,
56}
57
58impl EguiOverlay {
59    /// Creates a new `EguiOverlay`.
60    ///
61    /// # Arguments
62    /// * `event_loop` - The winit event loop (needed by `egui_winit::State`).
63    /// * `surface_format` - The wgpu surface texture format.
64    /// * `device` - The wgpu device for pipeline initialization.
65    /// * `shader_source` - The WGSL shader source for egui rendering.
66    pub fn new(
67        event_loop: &winit::event_loop::ActiveEventLoop,
68        surface_format: wgpu::TextureFormat,
69        device: &wgpu::Device,
70        shader_source: &str,
71    ) -> Self {
72        let ctx = egui::Context::default();
73        let winit_state = egui_winit::State::new(
74            ctx.clone(),
75            ViewportId::ROOT,
76            event_loop,
77            Some(1.0), // default pixels_per_point
78            None,      // theme
79            None,      // max_texture_side
80        );
81
82        let mut renderer = EguiWgpuRenderer::new(surface_format);
83        renderer.initialize(device, shader_source);
84
85        Self {
86            ctx,
87            winit_state,
88            renderer,
89            screen: OverlayScreenDescriptor {
90                width_px: 1,
91                height_px: 1,
92                scale_factor: 1.0,
93            },
94        }
95    }
96
97    /// Returns a clone of the egui context (cheap — internally Arc'd).
98    ///
99    /// The editor application stores this to build UI during `update()`.
100    pub fn context(&self) -> egui::Context {
101        self.ctx.clone()
102    }
103
104    /// Registers an external wgpu texture view with the egui renderer.
105    ///
106    /// Returns the egui `TextureId` that can later be mapped to a
107    /// [`ViewportTextureHandle`].
108    pub fn register_viewport_texture(
109        &mut self,
110        device: &wgpu::Device,
111        view: &wgpu::TextureView,
112    ) -> egui::TextureId {
113        self.renderer.register_external_texture(device, view)
114    }
115
116    /// Updates an existing viewport texture to point to a new wgpu view
117    /// (e.g. after a resize).
118    pub fn update_viewport_texture(
119        &mut self,
120        device: &wgpu::Device,
121        id: egui::TextureId,
122        view: &wgpu::TextureView,
123    ) {
124        self.renderer.update_external_texture(device, id, view);
125    }
126}
127
128impl EditorOverlay for EguiOverlay {
129    fn handle_window_event(&mut self, window: &dyn Any, event: &dyn Any) -> bool {
130        let Some(winit_window) = window.downcast_ref::<winit::window::Window>() else {
131            return false;
132        };
133        let Some(winit_event) = event.downcast_ref::<winit::event::WindowEvent>() else {
134            return false;
135        };
136
137        let response = self.winit_state.on_window_event(winit_window, winit_event);
138        response.consumed
139    }
140
141    fn begin_frame(&mut self, window: &dyn Any, screen: OverlayScreenDescriptor) {
142        self.screen = screen;
143        if let Some(winit_window) = window.downcast_ref::<winit::window::Window>() {
144            let raw_input = self.winit_state.take_egui_input(winit_window);
145            self.ctx.begin_pass(raw_input);
146        } else {
147            log::warn!("EguiOverlay: begin_frame called with non-winit window");
148        }
149    }
150
151    fn ui_context(&self) -> &dyn Any {
152        &self.ctx
153    }
154
155    fn end_frame_and_render(&mut self, render_state: &mut dyn Any) -> Result<(), OverlayError> {
156        let output = self.ctx.end_pass();
157
158        // Tessellate
159        let pixels_per_point = output.pixels_per_point;
160        let primitives = self.ctx.tessellate(output.shapes, pixels_per_point);
161        let textures_delta = output.textures_delta;
162
163        // Get the typed render state
164        let state = render_state
165            .downcast_mut::<EguiFrameRenderState>()
166            .ok_or_else(|| OverlayError("Expected EguiFrameRenderState".to_string()))?;
167
168        // Lock the graphics context for device/queue access
169        let gc = state
170            .graphics_context
171            .lock()
172            .map_err(|_| OverlayError("Failed to lock graphics context".to_string()))?;
173
174        // Update textures
175        self.renderer
176            .update_textures(&gc.device, &gc.queue, &textures_delta);
177
178        // Get a mutable reference to the encoder
179        let encoder = state
180            .encoder
181            .as_mut()
182            .ok_or_else(|| OverlayError("No command encoder available".to_string()))?;
183
184        // Render
185        let mut egui_render_state = EguiRenderState {
186            device: &gc.device,
187            queue: &gc.queue,
188            encoder,
189            target_view: &state.target_view,
190            width_px: self.screen.width_px,
191            height_px: self.screen.height_px,
192        };
193
194        self.renderer
195            .render(&mut egui_render_state, &primitives, pixels_per_point);
196
197        Ok(())
198    }
199
200    fn wants_pointer_input(&self) -> bool {
201        self.ctx.egui_wants_pointer_input()
202    }
203
204    fn wants_keyboard_input(&self) -> bool {
205        self.ctx.egui_wants_keyboard_input()
206    }
207
208    fn as_any(&self) -> &dyn Any {
209        self
210    }
211
212    fn as_any_mut(&mut self) -> &mut dyn Any {
213        self
214    }
215}
216
217// SAFETY: egui::Context is Send+Sync (Arc-based). EguiWgpuRenderer holds only
218// wgpu types which are Send+Sync. egui_winit::State requires Send which it
219// satisfies on desktop platforms.
220unsafe impl Send for EguiOverlay {}
221unsafe impl Sync for EguiOverlay {}