egui-ark 0.38.1-pre.15

Bindings between the egui GUI library and ark
Documentation
use ark::applet::input;
use ark::render;
use ark::render::TextureFormat;
use ark::ColorRgba8;
use ark::Vec2;
use egui::FontData;
use egui::FontDefinitions;
use egui::FontFamily;
use std::collections::HashMap;

/// Bindings to the egui library.
///
/// Instantiate one [`EguiArk`] in your module.
///
/// ``` no_run
/// # fn applet() -> ark::applet::Applet { unimplemented!() }
/// # fn render() -> ark::render::Render { unimplemented!() }
/// let mut egui = EguiArk::default();
/// let ctx = egui.begin_frame(&applet());
/// ui(ctx);
/// let ctx = egui.end_frame(&render(), &applet());
///
/// fn ui(ctx: &mut egui::Context) {
///     egui::Window::new("My window").show(ctx, |ui| {
///         ui.label("Hello world!");
///     });
/// }
/// ```
pub struct EguiArk {
    input_mngr: input::InputManager,
    egui_input: egui::RawInput,
    ctx: egui::Context,
    painter: EguiPainter,
}

impl Default for EguiArk {
    fn default() -> EguiArk {
        let context = egui::Context::default();
        // Make sure to overwrite default font definitions to prevent inclusion of epaint licenses.
        context.set_fonts(font_definitions());

        EguiArk {
            input_mngr: input::InputManager::default(),
            egui_input: egui::RawInput::default(),
            ctx: context,
            painter: EguiPainter::default(),
        }
    }
}

impl EguiArk {
    /// Access to the egui context.
    pub fn ctx(&self) -> &egui::Context {
        &self.ctx
    }

    /// Call before using egui. This gathers input and prepares for a new frame.
    pub fn begin_frame(&mut self, applet: &ark::applet::Applet) -> egui::Context {
        ark::profiler::function!();
        if applet.window_state().is_some() {
            self.input_mngr.update(applet);
            update_input(&mut self.egui_input, applet, &self.input_mngr);
        }
        ark::profiler::scope!("egui::begin_frame");
        self.ctx.begin_frame(self.egui_input.take());
        self.ctx.clone()
    }

    /// Call after using egui. This will render the interface.
    pub fn end_frame(&mut self, render: &render::Render, applet: &ark::applet::Applet) {
        ark::profiler::function!();

        let full_output = {
            ark::profiler::scope!("egui::end_frame");
            self.ctx.end_frame()
        };

        let platform_output = full_output.platform_output;

        if applet.window_state().is_some() {
            let paint_jobs = {
                ark::profiler::scope!("egui::tessellate");
                self.ctx.tessellate(full_output.shapes)
            };
            if !platform_output.copied_text.is_empty() {
                applet.set_clipboard_string(&platform_output.copied_text);
            }

            if platform_output.cursor_icon == egui::CursorIcon::None {
                applet.set_cursor_mode(ark::applet::CursorMode::Hide);
            } else {
                applet.set_cursor_mode(ark::applet::CursorMode::None);
                applet.set_cursor_shape(from_egui_cursor(platform_output.cursor_icon));
            }

            self.painter
                .upload_font_textures(render, full_output.textures_delta);
            self.painter.paint(render, &self.ctx, &paint_jobs);
        }
    }

    /// True if egui is currently interested in the pointer (mouse/touch).
    /// Could be the mouse is hovering over a egui window,
    /// or the user is dragging an egui widget.
    /// If false, the mouse is outside of any egui area and so
    /// you may be interested in what it is doing (e.g. controlling your game).
    pub fn wants_pointer_input(&self) -> bool {
        self.ctx.wants_pointer_input()
    }

    /// If true, egui is currently listening on text input (e.g. typing text in a `TextEdit`).
    pub fn wants_keyboard_input(&self) -> bool {
        self.ctx.wants_keyboard_input()
    }

    pub fn is_mouse_down(&self) -> bool {
        self.ctx.input().pointer.any_down()
    }

    /// Save egui state (window positions etc)
    pub fn persist(&self) -> serde_json::Value {
        serde_json::to_value(&*self.ctx.memory()).unwrap_or_default()
    }

    /// Restore egui (window positions etc)
    pub fn restore(&self, value: serde_json::Value) {
        match serde_json::from_value(value) {
            Ok(memory) => {
                *self.ctx.memory() = memory;
            }
            Err(err) => {
                ark::warn!("Failed to restore egui state: {}", err);
            }
        }
    }

    /// Almost the same as `available_rect()`, however, in *physical* pixels, not *logical* (i.e.
    /// `available_rect()` scaled by the DPI factor).
    ///
    /// How much space is still available after panels has been added.
    /// This is the "background" area, what egui doesn't cover with panels (but may cover with windows).
    /// This is also the area to which windows are constrained.
    pub fn viewport(&self) -> ark::render::Rectangle {
        let available_rect = self.ctx.available_rect();
        let pixels_per_point = self.ctx.input().pixels_per_point();
        ark::render::Rectangle {
            min_x: available_rect.min.x * pixels_per_point,
            min_y: available_rect.min.y * pixels_per_point,
            max_x: available_rect.max.x * pixels_per_point,
            max_y: available_rect.max.y * pixels_per_point,
        }
    }
}

fn from_egui_cursor(cursor: egui::CursorIcon) -> ark::applet::CursorShape {
    use ark::applet::CursorShape;
    match cursor {
        egui::CursorIcon::None => unreachable!("Should have been handled outside this function"),
        egui::CursorIcon::Default => CursorShape::Default,
        egui::CursorIcon::ContextMenu => CursorShape::ContextMenu,
        egui::CursorIcon::Help => CursorShape::Help,
        egui::CursorIcon::PointingHand => CursorShape::Hand,
        egui::CursorIcon::Progress => CursorShape::Progress,
        egui::CursorIcon::Wait => CursorShape::Wait,
        egui::CursorIcon::Cell => CursorShape::Cell,
        egui::CursorIcon::Crosshair => CursorShape::Crosshair,
        egui::CursorIcon::Text => CursorShape::Text,
        egui::CursorIcon::VerticalText => CursorShape::VerticalText,
        egui::CursorIcon::Alias => CursorShape::Alias,
        egui::CursorIcon::Copy => CursorShape::Copy,
        egui::CursorIcon::Move => CursorShape::Move,
        egui::CursorIcon::NoDrop => CursorShape::NoDrop,
        egui::CursorIcon::NotAllowed => CursorShape::NotAllowed,
        egui::CursorIcon::Grab => CursorShape::Grab,
        egui::CursorIcon::Grabbing => CursorShape::Grabbing,
        egui::CursorIcon::AllScroll => CursorShape::AllScroll,
        egui::CursorIcon::ResizeHorizontal => CursorShape::EWResize,
        egui::CursorIcon::ResizeNeSw => CursorShape::NESWResize,
        egui::CursorIcon::ResizeNwSe => CursorShape::NWSEResize,
        egui::CursorIcon::ResizeVertical => CursorShape::NSResize,
        egui::CursorIcon::ZoomIn => CursorShape::ZoomIn,
        egui::CursorIcon::ZoomOut => CursorShape::ZoomOut,
        egui::CursorIcon::ResizeEast => CursorShape::EResize,
        egui::CursorIcon::ResizeSouthEast => CursorShape::SEResize,
        egui::CursorIcon::ResizeSouth => CursorShape::SResize,
        egui::CursorIcon::ResizeSouthWest => CursorShape::SWResize,
        egui::CursorIcon::ResizeWest => CursorShape::WResize,
        egui::CursorIcon::ResizeNorthWest => CursorShape::NWResize,
        egui::CursorIcon::ResizeNorth => CursorShape::NResize,
        egui::CursorIcon::ResizeNorthEast => CursorShape::NEResize,
        egui::CursorIcon::ResizeColumn => CursorShape::RowResize,
        egui::CursorIcon::ResizeRow => CursorShape::ColResize,
    }
}

// ----------------------------------------------------------------------------

impl serde::Serialize for EguiArk {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        // (&*self.ctx.memory()).serialize(serializer) // ERR: `KeyMustBeString` :(

        self.persist().serialize(serializer)
    }
}

impl<'de> serde::Deserialize<'de> for EguiArk {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let egui_ark = Self::default();

        // *egui_ark.ctx.memory() = egui::Memory::deserialize(deserializer)?; // Can't do this due to Flexbuffers bug

        egui_ark.restore(serde_json::Value::deserialize(deserializer)?);

        Ok(egui_ark)
    }
}

// ----------------------------------------------------------------------------

#[derive(Default)]
struct EguiPainter {
    /// The font textures used by egui
    egui_textures: HashMap<egui::TextureId, render::Texture>,

    // Reuse buffers to avoid allocations:
    positions: Vec<Vec2>,
    colors: Vec<ColorRgba8>,
    uvs: Vec<Vec2>,
}

impl EguiPainter {
    pub fn upload_font_textures(
        &mut self,
        render: &render::Render,
        textures_delta: egui::TexturesDelta,
    ) {
        ark::profiler::function!();
        for (texture_id, image_delta) in textures_delta.set {
            if let Some(pos) = image_delta.pos {
                // This is an update. Ignore for now until the API is added.
                if let Some(texture) = self.egui_textures.get_mut(&texture_id) {
                    // TODO
                    let pixels = get_rgba_pixels(&image_delta.image);
                    texture.update_rectangle(
                        pos[0] as u32,
                        pos[1] as u32,
                        image_delta.image.width() as u32,
                        image_delta.image.height() as u32,
                        &pixels,
                    );
                }
            } else {
                self.egui_textures
                    .insert(texture_id, as_ark_texture(render, &image_delta));
            }
        }

        for free in textures_delta.free {
            self.egui_textures.remove(&free);
        }
    }

    pub fn paint(
        &mut self,
        render: &render::Render,
        egui_ctx: &egui::Context,
        meshes: &[egui::ClippedPrimitive],
    ) {
        ark::profiler::function!();
        let dpi_factor = egui_ctx.input().pixels_per_point();
        for egui::ClippedPrimitive {
            primitive,
            clip_rect,
        } in meshes
        {
            self.paint_mesh(render, dpi_factor, clip_rect, primitive);
        }
    }

    fn paint_mesh(
        &mut self,
        render: &render::Render,
        dpi_factor: f32,
        clip_rect: &egui::Rect,
        primitive: &egui::epaint::Primitive,
    ) {
        ark::profiler::function!();

        let mesh = match primitive {
            egui::epaint::Primitive::Mesh(mesh) => mesh,
            _ => return,
        };

        if !mesh.is_valid() {
            ark::error!("egui generated an invalid triangle mesh");
            return;
        }

        self.positions.clear();
        self.colors.clear();
        self.uvs.clear();
        for v in &mesh.vertices {
            self.positions
                .push(dpi_factor * Vec2::new(v.pos.x, v.pos.y));
            self.colors.push(ColorRgba8(v.color.to_array()));
            self.uvs.push(Vec2::new(v.uv.x, v.uv.y));
        }

        let clip_rect = render::Rectangle {
            min_x: dpi_factor * clip_rect.min.x,
            min_y: dpi_factor * clip_rect.min.y,
            max_x: dpi_factor * clip_rect.max.x,
            max_y: dpi_factor * clip_rect.max.y,
        };

        let texture_handle = if let Some(texture) = self.egui_textures.get(&mesh.texture_id) {
            texture.handle()
        } else {
            return;
        };

        let indices: &[u32] = &mesh.indices;
        let indices: &[[u32; 3]] = unsafe { transmute_slice(indices) };
        assert_eq!(mesh.indices.len(), 3 * indices.len());

        ark::profiler::scope!("draw_textured_triangles");
        render.draw_textured_triangles(
            &clip_rect,
            texture_handle,
            indices,
            &self.positions,
            &self.colors,
            &self.uvs,
        );
    }
}

/// Convert e.g. &[[u32; 3]] to three times as long slice of &[u32]
#[allow(clippy::integer_division)]
unsafe fn transmute_slice<Target: Copy, Source: Copy>(source: &[Source]) -> &[Target] {
    use std::mem::size_of;

    let target_len = source.len() * size_of::<Source>() / size_of::<Target>();

    assert_eq!(
        target_len * size_of::<Target>(),
        source.len() * size_of::<Source>(),
        "Source slice length is not an even multiple of the target"
    );
    unsafe { std::slice::from_raw_parts(source.as_ptr().cast::<Target>(), target_len) }
}

fn get_rgba_pixels(image: &egui::epaint::ImageData) -> Vec<u8> {
    let mut pixels = Vec::with_capacity(image.width() * image.height() * 4);
    match image {
        egui::epaint::ImageData::Font(font) => {
            for srgba in font.srgba_pixels(Some(1.0)) {
                pixels.push(srgba[0]);
                pixels.push(srgba[1]);
                pixels.push(srgba[2]);
                pixels.push(srgba[3]);
            }
        }
        _ => {
            pixels.shrink_to_fit();
        }
    }

    pixels
}

fn as_ark_texture(
    render: &render::Render,
    image_delta: &egui::epaint::ImageDelta,
) -> render::Texture {
    let pixels = get_rgba_pixels(&image_delta.image);

    ark::profiler::scope!("render::Texture::create");
    render
        .create_texture()
        .name("egui")
        .dimensions(image_delta.image.width(), image_delta.image.height())
        .format(TextureFormat::R8G8B8A8_UNORM)
        .data(&pixels)
        .build()
        .expect("Failed to create egui texture")
}

/// Update the input given to egui with input pulled from ark
fn update_input(
    egui_input: &mut egui::RawInput,
    applet: &ark::applet::Applet,
    input_mngr: &input::InputManager,
) {
    ark::profiler::function!();
    let window_state = if let Some(window_state) = applet.window_state() {
        window_state
    } else {
        return;
    };

    egui_input.screen_rect = Some(egui::Rect::from_min_size(
        Default::default(),
        egui::vec2(window_state.width, window_state.height),
    ));
    egui_input.pixels_per_point = Some(window_state.dpi_factor);
    egui_input.time = Some(applet.real_time_since_start());
    egui_input.events.clear();
    egui_input.modifiers = as_egui_modifiers(&input_mngr.state().modifiers);

    for (state, event) in input_mngr.events() {
        match event {
            input::Event::Key { key, pressed } => {
                if let Some(key) = as_egui_key(*key) {
                    egui_input.events.push(egui::Event::Key {
                        pressed: *pressed,
                        key,
                        modifiers: as_egui_modifiers(&state.modifiers),
                    });
                }
            }
            input::Event::Char(chr) => {
                if *chr != '\r' {
                    egui_input.events.push(egui::Event::Text(chr.to_string()));
                }
            }

            input::Event::PointerMove { pos, primary, .. } => {
                if *primary {
                    egui_input
                        .events
                        .push(egui::Event::PointerMoved(egui::pos2(pos.x, pos.y)));
                }
            }
            input::Event::PointerButton {
                pressed,
                button,
                pos,
                ..
            } => {
                egui_input.events.push(egui::Event::PointerButton {
                    pos: egui::pos2(pos.x, pos.y),
                    button: as_egui_button(button),
                    pressed: *pressed,
                    modifiers: as_egui_modifiers(&state.modifiers),
                });
            }
            input::Event::PointerDelta { .. } => {}
            input::Event::Scroll { delta, .. } => {
                egui_input
                    .events
                    .push(egui::Event::Scroll(egui::vec2(delta.x, delta.y)));
            }
            input::Event::Command(command) => {
                match command {
                    input::Command::Copy => egui_input.events.push(egui::Event::Copy),
                    input::Command::Cut => egui_input.events.push(egui::Event::Cut),
                    input::Command::Paste => {
                        if let Some(s) = applet.clipboard_string() {
                            egui_input.events.push(egui::Event::Text(s));
                        }
                    }
                    // egui checks for Cmd+Z itself
                    input::Command::Undo | input::Command::Redo | input::Command::Save |
                    // egui doesn't care
                    input::Command::New => {}
                }
            }
            input::Event::Axis { .. }
            | input::Event::GamepadButton { .. }
            | input::Event::RawMidi { .. } => {}
        }
    }
}

fn as_egui_key(code: input::Key) -> Option<egui::Key> {
    match code {
        input::Key::Down => Some(egui::Key::ArrowDown),
        input::Key::Left => Some(egui::Key::ArrowLeft),
        input::Key::Right => Some(egui::Key::ArrowRight),
        input::Key::Up => Some(egui::Key::ArrowUp),

        input::Key::Escape => Some(egui::Key::Escape),
        input::Key::Tab => Some(egui::Key::Tab),
        input::Key::Back => Some(egui::Key::Backspace),
        input::Key::Return => Some(egui::Key::Enter),
        input::Key::Space => Some(egui::Key::Space),

        input::Key::Insert => Some(egui::Key::Insert),
        input::Key::Delete => Some(egui::Key::Delete),
        input::Key::Home => Some(egui::Key::Home),
        input::Key::End => Some(egui::Key::End),
        input::Key::PageUp => Some(egui::Key::PageUp),
        input::Key::PageDown => Some(egui::Key::PageDown),

        input::Key::Key0 => Some(egui::Key::Num0),
        input::Key::Key1 => Some(egui::Key::Num1),
        input::Key::Key2 => Some(egui::Key::Num2),
        input::Key::Key3 => Some(egui::Key::Num3),
        input::Key::Key4 => Some(egui::Key::Num4),
        input::Key::Key5 => Some(egui::Key::Num5),
        input::Key::Key6 => Some(egui::Key::Num6),
        input::Key::Key7 => Some(egui::Key::Num7),
        input::Key::Key8 => Some(egui::Key::Num8),
        input::Key::Key9 => Some(egui::Key::Num9),

        input::Key::A => Some(egui::Key::A),
        input::Key::B => Some(egui::Key::B),
        input::Key::C => Some(egui::Key::C),
        input::Key::D => Some(egui::Key::D),
        input::Key::E => Some(egui::Key::E),
        input::Key::F => Some(egui::Key::F),
        input::Key::G => Some(egui::Key::G),
        input::Key::H => Some(egui::Key::H),
        input::Key::I => Some(egui::Key::I),
        input::Key::J => Some(egui::Key::J),
        input::Key::K => Some(egui::Key::K),
        input::Key::L => Some(egui::Key::L),
        input::Key::M => Some(egui::Key::M),
        input::Key::N => Some(egui::Key::N),
        input::Key::O => Some(egui::Key::O),
        input::Key::P => Some(egui::Key::P),
        input::Key::Q => Some(egui::Key::Q),
        input::Key::R => Some(egui::Key::R),
        input::Key::S => Some(egui::Key::S),
        input::Key::T => Some(egui::Key::T),
        input::Key::U => Some(egui::Key::U),
        input::Key::V => Some(egui::Key::V),
        input::Key::W => Some(egui::Key::W),
        input::Key::X => Some(egui::Key::X),
        input::Key::Y => Some(egui::Key::Y),
        input::Key::Z => Some(egui::Key::Z),

        _ => None,
    }
}

fn as_egui_modifiers(modifiers: &input::Modifiers) -> egui::Modifiers {
    egui::Modifiers {
        alt: modifiers.alt,
        ctrl: modifiers.ctrl,
        shift: modifiers.shift,
        mac_cmd: modifiers.cmd, // close enough
        command: modifiers.cmd,
    }
}

fn as_egui_button(button: &input::PointerButton) -> egui::PointerButton {
    match button {
        input::PointerButton::Primary => egui::PointerButton::Primary,
        input::PointerButton::Secondary => egui::PointerButton::Secondary,
        input::PointerButton::Middle => egui::PointerButton::Middle,
    }
}

pub fn font_definitions() -> egui::FontDefinitions {
    // Due to font-licensing clear out default fonts.
    let mut definitions = FontDefinitions::default();
    definitions.font_data.clear();
    definitions.families.clear();

    definitions.font_data.insert(
        "Roboto".to_owned(),
        FontData::from_static(include_bytes!("../assets/Roboto-Medium.ttf")),
    );
    definitions
        .families
        .insert(FontFamily::Proportional, vec!["Roboto".to_owned()]);
    definitions
        .families
        .insert(FontFamily::Monospace, vec!["Roboto".to_owned()]);
    definitions
}