1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
//! A renderer for egui UI using wgpu.
use crate::context::Context;
use egui::{Context as EguiContext, RawInput};
use egui_wgpu::RendererOptions;
/// Structure which manages the egui UI rendering.
pub struct EguiRenderer {
egui_ctx: EguiContext,
renderer: egui_wgpu::Renderer,
shapes: Vec<egui::epaint::ClippedShape>,
textures_delta: egui::TexturesDelta,
}
impl EguiRenderer {
/// Creates a new egui renderer.
pub fn new() -> EguiRenderer {
let egui_ctx = EguiContext::default();
// Load fonts manually - use kiss3d's embedded font
let mut fonts = egui::FontDefinitions::default();
// Add WorkSans font from kiss3d
fonts.font_data.insert(
"WorkSans".to_owned(),
egui::FontData::from_static(include_bytes!("../text/WorkSans-Regular.ttf")).into(),
);
// Set it as the proportional font
fonts
.families
.get_mut(&egui::FontFamily::Proportional)
.unwrap()
.insert(0, "WorkSans".to_owned());
// Set it as the monospace font too
fonts
.families
.get_mut(&egui::FontFamily::Monospace)
.unwrap()
.insert(0, "WorkSans".to_owned());
egui_ctx.set_fonts(fonts);
// Set default pixels_per_point to avoid DPI warnings.
// Not using 1.0 exactly so that draw_ui() gets a chance
// to initialize it to the actual value (which might be 1)
// and trigger a redraw.
egui_ctx.set_pixels_per_point(0.987);
// Run a dummy frame to initialize fonts with correct DPI
let dummy_input = RawInput::default();
egui_ctx.begin_pass(dummy_input);
let mut dummy_output = egui_ctx.end_pass();
let ctxt = Context::get();
// Create the egui-wgpu renderer
let mut renderer = egui_wgpu::Renderer::new(
&ctxt.device,
ctxt.surface_format,
RendererOptions {
msaa_samples: 1,
dithering: true,
..Default::default()
},
);
// Apply textures from the dummy pass (font textures, etc.)
for (id, image_deltas) in &dummy_output.textures_delta.set {
for image_delta in image_deltas {
renderer.update_texture(&ctxt.device, &ctxt.queue, *id, image_delta);
}
}
// `TexturesDelta` asserts on drop that every delta was handled.
dummy_output.textures_delta.clear();
EguiRenderer {
egui_ctx,
renderer,
shapes: Vec::new(),
textures_delta: Default::default(),
}
}
/// Get a mutable reference to the egui Context.
pub fn context_mut(&mut self) -> &mut EguiContext {
&mut self.egui_ctx
}
/// Get a reference to the egui Context.
pub fn context(&self) -> &EguiContext {
&self.egui_ctx
}
/// Begin a new frame with the given raw input.
pub fn begin_frame(&mut self, raw_input: RawInput) {
self.egui_ctx.begin_pass(raw_input);
}
/// End the current frame and prepare for rendering.
pub fn end_frame(&mut self) {
let output = self.egui_ctx.end_pass();
self.shapes = output.shapes;
// Append rather than replace: if a previous frame's render was skipped
// (e.g. failed to acquire surface texture), we must not lose its texture
// deltas (such as the font atlas glyph upload).
self.textures_delta.append(output.textures_delta);
}
/// Registers a native wgpu texture view with egui, returning a
/// [`egui::TextureId`] that `ui.image((id, size))` can draw — no CPU copy
/// involved. The texture stays registered until
/// [`Self::unregister_native_texture`].
pub fn register_native_texture(
&mut self,
view: &wgpu::TextureView,
filter: wgpu::FilterMode,
) -> egui::TextureId {
let ctxt = Context::get();
self.renderer
.register_native_texture(&ctxt.device, view, filter)
}
/// Frees a texture id previously returned by
/// [`Self::register_native_texture`].
pub fn unregister_native_texture(&mut self, id: egui::TextureId) {
self.renderer.free_texture(&id);
}
/// Returns true if egui wants to capture the mouse (e.g., hovering over a widget).
///
/// This deliberately does not use egui's own `egui_wants_pointer_input`. That
/// one treats the whole background layer as egui's whenever
/// `root_ui_available_rect` is unset, and egui only fills that rect in from
/// its closure-based `Context::run_ui`; a `begin_pass`/`end_pass` integration
/// like ours always leaves it unset. Up to egui 0.35 a legacy fallback still
/// gave the right answer, but 0.36 dropped it, so egui started claiming every
/// hover and scroll over the empty 3D viewport and the camera stopped seeing
/// them.
pub fn wants_pointer_input(&self) -> bool {
egui_captures_pointer(&self.egui_ctx)
}
/// Returns true if egui wants to capture keyboard input (e.g., text input focused).
pub fn wants_keyboard_input(&self) -> bool {
self.egui_ctx.egui_wants_keyboard_input()
}
/// Actually renders the UI.
pub fn render(
&mut self,
color_view: &wgpu::TextureView,
_depth_view: &wgpu::TextureView,
width: u32,
height: u32,
scale_factor: f32,
) {
let ctxt = Context::get();
// Update textures
for (id, image_deltas) in &self.textures_delta.set {
for image_delta in image_deltas {
self.renderer
.update_texture(&ctxt.device, &ctxt.queue, *id, image_delta);
}
}
// Prepare clipped primitives
let clipped_primitives = self.egui_ctx.tessellate(self.shapes.clone(), scale_factor);
// Create screen descriptor
let screen_descriptor = egui_wgpu::ScreenDescriptor {
size_in_pixels: [width, height],
pixels_per_point: scale_factor,
};
// Create our own encoder for egui rendering to avoid lifetime issues
let mut encoder = ctxt.create_command_encoder(Some("egui_command_encoder"));
// Update buffers
self.renderer.update_buffers(
&ctxt.device,
&ctxt.queue,
&mut encoder,
&clipped_primitives,
&screen_descriptor,
);
// Render
{
// egui doesn't need depth testing - it renders 2D overlays
let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("egui_render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
multiview_mask: None,
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
// egui-wgpu requires 'static lifetime, so we use forget_lifetime
// SAFETY: The render pass will be dropped before the encoder is finished,
// and we don't use the encoder for anything else after this.
let mut render_pass = render_pass.forget_lifetime();
self.renderer
.render(&mut render_pass, &clipped_primitives, &screen_descriptor);
}
// Submit the egui commands
ctxt.submit(std::iter::once(encoder.finish()));
// Free textures
for id in &self.textures_delta.free {
self.renderer.free_texture(id);
}
self.textures_delta.clear();
self.shapes.clear();
}
}
impl Default for EguiRenderer {
fn default() -> Self {
Self::new()
}
}
impl Drop for EguiRenderer {
fn drop(&mut self) {
// `TexturesDelta` asserts on drop that every delta was handled. Deltas can
// still be pending here if the last frame ended without being rendered.
self.textures_delta.clear();
}
}
/// Whether egui should consume pointer events, given the current state of `ctx`.
fn egui_captures_pointer(ctx: &EguiContext) -> bool {
// Actively driving a widget (dragging a slider, holding a button down): egui
// owns the pointer wherever it goes.
if ctx.egui_is_using_pointer() {
return true;
}
// A drag that began outside egui stays with the app for its whole duration,
// even if it wanders over a window.
if ctx.input(|i| i.pointer.any_down()) {
return false;
}
// Merely hovering: capture only over an actual egui area. Everything egui
// draws on its own layers (windows, popups, menus, tooltips) sits above
// `Order::Background`, which is the catch-all covering the whole viewport.
ctx.input(|i| i.pointer.interact_pos())
.and_then(|pos| ctx.layer_id_at(pos))
.is_some_and(|layer| layer.order != egui::Order::Background)
}
#[cfg(test)]
mod tests {
use super::*;
const SCREEN: egui::Vec2 = egui::vec2(800.0, 600.0);
const WINDOW_POS: egui::Pos2 = egui::pos2(10.0, 10.0);
const OVER_WINDOW: egui::Pos2 = egui::pos2(60.0, 60.0);
const OVER_VIEWPORT: egui::Pos2 = egui::pos2(600.0, 500.0);
/// Drives a context the way `EguiRenderer` does (manual `begin_pass` /
/// `end_pass`, UI drawn as an `egui::Window`) and reports whether egui would
/// swallow the pointer.
fn captures_at(pointer: egui::Pos2, button_down: bool) -> bool {
let ctx = EguiContext::default();
ctx.set_pixels_per_point(1.0);
// A few passes, so the layer rects from an earlier pass are available.
for _ in 0..3 {
let mut events = vec![egui::Event::PointerMoved(pointer)];
if button_down {
events.push(egui::Event::PointerButton {
pos: pointer,
button: egui::PointerButton::Primary,
pressed: true,
modifiers: Default::default(),
});
}
ctx.begin_pass(RawInput {
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, SCREEN)),
events,
..Default::default()
});
egui::Window::new("panel")
.fixed_pos(WINDOW_POS)
.fixed_size(egui::vec2(200.0, 120.0))
.show(&ctx, |ui| {
ui.label("hello");
let _ = ui.button("click me");
});
ctx.end_pass().textures_delta.clear();
}
egui_captures_pointer(&ctx)
}
// egui 0.36 dropped the legacy fallback in `egui_wants_pointer_input`, which
// made it claim the pointer anywhere over the background layer. That starved
// the cameras of every hover and scroll event over the 3D viewport: zoom went
// dead and `last_cursor_pos` only advanced during drags, so each new click
// teleported the camera.
#[test]
fn viewport_pointer_is_left_to_the_camera() {
assert!(!captures_at(OVER_VIEWPORT, false));
assert!(!captures_at(OVER_VIEWPORT, true));
}
#[test]
fn pointer_over_a_widget_goes_to_egui() {
assert!(captures_at(OVER_WINDOW, false));
assert!(captures_at(OVER_WINDOW, true));
}
}