popout 0.1.1

Small and simple modal popups powered by egui
Documentation
use super::Result;
use crate::Error;
use crate::renderer::EguiRenderer;
use egui::Vec2;
use egui_wgpu::wgpu::SurfaceError;
use egui_wgpu::{ScreenDescriptor, wgpu};
use std::sync::Arc;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::ActiveEventLoop;
use winit::window::{Window, WindowAttributes, WindowId};

pub struct State {
    pub device: wgpu::Device,
    pub queue: wgpu::Queue,
    pub surface_config: wgpu::SurfaceConfiguration,
    pub surface: wgpu::Surface<'static>,
    pub scale_factor: f32,
    pub egui_renderer: EguiRenderer,
}

impl State {
    async fn new(
        instance: &wgpu::Instance,
        surface: wgpu::Surface<'static>,
        window: &winit::window::Window,
        width: u32,
        height: u32,
    ) -> Result<Self> {
        let power_pref = wgpu::PowerPreference::default();
        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: power_pref,
                force_fallback_adapter: false,
                compatible_surface: Some(&surface),
            })
            .await
            .ok_or(Error::FailedToFindAdapter)?;

        let features = wgpu::Features::empty();
        let (device, queue) = adapter
            .request_device(
                &wgpu::DeviceDescriptor {
                    label: None,
                    required_features: features,
                    required_limits: Default::default(),
                    memory_hints: Default::default(),
                },
                None,
            )
            .await?;

        let swapchain_capabilities = surface.get_capabilities(&adapter);
        let selected_format = wgpu::TextureFormat::Bgra8UnormSrgb;
        let swapchain_format = swapchain_capabilities
            .formats
            .iter()
            .find(|d| **d == selected_format)
            .ok_or(Error::FailedToSelectSurfaceTextureFormat)?;

        let surface_config = wgpu::SurfaceConfiguration {
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            format: *swapchain_format,
            width,
            height,
            present_mode: wgpu::PresentMode::AutoVsync,
            desired_maximum_frame_latency: 0,
            alpha_mode: swapchain_capabilities.alpha_modes[0],
            view_formats: vec![],
        };

        surface.configure(&device, &surface_config);

        let egui_renderer = EguiRenderer::new(&device, surface_config.format, None, 1, window);

        let scale_factor = 1.0;

        Ok(Self {
            device,
            queue,
            surface,
            surface_config,
            egui_renderer,
            scale_factor,
        })
    }

    fn resize_surface(&mut self, width: u32, height: u32) {
        self.surface_config.width = width;
        self.surface_config.height = height;
        self.surface.configure(&self.device, &self.surface_config);
    }
}

pub struct PopupWindow<F, R> {
    instance: wgpu::Instance,
    state: Option<State>,
    window: Option<Arc<Window>>,
    pub draw_result: Option<R>,
    draw_fn: F,
    min_sz: Option<Vec2>,
    window_info: WindowAttributes,
    pub error: Option<Error>,
}

impl<F, R> PopupWindow<F, R>
where
    F: FnMut(&mut egui::Ui) -> Option<R>,
{
    pub fn new(draw_fn: F, window_info: WindowAttributes) -> Self {
        let instance = egui_wgpu::wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
        Self {
            instance,
            state: None,
            window: None,
            draw_result: None,
            draw_fn,
            min_sz: None,
            window_info,
            error: None,
        }
    }

    async fn set_window(&mut self, window: Window) {
        assert!(self.error.is_none());
        let window = Arc::new(window);
        let initial_sz = window.inner_size();

        let surface = match self.instance.create_surface(window.clone()) {
            Ok(v) => v,
            Err(e) => {
                self.error.replace(e.into());
                return;
            }
        };

        let state = match State::new(
            &self.instance,
            surface,
            &window,
            initial_sz.width,
            initial_sz.height,
        )
        .await
        {
            Ok(v) => v,
            Err(e) => {
                self.error.replace(e);
                return;
            }
        };

        self.window.get_or_insert(window);
        self.state.get_or_insert(state);
    }

    fn handle_resized(&mut self, width: u32, height: u32) {
        if width > 0 && height > 0 {
            let min_sz = self.min_sz.unwrap_or(Vec2::ZERO);
            let width = width.max(min_sz.x as u32);
            let height = height.max(min_sz.y as u32);
            self.state.as_mut().unwrap().resize_surface(width, height);
        }
    }

    fn handle_redraw(&mut self) -> Option<R> {
        assert!(self.error.is_none());
        // Attempt to handle minimizing window
        if let Some(window) = self.window.as_ref() {
            if let Some(min) = window.is_minimized() {
                if min {
                    tracing::debug!("redraw called while window is minimized, ignoring");
                    return None;
                }
            }
        }

        let state = self.state.as_mut().unwrap();

        let screen_descriptor = ScreenDescriptor {
            size_in_pixels: [state.surface_config.width, state.surface_config.height],
            pixels_per_point: self.window.as_ref().unwrap().scale_factor() as f32
                * state.scale_factor,
        };

        let surface_texture = state.surface.get_current_texture();

        match surface_texture {
            Err(SurfaceError::Outdated) => {
                // Ignoring outdated to allow resizing and minimization
                tracing::debug!("wgpu surface outdated");
                return None;
            }
            Err(e) => {
                self.error.replace(e.into());
                return None;
            }
            Ok(_) => {}
        };

        let surface_texture = surface_texture.unwrap();

        let surface_view = surface_texture
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());

        let mut encoder = state
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });

        let window = self.window.as_ref().unwrap();

        let mut out = None;
        {
            state.egui_renderer.begin_frame(window);

            egui::CentralPanel::default().show(state.egui_renderer.context(), |ui| {
                out = (self.draw_fn)(ui);
            });

            state.egui_renderer.end_frame_and_draw(
                &state.device,
                &state.queue,
                &mut encoder,
                window,
                &surface_view,
                screen_descriptor,
            );
        };

        state.queue.submit(Some(encoder.finish()));
        surface_texture.present();

        out
    }

    fn close(&mut self, event_loop: &ActiveEventLoop) {
        self.state.take();
        assert!(
            Arc::strong_count(self.window.as_ref().unwrap()) <= 1,
            "we are not the last window reference?"
        );
        self.window.take();
        event_loop.exit();
    }
}

impl<F, R> ApplicationHandler for PopupWindow<F, R>
where
    F: FnMut(&mut egui::Ui) -> Option<R>,
{
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.error.is_some() {
            return;
        }
        let window = event_loop.create_window(self.window_info.clone()).unwrap();
        pollster::block_on(self.set_window(window));
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
        if self.error.is_some() {
            self.close(event_loop);
        }
        let Some(win) = self.window.as_ref() else {
            return;
        };

        // let egui render to process the event first
        self.state
            .as_mut()
            .unwrap()
            .egui_renderer
            .handle_input(win, &event);

        match event {
            WindowEvent::CloseRequested => {
                self.close(event_loop);
            }
            WindowEvent::RedrawRequested => {
                self.draw_result = self.handle_redraw();
                if self.draw_result.is_some() {
                    self.close(event_loop);
                } else {
                    self.window.as_ref().unwrap().request_redraw();
                }
            }
            WindowEvent::Resized(new_size) => {
                //self.resized = true;
                self.handle_resized(new_size.width, new_size.height);
            }
            _ => (),
        }
    }
}