use std::cell::RefCell;
use std::rc::Rc;
use gdnative::api::{
File, GlobalConstants, ImageTexture, InputEventMouseButton, InputEventMouseMotion, VisualServer,
};
use gdnative::nativescript::property::{EnumHint, StringHint};
use gdnative::prelude::*;
pub(crate) mod enum_conversions;
pub mod egui_helpers;
pub fn egui2color(c: egui::Color32) -> Color {
let as_f32 = |x| x as f32 / u8::MAX as f32;
Color::rgba(as_f32(c.r()), as_f32(c.g()), as_f32(c.b()), as_f32(c.a()))
}
pub fn color2egui(c: Color) -> egui::Color32 {
let as_u8 = |x| (x * (u8::MAX as f32)) as u8;
egui::Color32::from_rgba_premultiplied(as_u8(c.r), as_u8(c.g), as_u8(c.b), as_u8(c.a))
}
fn u64_to_rid(x: u64) -> Rid {
unsafe { Rid::from_sys(std::mem::transmute::<u64, gdnative::sys::godot_rid>(x)) }
}
pub fn rid_to_egui_texture_id(x: Rid) -> egui::TextureId {
unsafe { egui::TextureId::User(std::mem::transmute::<gdnative::sys::godot_rid, u64>(*x.sys())) }
}
struct VisualServerMesh {
canvas_item: Rid,
}
struct SyncedTexture {
texture_version: Option<u64>,
godot_texture: Ref<ImageTexture>,
}
#[derive(NativeClass)]
#[inherit(gdnative::api::Control)]
#[register_with(register_properties)]
pub struct GodotEgui {
pub egui_ctx: egui::CtxRef,
meshes: Vec<VisualServerMesh>,
main_texture: SyncedTexture,
raw_input: Rc<RefCell<egui::RawInput>>,
mouse_was_captured: bool,
#[property]
override_default_fonts: bool,
custom_fonts: [Option<String>; 5],
#[property]
scroll_speed: f32,
#[property]
consume_mouse_events: bool,
#[property]
disable_texture_filtering: bool,
}
fn register_properties(builder: &ClassBuilder<GodotEgui>) {
for i in 0..5 {
builder
.add_property::<String>(&format!("custom_font_{}", i + 1))
.with_getter(move |x: &GodotEgui, _| x.custom_fonts[i].as_ref().cloned().unwrap_or_default())
.with_setter(move |x: &mut GodotEgui, _, new_val| x.custom_fonts[i] = Some(new_val))
.with_default("".to_owned())
.with_hint(StringHint::File(EnumHint::new(vec!["*.ttf".to_owned(), "*.otf".to_owned()])))
.done();
}
}
#[gdnative::methods]
impl GodotEgui {
pub fn new(_owner: TRef<Control>) -> GodotEgui {
GodotEgui {
egui_ctx: Default::default(),
meshes: vec![],
main_texture: SyncedTexture {
texture_version: None,
godot_texture: ImageTexture::new().into_shared(),
},
raw_input: Rc::new(RefCell::new(egui::RawInput::default())),
mouse_was_captured: false,
override_default_fonts: false,
custom_fonts: [None, None, None, None, None],
scroll_speed: 20.0,
consume_mouse_events: true,
disable_texture_filtering: false,
}
}
#[export]
fn _ready(&mut self, _owner: TRef<Control>) {
self.egui_ctx.begin_frame(egui::RawInput::default());
let _ = self.egui_ctx.end_frame();
let mut font_defs = self.egui_ctx.fonts().definitions().clone();
if self.override_default_fonts {
font_defs.fonts_for_family.get_mut(&egui::FontFamily::Proportional).unwrap().clear()
}
for font_path in self
.custom_fonts
.iter()
.filter(|x| x.as_ref().map(|x| !x.is_empty()).unwrap_or(false))
.map(|x| x.as_ref().unwrap())
{
let font_file = gdnative::api::File::new();
match font_file.open(font_path, File::READ) {
Ok(_) => {
let file_data = font_file.get_buffer(font_file.get_len());
let file_data = std::borrow::Cow::Owned(file_data.read().as_slice().to_owned());
font_defs.font_data.insert(font_path.to_owned(), file_data);
font_defs
.fonts_for_family
.get_mut(&egui::FontFamily::Proportional)
.unwrap()
.push(font_path.to_owned());
}
Err(error) => {
godot_error!("GodotEgui could not load a custom font file: {:?}", error);
}
}
}
self.egui_ctx.set_fonts(font_defs);
}
fn maybe_set_mouse_input_as_handled(&self, owner: TRef<Control>) {
if self.mouse_was_captured && self.consume_mouse_events {
unsafe { owner.get_viewport().expect("Viewport").assume_safe().set_input_as_handled() }
}
}
#[export]
fn _input(&mut self, owner: TRef<Control>, event: Ref<InputEvent>) {
let event = unsafe { event.assume_safe() };
let mut raw_input = self.raw_input.borrow_mut();
let mouse_pos_to_egui = |mouse_pos: Vector2| {
let transformed_pos = mouse_pos - owner.get_global_rect().origin.to_vector();
egui::Pos2 { x: transformed_pos.x, y: transformed_pos.y }
};
if let Some(motion_ev) = event.cast::<InputEventMouseMotion>() {
self.maybe_set_mouse_input_as_handled(owner);
raw_input.events.push(egui::Event::PointerMoved(mouse_pos_to_egui(motion_ev.position())))
}
if let Some(button_ev) = event.cast::<InputEventMouseButton>() {
self.maybe_set_mouse_input_as_handled(owner);
if let Some(button) = enum_conversions::mouse_button_index_to_egui(button_ev.button_index()) {
raw_input.events.push(egui::Event::PointerButton {
pos: mouse_pos_to_egui(button_ev.position()),
button,
pressed: button_ev.is_pressed(),
modifiers: Default::default(),
})
}
if button_ev.is_pressed() {
match button_ev.button_index() {
GlobalConstants::BUTTON_WHEEL_UP => {
raw_input.scroll_delta = egui::Vec2::new(0.0, 1.0) * self.scroll_speed
}
GlobalConstants::BUTTON_WHEEL_DOWN => {
raw_input.scroll_delta = egui::Vec2::new(0.0, -1.0) * self.scroll_speed
}
_ => {}
}
}
}
if let Some(key_ev) = event.cast::<InputEventKey>() {
if let Some(key) = enum_conversions::scancode_to_egui(key_ev.scancode()) {
let mods = key_ev.get_scancode_with_modifiers();
let modifiers = egui::Modifiers {
ctrl: (mods & GlobalConstants::KEY_MASK_CTRL) == 0,
shift: (mods & GlobalConstants::KEY_MASK_SHIFT) == 0,
alt: (mods & GlobalConstants::KEY_ALT) == 0,
..Default::default()
};
raw_input.events.push(egui::Event::Key { key, pressed: key_ev.is_pressed(), modifiers })
}
if key_ev.is_pressed() && key_ev.unicode() != 0 {
let utf8_bytes = [key_ev.unicode() as u8];
if let Ok(utf8) = std::str::from_utf8(&utf8_bytes) {
raw_input.events.push(egui::Event::Text(String::from(utf8)));
}
}
}
}
fn paint_shapes(
&mut self, owner: TRef<Control>, clipped_meshes: Vec<egui::ClippedMesh>, egui_texture: &egui::Texture,
) {
let vs = unsafe { VisualServer::godot_singleton() };
if self.main_texture.texture_version != Some(egui_texture.version) {
let pixels: ByteArray =
egui_texture.pixels.iter().map(|alpha| [255u8, 255u8, 255u8, *alpha]).flatten().collect();
let image = Image::new();
image.create_from_data(
egui_texture.width as i64,
egui_texture.height as i64,
false,
Image::FORMAT_RGBA8,
pixels,
);
self.main_texture.texture_version = Some(egui_texture.version);
let new_tex = ImageTexture::new();
let flags =
if self.disable_texture_filtering { 0 } else { Texture::FLAG_FILTER | Texture::FLAG_MIPMAPS };
new_tex.create_from_image(image, flags);
self.main_texture.godot_texture = new_tex.into_shared();
}
let egui_texture_rid = unsafe { self.main_texture.godot_texture.assume_safe() }.get_rid();
for idx in 0..clipped_meshes.len() {
if idx >= self.meshes.len() {
let canvas_item = vs.canvas_item_create();
vs.canvas_item_set_parent(canvas_item, owner.get_canvas_item());
vs.canvas_item_set_draw_index(canvas_item, idx as i64);
vs.canvas_item_clear(canvas_item);
self.meshes.push(VisualServerMesh { canvas_item });
}
}
for _idx in (clipped_meshes.len()..self.meshes.len()).rev() {
let vs_mesh = self.meshes.pop().expect("This should always pop");
vs.free_rid(vs_mesh.canvas_item);
}
assert!(
clipped_meshes.len() == self.meshes.len(),
"At this point, the number of canvas items should be the same as the number of egui meshes."
);
for (egui::ClippedMesh(clip_rect, mesh), vs_mesh) in clipped_meshes.into_iter().zip(self.meshes.iter_mut())
{
if mesh.vertices.is_empty() {
vs.canvas_item_clear(vs_mesh.canvas_item);
continue;
}
let texture_rid = match mesh.texture_id {
egui::TextureId::Egui => egui_texture_rid,
egui::TextureId::User(id) => u64_to_rid(id),
};
#[allow(clippy::unsound_collection_transmute)]
let indices = Int32Array::from_vec(unsafe { std::mem::transmute::<_, Vec<i32>>(mesh.indices) });
let vertices = mesh
.vertices
.iter()
.map(|x| x.pos)
.map(|pos| Vector2::new(pos.x, pos.y))
.collect::<Vector2Array>();
let uvs =
mesh.vertices.iter().map(|x| x.uv).map(|uv| Vector2::new(uv.x, uv.y)).collect::<Vector2Array>();
let colors = mesh.vertices.iter().map(|x| x.color).map(egui2color).collect::<ColorArray>();
vs.canvas_item_clear(vs_mesh.canvas_item);
vs.canvas_item_add_triangle_array(
vs_mesh.canvas_item,
indices,
vertices,
colors,
uvs,
Int32Array::new(),
Float32Array::new(),
texture_rid,
-1,
Rid::new(),
false,
false,
);
vs.canvas_item_set_clip(vs_mesh.canvas_item, true);
vs.canvas_item_set_custom_rect(
vs_mesh.canvas_item,
true,
Rect2 {
origin: Point2::new(clip_rect.min.x, clip_rect.min.y),
size: Size2::new(clip_rect.max.x - clip_rect.min.x, clip_rect.max.y - clip_rect.min.y),
},
);
}
}
pub fn update_ctx(&mut self, owner: TRef<Control>, draw_fn: impl FnOnce(&mut egui::CtxRef)) {
let mut raw_input = self.raw_input.take();
let size = owner.get_rect().size;
raw_input.screen_rect =
Some(egui::Rect::from_min_size(Default::default(), egui::Vec2::new(size.width, size.height)));
self.egui_ctx.begin_frame(raw_input);
draw_fn(&mut self.egui_ctx);
let (_output, shapes) = self.egui_ctx.end_frame();
self.mouse_was_captured = self.egui_ctx.is_using_pointer();
let clipped_meshes = self.egui_ctx.tessellate(shapes);
self.paint_shapes(owner, clipped_meshes, &self.egui_ctx.texture());
}
pub fn update(
&mut self, owner: TRef<Control>, frame: Option<egui::Frame>, draw_fn: impl FnOnce(&mut egui::Ui),
) {
self.update_ctx(owner, |egui_ctx| {
egui::CentralPanel::default()
.frame(frame.unwrap_or(egui::Frame {
margin: egui::Vec2::new(10.0, 10.0),
fill: (egui::Color32::from_white_alpha(0)),
..Default::default()
}))
.show(egui_ctx, draw_fn);
})
}
pub fn mouse_was_captured(&self) -> bool { self.mouse_was_captured }
}
pub fn register_classes(handle: InitHandle) { handle.add_class::<GodotEgui>(); }
pub fn register_classes_as_tool(handle: InitHandle) { handle.add_tool_class::<GodotEgui>(); }