#![warn(missing_docs)]
#![allow(clippy::tabs_in_doc_comments)]
pub use egui;
use std::{fmt::Display, sync::Arc, time::Instant};
use copypasta::{ClipboardContext, ClipboardProvider};
use egui::{ClippedMesh, CtxRef, RawInput};
use tetra::{
graphics::{self, BlendAlphaMode, BlendMode},
Event, TetraError,
};
const SCROLL_SENSITIVITY: f32 = 48.0;
const ZOOM_SENSITIVITY: f32 = 1.25;
fn tetra_vec2_to_egui_pos2(tetra_vec2: tetra::math::Vec2<f32>) -> egui::Pos2 {
egui::pos2(tetra_vec2.x, tetra_vec2.y)
}
fn egui_pos2_to_tetra_vec2(egui_pos2: egui::Pos2) -> tetra::math::Vec2<f32> {
tetra::math::Vec2::new(egui_pos2.x, egui_pos2.y)
}
fn egui_rect_to_tetra_rectangle(egui_rect: egui::Rect) -> tetra::graphics::Rectangle<i32> {
tetra::graphics::Rectangle::new(
egui_rect.left() as i32,
egui_rect.top() as i32,
egui_rect.width() as i32,
egui_rect.height() as i32,
)
}
fn egui_color32_to_tetra_color(egui_color: egui::Color32) -> tetra::graphics::Color {
tetra::graphics::Color::rgba8(
egui_color.r(),
egui_color.g(),
egui_color.b(),
egui_color.a(),
)
}
fn tetra_key_to_egui_key(key: tetra::input::Key) -> Option<egui::Key> {
match key {
tetra::input::Key::A => Some(egui::Key::A),
tetra::input::Key::B => Some(egui::Key::B),
tetra::input::Key::C => Some(egui::Key::C),
tetra::input::Key::D => Some(egui::Key::D),
tetra::input::Key::E => Some(egui::Key::E),
tetra::input::Key::F => Some(egui::Key::F),
tetra::input::Key::G => Some(egui::Key::G),
tetra::input::Key::H => Some(egui::Key::H),
tetra::input::Key::I => Some(egui::Key::I),
tetra::input::Key::J => Some(egui::Key::J),
tetra::input::Key::K => Some(egui::Key::K),
tetra::input::Key::L => Some(egui::Key::L),
tetra::input::Key::M => Some(egui::Key::M),
tetra::input::Key::N => Some(egui::Key::N),
tetra::input::Key::O => Some(egui::Key::O),
tetra::input::Key::P => Some(egui::Key::P),
tetra::input::Key::Q => Some(egui::Key::Q),
tetra::input::Key::R => Some(egui::Key::R),
tetra::input::Key::S => Some(egui::Key::S),
tetra::input::Key::T => Some(egui::Key::T),
tetra::input::Key::U => Some(egui::Key::U),
tetra::input::Key::V => Some(egui::Key::V),
tetra::input::Key::W => Some(egui::Key::W),
tetra::input::Key::X => Some(egui::Key::X),
tetra::input::Key::Y => Some(egui::Key::Y),
tetra::input::Key::Z => Some(egui::Key::Z),
tetra::input::Key::Num0 => Some(egui::Key::Num0),
tetra::input::Key::Num1 => Some(egui::Key::Num1),
tetra::input::Key::Num2 => Some(egui::Key::Num2),
tetra::input::Key::Num3 => Some(egui::Key::Num3),
tetra::input::Key::Num4 => Some(egui::Key::Num4),
tetra::input::Key::Num5 => Some(egui::Key::Num5),
tetra::input::Key::Num6 => Some(egui::Key::Num6),
tetra::input::Key::Num7 => Some(egui::Key::Num7),
tetra::input::Key::Num8 => Some(egui::Key::Num8),
tetra::input::Key::Num9 => Some(egui::Key::Num9),
tetra::input::Key::NumPad0 => Some(egui::Key::Num0),
tetra::input::Key::NumPad1 => Some(egui::Key::Num1),
tetra::input::Key::NumPad2 => Some(egui::Key::Num2),
tetra::input::Key::NumPad3 => Some(egui::Key::Num3),
tetra::input::Key::NumPad4 => Some(egui::Key::Num4),
tetra::input::Key::NumPad5 => Some(egui::Key::Num5),
tetra::input::Key::NumPad6 => Some(egui::Key::Num6),
tetra::input::Key::NumPad7 => Some(egui::Key::Num7),
tetra::input::Key::NumPad8 => Some(egui::Key::Num8),
tetra::input::Key::NumPad9 => Some(egui::Key::Num9),
tetra::input::Key::NumPadEnter => Some(egui::Key::Enter),
tetra::input::Key::Up => Some(egui::Key::ArrowUp),
tetra::input::Key::Down => Some(egui::Key::ArrowDown),
tetra::input::Key::Left => Some(egui::Key::ArrowLeft),
tetra::input::Key::Right => Some(egui::Key::ArrowRight),
tetra::input::Key::Backspace => Some(egui::Key::Backspace),
tetra::input::Key::Delete => Some(egui::Key::Delete),
tetra::input::Key::End => Some(egui::Key::End),
tetra::input::Key::Enter => Some(egui::Key::Enter),
tetra::input::Key::Escape => Some(egui::Key::Escape),
tetra::input::Key::Home => Some(egui::Key::Home),
tetra::input::Key::Insert => Some(egui::Key::Insert),
tetra::input::Key::PageDown => Some(egui::Key::PageDown),
tetra::input::Key::PageUp => Some(egui::Key::PageUp),
tetra::input::Key::Space => Some(egui::Key::Space),
tetra::input::Key::Tab => Some(egui::Key::Tab),
_ => None,
}
}
fn tetra_mouse_button_to_egui_pointer_button(
tetra_mouse_button: tetra::input::MouseButton,
) -> Option<egui::PointerButton> {
match tetra_mouse_button {
tetra::input::MouseButton::Left => Some(egui::PointerButton::Primary),
tetra::input::MouseButton::Middle => Some(egui::PointerButton::Middle),
tetra::input::MouseButton::Right => Some(egui::PointerButton::Secondary),
_ => None,
}
}
fn egui_mesh_to_tetra_mesh(
ctx: &mut tetra::Context,
egui_mesh: egui::epaint::Mesh,
texture: tetra::graphics::Texture,
) -> tetra::Result<tetra::graphics::mesh::Mesh> {
let index_buffer = tetra::graphics::mesh::IndexBuffer::new(ctx, &egui_mesh.indices)?;
let vertices: Vec<tetra::graphics::mesh::Vertex> = egui_mesh
.vertices
.iter()
.map(|vertex| {
tetra::graphics::mesh::Vertex::new(
egui_pos2_to_tetra_vec2(vertex.pos),
egui_pos2_to_tetra_vec2(vertex.uv),
egui_color32_to_tetra_color(vertex.color),
)
})
.collect();
let vertex_buffer = tetra::graphics::mesh::VertexBuffer::new(ctx, &vertices)?;
let mut mesh = tetra::graphics::mesh::Mesh::indexed(vertex_buffer, index_buffer);
mesh.set_texture(texture);
mesh.set_backface_culling(false);
Ok(mesh)
}
fn egui_font_image_to_tetra_texture(
ctx: &mut tetra::Context,
egui_font_image: Arc<egui::FontImage>,
) -> tetra::Result<tetra::graphics::Texture> {
let mut pixels = vec![];
for alpha in &egui_font_image.pixels {
pixels.push(*alpha);
pixels.push(*alpha);
pixels.push(*alpha);
pixels.push(*alpha);
}
tetra::graphics::Texture::from_rgba(
ctx,
egui_font_image.width as i32,
egui_font_image.height as i32,
&pixels,
)
}
#[derive(Debug)]
pub enum Error {
TetraError(TetraError),
OpenError(std::io::Error),
ClipboardError(Box<dyn std::error::Error + Send + Sync>),
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::TetraError(error) => error.fmt(f),
Error::OpenError(error) => error.fmt(f),
Error::ClipboardError(error) => error.fmt(f),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::TetraError(error) => Some(error),
Error::OpenError(error) => Some(error),
Error::ClipboardError(error) => Some(error.as_ref()),
}
}
}
impl From<TetraError> for Error {
fn from(error: TetraError) -> Self {
Self::TetraError(error)
}
}
impl From<std::io::Error> for Error {
fn from(v: std::io::Error) -> Self {
Self::OpenError(v)
}
}
impl From<Box<dyn std::error::Error + Send + Sync>> for Error {
fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
Self::ClipboardError(error)
}
}
pub struct EguiWrapper {
raw_input: RawInput,
ctx: CtxRef,
texture: Option<tetra::graphics::Texture>,
last_frame_time: Instant,
meshes: Vec<(tetra::graphics::Rectangle<i32>, tetra::graphics::mesh::Mesh)>,
}
impl EguiWrapper {
pub fn new() -> Self {
Self {
raw_input: RawInput::default(),
ctx: CtxRef::default(),
texture: None,
last_frame_time: Instant::now(),
meshes: vec![],
}
}
pub fn ctx(&self) -> &egui::CtxRef {
&self.ctx
}
pub fn event(&mut self, ctx: &tetra::Context, event: &tetra::Event) -> Result<(), Error> {
match event {
tetra::Event::KeyPressed { key } => {
match key {
tetra::input::Key::LeftCtrl | tetra::input::Key::RightCtrl => {
self.raw_input.modifiers.ctrl = true;
self.raw_input.modifiers.command = true;
}
tetra::input::Key::LeftShift | tetra::input::Key::RightShift => {
self.raw_input.modifiers.shift = true;
}
tetra::input::Key::LeftAlt | tetra::input::Key::RightAlt => {
self.raw_input.modifiers.alt = true;
}
_ => {}
}
if tetra::input::is_key_down(ctx, tetra::input::Key::LeftCtrl)
| tetra::input::is_key_down(ctx, tetra::input::Key::RightCtrl)
{
if let tetra::input::Key::C = key {
self.raw_input.events.push(egui::Event::Copy);
}
if let tetra::input::Key::X = key {
self.raw_input.events.push(egui::Event::Cut);
}
if let tetra::input::Key::V = key {
self.raw_input
.events
.push(egui::Event::Text(ClipboardContext::new()?.get_contents()?));
}
}
if let Some(key) = tetra_key_to_egui_key(*key) {
self.raw_input.events.push(egui::Event::Key {
key,
pressed: true,
modifiers: self.raw_input.modifiers,
});
}
}
tetra::Event::KeyReleased { key } => {
match key {
tetra::input::Key::LeftCtrl | tetra::input::Key::RightCtrl => {
self.raw_input.modifiers.ctrl = false;
self.raw_input.modifiers.command = false;
}
tetra::input::Key::LeftShift | tetra::input::Key::RightShift => {
self.raw_input.modifiers.shift = false;
}
tetra::input::Key::LeftAlt | tetra::input::Key::RightAlt => {
self.raw_input.modifiers.alt = false;
}
_ => {}
}
if let Some(key) = tetra_key_to_egui_key(*key) {
self.raw_input.events.push(egui::Event::Key {
key,
pressed: false,
modifiers: self.raw_input.modifiers,
});
}
}
tetra::Event::MouseButtonPressed { button } => {
if let Some(button) = tetra_mouse_button_to_egui_pointer_button(*button) {
self.raw_input.events.push(egui::Event::PointerButton {
pos: tetra_vec2_to_egui_pos2(tetra::input::get_mouse_position(ctx)),
button,
pressed: true,
modifiers: self.raw_input.modifiers,
});
}
}
tetra::Event::MouseButtonReleased { button } => {
if let Some(button) = tetra_mouse_button_to_egui_pointer_button(*button) {
self.raw_input.events.push(egui::Event::PointerButton {
pos: tetra_vec2_to_egui_pos2(tetra::input::get_mouse_position(ctx)),
button,
pressed: false,
modifiers: self.raw_input.modifiers,
});
}
}
tetra::Event::MouseMoved { position, .. } => {
self.raw_input
.events
.push(egui::Event::PointerMoved(tetra_vec2_to_egui_pos2(
*position,
)));
}
tetra::Event::MouseWheelMoved { amount } => {
if tetra::input::is_key_down(ctx, tetra::input::Key::LeftCtrl)
|| tetra::input::is_key_down(ctx, tetra::input::Key::RightCtrl)
{
self.raw_input
.events
.push(egui::Event::Zoom(ZOOM_SENSITIVITY.powi(amount.y)));
} else {
self.raw_input.events.push(egui::Event::Scroll(
egui::vec2(amount.x as f32, amount.y as f32) * SCROLL_SENSITIVITY,
));
}
}
tetra::Event::TextInput { text } => {
self.raw_input.events.push(egui::Event::Text(text.clone()));
}
_ => {}
}
Ok(())
}
pub fn begin_frame(&mut self, ctx: &mut tetra::Context) -> Result<(), Error> {
let now = Instant::now();
self.raw_input.screen_rect = Some(egui::Rect {
min: egui::pos2(0.0, 0.0),
max: egui::pos2(
tetra::window::get_width(ctx) as f32,
tetra::window::get_height(ctx) as f32,
),
});
self.raw_input.predicted_dt = (now - self.last_frame_time).as_secs_f32();
self.last_frame_time = now;
self.meshes.clear();
self.ctx.begin_frame(self.raw_input.take());
if self.texture.is_none() {
self.texture = Some(egui_font_image_to_tetra_texture(
ctx,
self.ctx.font_image(),
)?);
}
Ok(())
}
pub fn end_frame(&mut self, ctx: &mut tetra::Context) -> Result<(), Error> {
let (output, shapes) = self.ctx.end_frame();
if let Some(texture) = &self.texture {
let clipped_meshes = self.ctx.tessellate(shapes);
for ClippedMesh(rect, mesh) in clipped_meshes {
let rect = egui_rect_to_tetra_rectangle(rect);
let mesh = egui_mesh_to_tetra_mesh(ctx, mesh, texture.clone())?;
self.meshes.push((rect, mesh));
}
}
if let Some(open_url) = &output.open_url {
open::that(&open_url.url)?;
}
if !output.copied_text.is_empty() {
ClipboardContext::new()?.set_contents(output.copied_text)?;
}
Ok(())
}
pub fn draw_frame(&mut self, ctx: &mut tetra::Context) {
graphics::set_blend_mode(ctx, BlendMode::Alpha(BlendAlphaMode::Premultiplied));
for (rect, mesh) in &self.meshes {
graphics::set_scissor(ctx, *rect);
mesh.draw(ctx, tetra::math::Vec2::zero());
}
graphics::reset_scissor(ctx);
graphics::reset_blend_mode(ctx);
}
}
impl Default for EguiWrapper {
fn default() -> Self {
Self::new()
}
}
#[allow(unused_variables)]
pub trait State<E: From<Error> = Error> {
fn ui(&mut self, ctx: &mut tetra::Context, egui_ctx: &egui::CtxRef) -> Result<(), E> {
Ok(())
}
fn update(&mut self, ctx: &mut tetra::Context, egui_ctx: &egui::CtxRef) -> Result<(), E> {
Ok(())
}
fn draw(&mut self, ctx: &mut tetra::Context, egui_ctx: &egui::CtxRef) -> Result<(), E> {
Ok(())
}
fn event(
&mut self,
ctx: &mut tetra::Context,
egui_ctx: &egui::CtxRef,
event: Event,
) -> Result<(), E> {
Ok(())
}
}
pub struct StateWrapper<E: From<Error>> {
events: Vec<tetra::Event>,
state: Box<dyn State<E>>,
egui: EguiWrapper,
}
impl<E: From<Error>> StateWrapper<E> {
pub fn new(state: impl State<E> + 'static) -> Self {
Self {
events: vec![],
state: Box::new(state),
egui: EguiWrapper::new(),
}
}
pub fn ctx(&self) -> &egui::CtxRef {
self.egui.ctx()
}
}
impl<E: From<Error>> tetra::State<E> for StateWrapper<E> {
fn update(&mut self, ctx: &mut tetra::Context) -> Result<(), E> {
self.egui.begin_frame(ctx)?;
self.state.ui(ctx, self.egui.ctx())?;
self.egui.end_frame(ctx)?;
for event in self.events.drain(..) {
match &event {
Event::KeyPressed { .. } | Event::KeyReleased { .. } => {
if self.egui.ctx().wants_keyboard_input() {
continue;
}
}
Event::MouseButtonPressed { .. } | Event::MouseButtonReleased { .. } => {
if self.egui.ctx().is_using_pointer() {
continue;
}
}
Event::MouseMoved { .. } => {
if self.egui.ctx().is_using_pointer() {
continue;
}
}
Event::MouseWheelMoved { .. } => {
if self.egui.ctx().is_using_pointer() {
continue;
}
}
_ => {}
}
self.state.event(ctx, self.egui.ctx(), event)?;
}
self.state.update(ctx, self.egui.ctx())
}
fn draw(&mut self, ctx: &mut tetra::Context) -> Result<(), E> {
self.state.draw(ctx, self.egui.ctx())?;
self.egui.draw_frame(ctx);
Ok(())
}
fn event(&mut self, ctx: &mut tetra::Context, event: Event) -> Result<(), E> {
self.egui.event(ctx, &event)?;
self.events.push(event);
Ok(())
}
}