Skip to main content

polyscope_ui/
integration.rs

1//! egui integration with wgpu and winit.
2
3use egui::Context;
4use egui_wgpu::Renderer as EguiRenderer;
5use egui_wgpu::ScreenDescriptor;
6use egui_winit::State as EguiWinitState;
7use winit::event::WindowEvent;
8use winit::window::Window;
9
10/// Manages egui state and rendering.
11pub struct EguiIntegration {
12    pub context: Context,
13    pub state: EguiWinitState,
14    pub renderer: EguiRenderer,
15    /// Stored raw input from the most recent `begin_frame`, used for multi-pass rerun.
16    last_raw_input: egui::RawInput,
17}
18
19impl EguiIntegration {
20    /// Creates a new egui integration.
21    #[must_use]
22    pub fn new(device: &wgpu::Device, output_format: wgpu::TextureFormat, window: &Window) -> Self {
23        let context = Context::default();
24
25        // Configure dark theme
26        context.set_visuals(egui::Visuals::dark());
27
28        let viewport_id = context.viewport_id();
29        let state = EguiWinitState::new(context.clone(), viewport_id, window, None, None, None);
30
31        let renderer =
32            EguiRenderer::new(device, output_format, egui_wgpu::RendererOptions::default());
33
34        Self {
35            context,
36            state,
37            renderer,
38            last_raw_input: egui::RawInput::default(),
39        }
40    }
41
42    /// Handles a winit window event.
43    /// Returns true if egui consumed the event.
44    pub fn handle_event(&mut self, window: &Window, event: &WindowEvent) -> bool {
45        let response = self.state.on_window_event(window, event);
46        response.consumed
47    }
48
49    /// Begins a new frame by collecting input events and starting the egui pass.
50    pub fn begin_frame(&mut self, window: &Window) {
51        let raw_input = self.state.take_egui_input(window);
52        self.last_raw_input = raw_input.clone();
53        self.context.begin_pass(raw_input);
54    }
55
56    /// Begins a re-run pass for multi-pass layout.
57    ///
58    /// egui's `Grid` widget makes itself invisible on the first frame it appears
59    /// (a sizing pass) and calls `ctx.request_discard()` expecting a second pass.
60    /// This method starts that second pass using `take()` on the stored raw input,
61    /// which preserves `time`/`screen_rect`/`modifiers` but clears events — matching
62    /// the approach used by `Context::run()`.
63    pub fn begin_rerun_pass(&mut self) {
64        self.context.begin_pass(self.last_raw_input.take());
65    }
66
67    /// Ends the current egui pass and returns the output without handling platform events.
68    ///
69    /// Use this in a multi-pass loop. Call [`Self::handle_platform_output`] once
70    /// after the final pass.
71    pub fn end_pass(&mut self) -> egui::FullOutput {
72        self.context.end_pass()
73    }
74
75    /// Handles platform output (clipboard, cursor, IME, etc.).
76    pub fn handle_platform_output(&mut self, window: &Window, output: &egui::PlatformOutput) {
77        self.state.handle_platform_output(window, output.clone());
78    }
79
80    /// Ends the frame and returns paint jobs (convenience for single-pass usage).
81    pub fn end_frame(&mut self, window: &Window) -> egui::FullOutput {
82        let output = self.end_pass();
83        self.handle_platform_output(window, &output.platform_output);
84        output
85    }
86
87    /// Renders egui to the given render pass.
88    pub fn render(
89        &mut self,
90        device: &wgpu::Device,
91        queue: &wgpu::Queue,
92        encoder: &mut wgpu::CommandEncoder,
93        view: &wgpu::TextureView,
94        screen_descriptor: &ScreenDescriptor,
95        output: egui::FullOutput,
96    ) {
97        let paint_jobs = self
98            .context
99            .tessellate(output.shapes, output.pixels_per_point);
100
101        for (id, image_delta) in &output.textures_delta.set {
102            self.renderer
103                .update_texture(device, queue, *id, image_delta);
104        }
105
106        self.renderer
107            .update_buffers(device, queue, encoder, &paint_jobs, screen_descriptor);
108
109        {
110            let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
111                label: Some("egui render pass"),
112                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
113                    view,
114                    resolve_target: None,
115                    ops: wgpu::Operations {
116                        load: wgpu::LoadOp::Load, // Don't clear - render on top
117                        store: wgpu::StoreOp::Store,
118                    },
119                    depth_slice: None,
120                })],
121                depth_stencil_attachment: None,
122                ..Default::default()
123            });
124
125            // Convert to 'static lifetime as required by egui-wgpu's render method
126            let mut render_pass = render_pass.forget_lifetime();
127
128            self.renderer
129                .render(&mut render_pass, &paint_jobs, screen_descriptor);
130        }
131
132        for id in &output.textures_delta.free {
133            self.renderer.free_texture(id);
134        }
135    }
136}