f56a1eea3b
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
5.4 KiB
Rust
141 lines
5.4 KiB
Rust
//! egui ⇄ engine glue for the editor.
|
|
//!
|
|
//! The engine core stays UI-agnostic; all egui wiring lives here in the editor.
|
|
//! [`EguiLayer`] owns the [`egui_winit`] input state and the [`egui_wgpu`]
|
|
//! renderer, translates window events, and paints a built UI into the frame's
|
|
//! surface view (recorded with `LoadOp::Load`, so it composites on top of the
|
|
//! engine's clear).
|
|
|
|
use egui_wgpu::{Renderer, RendererOptions, ScreenDescriptor};
|
|
use egui_winit::State;
|
|
use oxide_engine::wgpu;
|
|
use oxide_engine::winit::event::WindowEvent;
|
|
use oxide_engine::winit::window::Window;
|
|
|
|
/// Holds the egui input state and GPU renderer for one window.
|
|
pub struct EguiLayer {
|
|
state: State,
|
|
renderer: Renderer,
|
|
}
|
|
|
|
impl EguiLayer {
|
|
/// Creates the layer for `window`, building a renderer that targets the
|
|
/// given surface format.
|
|
pub fn new(
|
|
window: &Window,
|
|
device: &wgpu::Device,
|
|
surface_format: wgpu::TextureFormat,
|
|
) -> Self {
|
|
let context = egui::Context::default();
|
|
let state = State::new(
|
|
context,
|
|
egui::ViewportId::ROOT,
|
|
window,
|
|
Some(window.scale_factor() as f32),
|
|
None,
|
|
None,
|
|
);
|
|
// Defaults: no MSAA, no depth/stencil, dithering on — matches the
|
|
// editor's flat clear-color surface.
|
|
let renderer = Renderer::new(device, surface_format, RendererOptions::default());
|
|
Self { state, renderer }
|
|
}
|
|
|
|
/// Feeds a window event to egui. Returns `true` if egui consumed it (e.g.
|
|
/// a click landed on a panel), so the caller can suppress its own handling.
|
|
pub fn on_window_event(&mut self, window: &Window, event: &WindowEvent) -> bool {
|
|
self.state.on_window_event(window, event).consumed
|
|
}
|
|
|
|
/// Whether the pointer is currently over a **floating** egui area — a
|
|
/// `Window` (Preferences, Layer Names, Groups, …) or other non-background
|
|
/// layer — rather than empty space or the background dock.
|
|
///
|
|
/// The viewport is painted under a transparent dock area (background
|
|
/// order), so a geometric "cursor inside the viewport rect" test can't tell
|
|
/// that a floating panel is sitting on top of it. The host uses this to
|
|
/// suppress viewport orbit/pan/zoom (and stray WASD while typing in a panel
|
|
/// that overlaps the viewport).
|
|
pub fn pointer_over_floating(&self) -> bool {
|
|
let ctx = self.state.egui_ctx();
|
|
let Some(pos) = ctx.pointer_latest_pos() else {
|
|
return false;
|
|
};
|
|
ctx.layer_id_at(pos)
|
|
.map(|layer| layer.order > egui::Order::Background)
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
/// Builds the UI via `build_ui` and paints it into `view`.
|
|
///
|
|
/// `build_ui` receives the root [`egui::Ui`]; panels are shown inside it
|
|
/// (egui 0.34's `show_inside` model). It may be called more than once per
|
|
/// frame if egui needs an extra layout pass, so it must be idempotent.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn paint(
|
|
&mut self,
|
|
window: &Window,
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
view: &wgpu::TextureView,
|
|
size: (u32, u32),
|
|
build_ui: impl FnMut(&mut egui::Ui),
|
|
) {
|
|
let raw_input = self.state.take_egui_input(window);
|
|
let context = self.state.egui_ctx().clone();
|
|
let output = context.run_ui(raw_input, build_ui);
|
|
self.state
|
|
.handle_platform_output(window, output.platform_output);
|
|
|
|
let primitives = context.tessellate(output.shapes, output.pixels_per_point);
|
|
let screen = ScreenDescriptor {
|
|
size_in_pixels: [size.0.max(1), size.1.max(1)],
|
|
pixels_per_point: output.pixels_per_point,
|
|
};
|
|
|
|
for (id, delta) in &output.textures_delta.set {
|
|
self.renderer.update_texture(device, queue, *id, delta);
|
|
}
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("oxide.editor.egui.encoder"),
|
|
});
|
|
// egui may emit its own command buffers (for paint callbacks); submit
|
|
// those ahead of our pass.
|
|
let user_buffers =
|
|
self.renderer
|
|
.update_buffers(device, queue, &mut encoder, &primitives, &screen);
|
|
{
|
|
let pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
label: Some("oxide.editor.egui.pass"),
|
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
view,
|
|
resolve_target: None,
|
|
depth_slice: None,
|
|
ops: wgpu::Operations {
|
|
// Load: keep the engine's clear; draw the UI over it.
|
|
load: wgpu::LoadOp::Load,
|
|
store: wgpu::StoreOp::Store,
|
|
},
|
|
})],
|
|
depth_stencil_attachment: None,
|
|
timestamp_writes: None,
|
|
occlusion_query_set: None,
|
|
multiview_mask: None,
|
|
});
|
|
// egui-wgpu wants a 'static pass; the encoder outlives it here.
|
|
let mut pass = pass.forget_lifetime();
|
|
self.renderer.render(&mut pass, &primitives, &screen);
|
|
}
|
|
|
|
for id in &output.textures_delta.free {
|
|
self.renderer.free_texture(id);
|
|
}
|
|
queue.submit(
|
|
user_buffers
|
|
.into_iter()
|
|
.chain(std::iter::once(encoder.finish())),
|
|
);
|
|
}
|
|
}
|