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;
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();
context.set_fonts(font_definitions());
EguiArk {
input_mngr: input::InputManager::default(),
egui_input: egui::RawInput::default(),
ctx: context,
painter: EguiPainter::default(),
}
}
}
impl EguiArk {
pub fn ctx(&self) -> &egui::Context {
&self.ctx
}
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()
}
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);
}
}
pub fn wants_pointer_input(&self) -> bool {
self.ctx.wants_pointer_input()
}
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()
}
pub fn persist(&self) -> serde_json::Value {
serde_json::to_value(&*self.ctx.memory()).unwrap_or_default()
}
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);
}
}
}
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.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.restore(serde_json::Value::deserialize(deserializer)?);
Ok(egui_ark)
}
}
#[derive(Default)]
struct EguiPainter {
egui_textures: HashMap<egui::TextureId, render::Texture>,
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 {
if let Some(texture) = self.egui_textures.get_mut(&texture_id) {
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,
);
}
}
#[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")
}
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));
}
}
input::Command::Undo | input::Command::Redo | input::Command::Save |
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, 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 {
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
}