#![deny(missing_docs)]
pub use egui;
mod egui_node;
mod systems;
mod transform_node;
use crate::{egui_node::EguiNode, systems::*, transform_node::EguiTransformNode};
use bevy::{
app::{stage as bevy_stage, AppBuilder, EventReader, Plugin},
asset::{Assets, Handle, HandleUntyped},
ecs::{IntoSystem, SystemStage},
input::mouse::MouseWheel,
log,
reflect::TypeUuid,
render::{
pipeline::{
BlendDescriptor, BlendFactor, BlendOperation, ColorStateDescriptor, ColorWrite,
CompareFunction, CullMode, DepthStencilStateDescriptor, FrontFace, IndexFormat,
PipelineDescriptor, RasterizationStateDescriptor, StencilStateDescriptor,
StencilStateFaceDescriptor,
},
render_graph::{base, base::Msaa, RenderGraph, WindowSwapChainNode, WindowTextureNode},
shader::{Shader, ShaderStage, ShaderStages},
stage as bevy_render_stage,
texture::{Texture, TextureFormat},
},
window::{CursorLeft, CursorMoved, ReceivedCharacter},
};
#[cfg(all(feature = "manage_clipboard", not(target_arch = "wasm32")))]
use clipboard::{ClipboardContext, ClipboardProvider};
#[cfg(all(feature = "manage_clipboard", not(target_arch = "wasm32")))]
use std::cell::{RefCell, RefMut};
use std::collections::HashMap;
#[cfg(all(feature = "manage_clipboard", not(target_arch = "wasm32")))]
use thread_local::ThreadLocal;
pub const EGUI_PIPELINE_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(PipelineDescriptor::TYPE_UUID, 9404026720151354217);
pub const EGUI_TRANSFORM_RESOURCE_BINDING_NAME: &str = "EguiTransform";
pub const EGUI_TEXTURE_RESOURCE_BINDING_NAME: &str = "EguiTexture_texture";
pub struct EguiPlugin;
#[derive(Clone, Debug, PartialEq)]
pub struct EguiSettings {
pub scale_factor: f64,
}
impl Default for EguiSettings {
fn default() -> Self {
Self { scale_factor: 1.0 }
}
}
#[derive(Clone, Debug, Default)]
pub struct EguiInput {
pub raw_input: egui::RawInput,
}
#[cfg(feature = "manage_clipboard")]
#[derive(Default)]
pub struct EguiClipboard {
#[cfg(not(target_arch = "wasm32"))]
clipboard: ThreadLocal<Option<RefCell<ClipboardContext>>>,
#[cfg(target_arch = "wasm32")]
clipboard: String,
}
#[cfg(feature = "manage_clipboard")]
impl EguiClipboard {
pub fn set_contents(&mut self, contents: &str) {
self.set_contents_impl(contents);
}
pub fn get_contents(&self) -> Option<String> {
self.get_contents_impl()
}
#[cfg(not(target_arch = "wasm32"))]
fn set_contents_impl(&self, contents: &str) {
if let Some(mut clipboard) = self.get() {
if let Err(err) = clipboard.set_contents(contents.to_owned()) {
log::error!("Failed to set clipboard contents: {:?}", err);
}
}
}
#[cfg(target_arch = "wasm32")]
fn set_contents_impl(&mut self, contents: &str) {
self.clipboard = contents.to_owned();
}
#[cfg(not(target_arch = "wasm32"))]
fn get_contents_impl(&self) -> Option<String> {
if let Some(mut clipboard) = self.get() {
match clipboard.get_contents() {
Ok(contents) => return Some(contents),
Err(err) => log::info!("Failed to get clipboard contents: {:?}", err),
}
};
None
}
#[cfg(target_arch = "wasm32")]
#[allow(clippy::unnecessary_wraps)]
fn get_contents_impl(&self) -> Option<String> {
Some(self.clipboard.clone())
}
#[cfg(not(target_arch = "wasm32"))]
fn get(&self) -> Option<RefMut<ClipboardContext>> {
self.clipboard
.get_or(|| {
ClipboardContext::new()
.map(RefCell::new)
.map_err(|err| {
log::info!("Failed to initialize clipboard: {:?}", err);
})
.ok()
})
.as_ref()
.map(|cell| cell.borrow_mut())
}
}
#[derive(Clone, Default)]
pub struct EguiShapes {
pub shapes: Vec<egui::paint::ClippedShape>,
}
#[derive(Clone, Default)]
pub struct EguiOutput {
pub output: egui::Output,
}
pub struct EguiContext {
pub ctx: egui::CtxRef,
egui_textures: HashMap<egui::TextureId, Handle<Texture>>,
mouse_position: Option<(f32, f32)>,
cursor_left: EventReader<CursorLeft>,
cursor_moved: EventReader<CursorMoved>,
mouse_wheel: EventReader<MouseWheel>,
received_character: EventReader<ReceivedCharacter>,
}
impl EguiContext {
fn new() -> Self {
Self {
ctx: Default::default(),
egui_textures: Default::default(),
mouse_position: Some((0.0, 0.0)),
cursor_left: Default::default(),
cursor_moved: Default::default(),
mouse_wheel: Default::default(),
received_character: Default::default(),
}
}
pub fn set_egui_texture(&mut self, id: u64, texture: Handle<Texture>) {
log::debug!("Set egui texture: {:?}", texture);
self.egui_textures
.insert(egui::TextureId::User(id), texture);
}
pub fn remove_egui_texture(&mut self, id: u64) {
let texture_handle = self.egui_textures.remove(&egui::TextureId::User(id));
log::debug!("Remove egui texture: {:?}", texture_handle);
}
fn remove_texture(&mut self, texture_handle: &Handle<Texture>) {
log::debug!("Removing egui handles: {:?}", texture_handle);
self.egui_textures = self
.egui_textures
.iter()
.map(|(id, texture)| (*id, texture.clone()))
.filter(|(_, texture)| texture != texture_handle)
.collect();
}
}
#[doc(hidden)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct WindowSize {
physical_width: f32,
physical_height: f32,
scale_factor: f32,
}
impl WindowSize {
fn new(physical_width: f32, physical_height: f32, scale_factor: f32) -> Self {
Self {
physical_width,
physical_height,
scale_factor,
}
}
#[inline]
fn width(&self) -> f32 {
self.physical_width / self.scale_factor
}
#[inline]
fn height(&self) -> f32 {
self.physical_height / self.scale_factor
}
}
pub mod node {
pub const EGUI_PASS: &str = "egui_pass";
pub const EGUI_TRANSFORM: &str = "egui_transform";
}
pub mod stage {
pub const INPUT: &str = "input";
pub const POST_INPUT: &str = "post_input";
pub const UI_FRAME: &str = "ui_frame";
pub const UI_FRAME_END: &str = "ui_frame_end";
pub const POST_UI_FRAME_END: &str = "post_ui_frame_end";
}
impl Plugin for EguiPlugin {
fn build(&self, app: &mut AppBuilder) {
app.add_stage_after(bevy_stage::EVENT, stage::INPUT, SystemStage::parallel());
app.add_stage_after(stage::INPUT, stage::POST_INPUT, SystemStage::parallel());
app.add_stage_after(stage::POST_INPUT, stage::UI_FRAME, SystemStage::parallel());
app.add_stage_before(
bevy_render_stage::RENDER_RESOURCE,
stage::UI_FRAME_END,
SystemStage::parallel(),
);
app.add_stage_after(
stage::UI_FRAME_END,
stage::POST_UI_FRAME_END,
SystemStage::parallel(),
);
app.add_system_to_stage(stage::INPUT, process_input.system());
app.add_system_to_stage(stage::UI_FRAME, begin_frame.system());
app.add_system_to_stage(stage::UI_FRAME_END, process_output.system());
let resources = app.resources_mut();
resources.get_or_insert_with(EguiSettings::default);
resources.get_or_insert_with(EguiInput::default);
resources.get_or_insert_with(EguiOutput::default);
resources.get_or_insert_with(EguiShapes::default);
#[cfg(feature = "manage_clipboard")]
resources.get_or_insert_with(EguiClipboard::default);
resources.insert(EguiContext::new());
resources.insert(WindowSize::new(0.0, 0.0, 0.0));
let mut pipelines = resources.get_mut::<Assets<PipelineDescriptor>>().unwrap();
let mut shaders = resources.get_mut::<Assets<Shader>>().unwrap();
let msaa = resources.get::<Msaa>().unwrap();
pipelines.set_untracked(
EGUI_PIPELINE_HANDLE,
build_egui_pipeline(&mut shaders, msaa.samples),
);
let mut render_graph = resources.get_mut::<RenderGraph>().unwrap();
render_graph.add_node(node::EGUI_PASS, EguiNode::new(&msaa));
render_graph
.add_node_edge(base::node::MAIN_PASS, node::EGUI_PASS)
.unwrap();
render_graph
.add_slot_edge(
base::node::PRIMARY_SWAP_CHAIN,
WindowSwapChainNode::OUT_TEXTURE,
node::EGUI_PASS,
if msaa.samples > 1 {
"color_resolve_target"
} else {
"color_attachment"
},
)
.unwrap();
render_graph
.add_slot_edge(
base::node::MAIN_DEPTH_TEXTURE,
WindowTextureNode::OUT_TEXTURE,
node::EGUI_PASS,
"depth",
)
.unwrap();
if msaa.samples > 1 {
render_graph
.add_slot_edge(
base::node::MAIN_SAMPLED_COLOR_ATTACHMENT,
WindowSwapChainNode::OUT_TEXTURE,
node::EGUI_PASS,
"color_attachment",
)
.unwrap();
}
render_graph.add_system_node(node::EGUI_TRANSFORM, EguiTransformNode::new());
render_graph
.add_node_edge(node::EGUI_TRANSFORM, node::EGUI_PASS)
.unwrap();
}
}
fn build_egui_pipeline(shaders: &mut Assets<Shader>, sample_count: u32) -> PipelineDescriptor {
PipelineDescriptor {
rasterization_state: Some(RasterizationStateDescriptor {
front_face: FrontFace::Cw,
cull_mode: CullMode::None,
depth_bias: 0,
depth_bias_slope_scale: 0.0,
depth_bias_clamp: 0.0,
clamp_depth: false,
}),
depth_stencil_state: Some(DepthStencilStateDescriptor {
format: TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: CompareFunction::LessEqual,
stencil: StencilStateDescriptor {
front: StencilStateFaceDescriptor::IGNORE,
back: StencilStateFaceDescriptor::IGNORE,
read_mask: 0,
write_mask: 0,
},
}),
color_states: vec![ColorStateDescriptor {
format: TextureFormat::default(),
color_blend: BlendDescriptor {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::OneMinusSrcAlpha,
operation: BlendOperation::Add,
},
alpha_blend: BlendDescriptor {
src_factor: BlendFactor::One,
dst_factor: BlendFactor::OneMinusSrcAlpha,
operation: BlendOperation::Add,
},
write_mask: ColorWrite::ALL,
}],
index_format: IndexFormat::Uint32,
sample_count,
..PipelineDescriptor::new(ShaderStages {
vertex: shaders.add(Shader::from_glsl(
ShaderStage::Vertex,
if cfg!(target_arch = "wasm32") {
include_str!("egui.es.vert")
} else {
include_str!("egui.vert")
},
)),
fragment: Some(shaders.add(Shader::from_glsl(
ShaderStage::Fragment,
if cfg!(target_arch = "wasm32") {
include_str!("egui.es.frag")
} else {
include_str!("egui.frag")
},
))),
})
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_readme_deps() {
version_sync::assert_markdown_deps_updated!("README.md");
}
}