egui-winit 0.0.0-alpha.1

egui platform support for winit
//! [egui](https://github.com/emilk/egui) + [winit](https://github.com/rust-windowing/winit)

pub mod util;

use egui::{math::vec2, paint::ClippedShape, CtxRef, Pos2};

use winit::event::Event;

use clipboard::{ClipboardContext, ClipboardProvider as _};
use tracing::error;

const SCROLL_LINE: f32 = 24.0;

/// [`Platform`] creation configuration
pub struct PlatformConfig {
    /// Width of the window in physical pixel.
    pub inital_width: u32,
    /// Height of the window in physical pixel.
    pub inital_height: u32,
    /// HiDPI scale factor.
    pub scale_factor: f64,
    /// Egui style configuration.
    pub style: egui::Style,
    /// Egui font configuration.
    pub font_definitions: egui::FontDefinitions,
}

/// egui platform support for winit.
pub struct Platform {
    context: CtxRef,

    raw_input: egui::RawInput,
    pointer_pos: egui::Pos2,
    modifier_state: winit::event::ModifiersState,
    start_instant: std::time::Instant,

    scale_factor: f64,
    clipboard: Option<ClipboardContext>,
}

// public API
impl Platform {
    /// Create a new [`Platform`].
    pub fn new(descriptor: PlatformConfig) -> Self {
        let context = CtxRef::default();
        context.set_style(descriptor.style);
        context.set_fonts(descriptor.font_definitions);

        let raw_input = egui::RawInput {
            pixels_per_point: Some(descriptor.scale_factor as f32),
            screen_rect: Some(egui::Rect::from_min_size(
                Default::default(),
                vec2(
                    descriptor.inital_width as f32,
                    descriptor.inital_height as f32,
                ) / descriptor.scale_factor as f32,
            )),
            ..Default::default()
        };

        let pointer_pos = Default::default();
        let modifier_state = winit::event::ModifiersState::empty();
        let start_instant = std::time::Instant::now();
        let scale_factor = descriptor.scale_factor;
        let clipboard = Self::init_clipboard();

        Self {
            context,

            raw_input,
            pointer_pos,
            modifier_state,
            start_instant,

            scale_factor,
            clipboard,
        }
    }

    /// Handles winit events and passes them to egui.
    pub fn handle_event<T>(&mut self, event: &Event<T>) -> bool {
        use winit::event::WindowEvent::*;

        if let Event::WindowEvent { event, .. } = event {
            match event {
                Resized(physical_size) => {
                    self.raw_input.screen_rect = Some(egui::Rect::from_min_size(
                        Pos2::ZERO,
                        vec2(physical_size.width as f32, physical_size.height as f32)
                            / self.scale_factor as f32,
                    ));
                    false
                }
                ScaleFactorChanged {
                    scale_factor,
                    new_inner_size,
                } => {
                    self.scale_factor = *scale_factor;
                    self.raw_input.pixels_per_point = Some(*scale_factor as f32);
                    self.raw_input.screen_rect = Some(egui::Rect::from_min_size(
                        Pos2::ZERO,
                        vec2(new_inner_size.width as f32, new_inner_size.height as f32)
                            / self.scale_factor as f32,
                    ));
                    false
                }
                MouseInput { state, button, .. } => {
                    if let Some(button) = util::translate::mouse_button_w2e(*button) {
                        self.raw_input.events.push(egui::Event::PointerButton {
                            pos: self.pointer_pos,
                            button,
                            pressed: matches!(*state, winit::event::ElementState::Pressed),
                            modifiers: Default::default(),
                        });
                    }
                    false
                }
                MouseWheel { delta, .. } => match delta {
                    winit::event::MouseScrollDelta::LineDelta(x, y) => {
                        self.raw_input.scroll_delta = vec2(*x, *y) * SCROLL_LINE;
                        self.context().wants_pointer_input()
                    }
                    winit::event::MouseScrollDelta::PixelDelta(delta) => {
                        self.raw_input.scroll_delta = vec2(delta.x as f32, delta.y as f32);
                        self.context().wants_pointer_input()
                    }
                },
                CursorMoved { position, .. } => {
                    self.pointer_pos =
                        util::translate::pos_w2e(position.to_logical(self.scale_factor));
                    self.raw_input
                        .events
                        .push(egui::Event::PointerMoved(self.pointer_pos));
                    self.context().is_using_pointer()
                }
                CursorLeft { .. } => {
                    self.raw_input.events.push(egui::Event::PointerGone);
                    false
                }
                ModifiersChanged(input) => {
                    self.modifier_state = *input;
                    self.context().wants_keyboard_input()
                }
                KeyboardInput {
                    input:
                        winit::event::KeyboardInput {
                            virtual_keycode: Some(key),
                            state,
                            ..
                        },
                    ..
                } => {
                    if let Some(event) = self.handle_key(*key, *state) {
                        self.raw_input.events.push(event);
                    }
                    self.context().wants_keyboard_input()
                }
                ReceivedCharacter(ch) => {
                    if util::is_egui_printable(*ch)
                        && !self.modifier_state.ctrl()
                        && !self.modifier_state.logo()
                    {
                        self.raw_input
                            .events
                            .push(egui::Event::Text(ch.to_string()));
                    }
                    self.context().wants_keyboard_input()
                }
                _ => false,
            }
        } else {
            false
        }
    }

    /// Starts a new frame.
    pub fn begin_frame(&mut self) {
        self.raw_input.time = Some(self.start_instant.elapsed().as_secs_f64());

        self.context.begin_frame(self.raw_input.take());
    }

    /// Ends the frame.
    /// Returns the shapes to tessellate and draw and whetever a repaint is needed or not.
    pub fn end_frame(&mut self, window: &winit::window::Window) -> (Vec<ClippedShape>, bool) {
        let (
            egui::Output {
                cursor_icon,
                open_url,
                copied_text,
                needs_repaint,
                events: _,
                text_cursor: _,
            },
            shapes,
        ) = self.context.end_frame();
        Self::handle_cursor_icon(cursor_icon, window);
        Self::handle_copied_text(copied_text, self.clipboard.as_mut());
        Self::handle_url(open_url);

        (shapes, needs_repaint)
    }

    /// Returns the internal egui context.
    pub fn context(&self) -> CtxRef {
        self.context.clone()
    }
}

// private implementation
impl Platform {
    fn init_clipboard() -> Option<ClipboardContext> {
        match ClipboardContext::new() {
            Ok(c) => Some(c),
            Err(e) => {
                error!("Failed to initalize clipboard support: {}", e);
                None
            }
        }
    }

    fn handle_key(
        &mut self,
        key: winit::event::VirtualKeyCode,
        state: winit::event::ElementState,
    ) -> Option<egui::Event> {
        use winit::event::VirtualKeyCode;
        match key {
            VirtualKeyCode::Copy => Some(egui::Event::Copy),
            VirtualKeyCode::Cut => Some(egui::Event::Cut),
            VirtualKeyCode::Paste => self
                .clipboard
                .as_mut()
                .and_then(|c| match c.get_contents() {
                    Ok(c) => Some(c),
                    Err(e) => {
                        error!("Failed to get clipboard contents: {}", e);
                        None
                    }
                })
                .map(egui::Event::Text),
            key => util::translate::key_w2e(key).map(|key| egui::Event::Key {
                key,
                pressed: matches!(state, winit::event::ElementState::Pressed),
                modifiers: util::translate::modifiers_w2e(self.modifier_state),
            }),
        }
    }

    fn handle_cursor_icon(cursor_icon: egui::CursorIcon, window: &winit::window::Window) {
        window.set_cursor_icon(util::translate::cursor_icon_e2w(cursor_icon));
    }

    fn handle_copied_text(copied_text: String, clipboard: Option<&mut ClipboardContext>) {
        if !copied_text.is_empty() {
            if let Some(clipboard) = clipboard {
                if let Err(err) = clipboard.set_contents(copied_text) {
                    error!("Failed to set clipoard contents: {}", err);
                }
            }
        }
    }

    fn handle_url(url: Option<egui::output::OpenUrl>) {
        if let Some(url) = url {
            // TODO: use `url.new_tab`
            if let Err(err) = webbrowser::open(&url.url) {
                error!("Failed to open url: {}", err);
            }
        }
    }
}