#![warn(missing_docs)]
#![allow(clippy::type_complexity)]
pub mod helpers;
pub mod input;
pub mod output;
#[cfg(feature = "picking")]
pub mod picking;
#[cfg(feature = "render")]
pub mod render;
#[cfg(target_arch = "wasm32")]
pub mod text_agent;
#[cfg(all(feature = "manage_clipboard", target_arch = "wasm32",))]
pub mod web_clipboard;
pub mod prelude {
pub use crate::{
EguiContext, EguiContextSettings, EguiContexts, EguiGlobalSettings, EguiMultipassSchedule,
EguiPlugin, EguiPrimaryContextPass, EguiStartupSet, PrimaryEguiContext, egui,
};
#[cfg(feature = "render")]
pub use crate::{EguiTextureHandle, EguiUserTextures};
}
pub use egui;
use crate::input::*;
#[cfg(target_arch = "wasm32")]
use crate::text_agent::{
SafariVirtualKeyboardTouchState, TextAgentChannel, VirtualTouchInfo, install_text_agent_system,
is_mobile_safari, process_safari_virtual_keyboard_system,
write_text_agent_channel_events_system,
};
#[cfg(all(
feature = "manage_clipboard",
not(any(target_arch = "wasm32", target_os = "android"))
))]
use arboard::Clipboard;
use bevy_app::prelude::*;
#[cfg(feature = "render")]
use bevy_asset::{AssetEvent, AssetId, Assets, Handle, load_internal_asset};
#[cfg(feature = "picking")]
use bevy_camera::NormalizedRenderTarget;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
lifecycle::HookContext,
prelude::*,
query::{QueryData, QueryEntityError, QuerySingleError},
schedule::{InternedScheduleLabel, ScheduleLabel},
system::SystemParam,
world::DeferredWorld,
};
#[cfg(feature = "render")]
use bevy_image::{Image, ImageSampler};
use bevy_input::InputSystems;
#[allow(unused_imports)]
use bevy_log as log;
#[cfg(feature = "picking")]
use bevy_picking::{
backend::{HitData, PointerHits},
pointer::{PointerId, PointerLocation},
};
#[cfg(feature = "render")]
use bevy_platform::collections::HashMap;
use bevy_platform::collections::HashSet;
use bevy_reflect::Reflect;
#[cfg(feature = "render")]
use bevy_render::{
ExtractSchedule, Render, RenderApp, RenderSystems,
extract_resource::{ExtractResource, ExtractResourcePlugin},
render_resource::SpecializedRenderPipelines,
};
use output::process_output_system;
#[cfg(all(
feature = "manage_clipboard",
not(any(target_arch = "wasm32", target_os = "android"))
))]
use std::cell::{RefCell, RefMut};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
pub struct EguiPlugin {
#[deprecated(
note = "The option to disable the multi-pass mode is now deprecated, use `EguiPlugin::default` instead"
)]
pub enable_multipass_for_primary_context: bool,
#[cfg(feature = "bevy_ui")]
pub ui_render_order: UiRenderOrder,
#[cfg(feature = "render")]
pub bindless_mode_array_size: Option<std::num::NonZero<u32>>,
}
impl Default for EguiPlugin {
fn default() -> Self {
Self {
#[allow(deprecated)]
enable_multipass_for_primary_context: true,
#[cfg(feature = "bevy_ui")]
ui_render_order: UiRenderOrder::EguiAboveBevyUi,
#[cfg(feature = "render")]
bindless_mode_array_size: std::num::NonZero::new(16),
}
}
}
#[cfg(feature = "bevy_ui")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UiRenderOrder {
EguiAboveBevyUi,
BevyUiAboveEgui,
}
#[derive(Clone, Debug, Resource, Reflect)]
pub struct EguiGlobalSettings {
pub auto_create_primary_context: bool,
pub enable_focused_non_window_context_updates: bool,
pub input_system_settings: EguiInputSystemSettings,
pub enable_absorb_bevy_input_system: bool,
pub enable_cursor_icon_updates: bool,
pub enable_ime: bool,
}
impl Default for EguiGlobalSettings {
fn default() -> Self {
Self {
auto_create_primary_context: true,
enable_focused_non_window_context_updates: true,
input_system_settings: EguiInputSystemSettings::default(),
enable_absorb_bevy_input_system: false,
enable_cursor_icon_updates: true,
enable_ime: true,
}
}
}
#[derive(Resource)]
pub struct EnableMultipassForPrimaryContext;
#[derive(Clone, Debug, Component, Reflect)]
pub struct EguiContextSettings {
pub run_manually: bool,
pub scale_factor: f32,
#[cfg(feature = "open_url")]
pub default_open_url_target: Option<String>,
#[cfg(feature = "picking")]
pub capture_pointer_input: bool,
pub input_system_settings: EguiInputSystemSettings,
pub enable_cursor_icon_updates: bool,
pub enable_ime: bool,
}
impl PartialEq for EguiContextSettings {
#[allow(clippy::let_and_return)]
fn eq(&self, other: &Self) -> bool {
let eq = self.scale_factor == other.scale_factor;
#[cfg(feature = "open_url")]
let eq = eq && self.default_open_url_target == other.default_open_url_target;
eq
}
}
impl Default for EguiContextSettings {
fn default() -> Self {
Self {
run_manually: false,
scale_factor: 1.0,
#[cfg(feature = "open_url")]
default_open_url_target: None,
#[cfg(feature = "picking")]
capture_pointer_input: true,
input_system_settings: EguiInputSystemSettings::default(),
enable_cursor_icon_updates: true,
enable_ime: true,
}
}
}
#[derive(Clone, Debug, Reflect, PartialEq, Eq)]
pub struct EguiInputSystemSettings {
pub run_write_modifiers_keys_state_system: bool,
pub run_write_window_pointer_moved_messages_system: bool,
pub run_write_pointer_button_messages_system: bool,
pub run_write_window_touch_messages_system: bool,
pub run_write_non_window_pointer_moved_messages_system: bool,
pub run_write_mouse_wheel_messages_system: bool,
pub run_write_non_window_touch_messages_system: bool,
pub run_write_keyboard_input_messages_system: bool,
pub run_write_ime_messages_system: bool,
pub run_write_file_dnd_messages_system: bool,
#[cfg(target_arch = "wasm32")]
pub run_write_text_agent_channel_messages_system: bool,
#[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
pub run_write_web_clipboard_messages_system: bool,
}
impl Default for EguiInputSystemSettings {
fn default() -> Self {
Self {
run_write_modifiers_keys_state_system: true,
run_write_window_pointer_moved_messages_system: true,
run_write_pointer_button_messages_system: true,
run_write_window_touch_messages_system: true,
run_write_non_window_pointer_moved_messages_system: true,
run_write_mouse_wheel_messages_system: true,
run_write_non_window_touch_messages_system: true,
run_write_keyboard_input_messages_system: true,
run_write_ime_messages_system: true,
run_write_file_dnd_messages_system: true,
#[cfg(target_arch = "wasm32")]
run_write_text_agent_channel_messages_system: true,
#[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
run_write_web_clipboard_messages_system: true,
}
}
}
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
pub struct EguiPrimaryContextPass;
#[derive(Component, Clone)]
#[require(EguiContext)]
#[component(on_insert = insert_schedule_if_multipass)]
pub struct PrimaryEguiContext;
fn insert_schedule_if_multipass(mut world: DeferredWorld, context: HookContext) {
if world.contains_resource::<EnableMultipassForPrimaryContext>() {
world
.commands()
.entity(context.entity)
.insert(EguiMultipassSchedule::new(EguiPrimaryContextPass));
}
}
#[derive(Component, Clone)]
#[require(EguiContext)]
pub struct EguiMultipassSchedule(pub InternedScheduleLabel);
impl EguiMultipassSchedule {
pub fn new(schedule: impl ScheduleLabel) -> Self {
Self(schedule.intern())
}
}
#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
pub struct EguiInput(pub egui::RawInput);
#[derive(Component, Clone, Default, Deref, DerefMut)]
pub struct EguiFullOutput(pub Option<egui::FullOutput>);
#[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
#[derive(Default, Resource)]
pub struct EguiClipboard {
#[cfg(not(target_arch = "wasm32"))]
clipboard: thread_local::ThreadLocal<Option<RefCell<Clipboard>>>,
#[cfg(target_arch = "wasm32")]
clipboard: web_clipboard::WebClipboard,
}
#[derive(Component, Clone, Default, Debug)]
pub struct EguiRenderOutput {
pub paint_jobs: Vec<egui::ClippedPrimitive>,
pub textures_delta: egui::TexturesDelta,
}
impl EguiRenderOutput {
pub fn is_empty(&self) -> bool {
self.paint_jobs.is_empty() && self.textures_delta.is_empty()
}
}
#[derive(Component, Clone, Default)]
pub struct EguiOutput {
pub platform_output: egui::PlatformOutput,
}
#[derive(Clone, Component, Default)]
#[require(
EguiContextSettings,
EguiInput,
EguiContextPointerPosition,
EguiContextPointerTouchId,
EguiContextImeState,
EguiFullOutput,
EguiRenderOutput,
EguiOutput
)]
pub struct EguiContext {
ctx: egui::Context,
}
impl EguiContext {
#[cfg(feature = "immutable_ctx")]
#[must_use]
pub fn get(&self) -> &egui::Context {
&self.ctx
}
#[must_use]
pub fn get_mut(&mut self) -> &mut egui::Context {
&mut self.ctx
}
}
type EguiContextsPrimaryQuery<'w, 's> =
Query<'w, 's, &'static mut EguiContext, With<PrimaryEguiContext>>;
type EguiContextsQuery<'w, 's> = Query<
'w,
's,
(
&'static mut EguiContext,
Option<&'static PrimaryEguiContext>,
),
>;
#[derive(SystemParam)]
pub struct EguiContexts<'w, 's> {
q: EguiContextsQuery<'w, 's>,
#[cfg(feature = "render")]
user_textures: ResMut<'w, EguiUserTextures>,
}
#[allow(clippy::manual_try_fold)]
impl EguiContexts<'_, '_> {
#[inline]
pub fn ctx_mut(&mut self) -> Result<&mut egui::Context, QuerySingleError> {
self.q.iter_mut().fold(
Err(QuerySingleError::NoEntities(
bevy_utils::prelude::DebugName::type_name::<EguiContextsPrimaryQuery>(),
)),
|result, (ctx, primary)| match (&result, primary) {
(Err(QuerySingleError::MultipleEntities(_)), _) => result,
(Err(QuerySingleError::NoEntities(_)), Some(_)) => Ok(ctx.into_inner().get_mut()),
(Err(QuerySingleError::NoEntities(_)), None) => result,
(Ok(_), Some(_)) => Err(QuerySingleError::MultipleEntities(
bevy_utils::prelude::DebugName::type_name::<EguiContextsPrimaryQuery>(),
)),
(Ok(_), None) => result,
},
)
}
#[inline]
pub fn ctx_for_entity_mut(
&mut self,
entity: Entity,
) -> Result<&mut egui::Context, QueryEntityError> {
self.q
.get_mut(entity)
.map(|(context, _primary)| context.into_inner().get_mut())
}
#[inline]
pub fn ctx_for_entities_mut<const N: usize>(
&mut self,
ids: [Entity; N],
) -> Result<[&mut egui::Context; N], QueryEntityError> {
self.q
.get_many_mut(ids)
.map(|arr| arr.map(|(ctx, _primary_window)| ctx.into_inner().get_mut()))
}
#[cfg(feature = "immutable_ctx")]
#[inline]
pub fn ctx(&self) -> Result<&egui::Context, QuerySingleError> {
self.q.iter().fold(
Err(QuerySingleError::NoEntities(
bevy_utils::prelude::DebugName::type_name::<EguiContextsPrimaryQuery>(),
)),
|result, (ctx, primary)| match (&result, primary) {
(Err(QuerySingleError::MultipleEntities(_)), _) => result,
(Err(QuerySingleError::NoEntities(_)), Some(_)) => Ok(ctx.get()),
(Err(QuerySingleError::NoEntities(_)), None) => result,
(Ok(_), Some(_)) => Err(QuerySingleError::MultipleEntities(
bevy_utils::prelude::DebugName::type_name::<EguiContextsPrimaryQuery>(),
)),
(Ok(_), None) => result,
},
)
}
#[inline]
#[cfg(feature = "immutable_ctx")]
pub fn ctx_for_entity(&self, entity: Entity) -> Result<&egui::Context, QueryEntityError> {
self.q.get(entity).map(|(context, _primary)| context.get())
}
#[cfg(feature = "render")]
pub fn add_image(&mut self, image: EguiTextureHandle) -> egui::TextureId {
self.user_textures.add_image(image)
}
#[cfg(feature = "render")]
#[track_caller]
pub fn remove_image(&mut self, image: impl Into<AssetId<Image>>) -> Option<egui::TextureId> {
self.user_textures.remove_image(image)
}
#[cfg(feature = "render")]
#[must_use]
#[track_caller]
pub fn image_id(&self, image: impl Into<AssetId<Image>>) -> Option<egui::TextureId> {
self.user_textures.image_id(image)
}
}
#[derive(Clone, Resource, ExtractResource)]
#[cfg(feature = "render")]
pub struct EguiUserTextures {
textures: HashMap<AssetId<Image>, (EguiTextureHandle, u64)>,
free_list: Vec<u64>,
}
#[cfg(feature = "render")]
impl Default for EguiUserTextures {
fn default() -> Self {
Self {
textures: HashMap::default(),
free_list: vec![0],
}
}
}
#[cfg(feature = "render")]
impl EguiUserTextures {
pub fn add_image(&mut self, image: EguiTextureHandle) -> egui::TextureId {
let (_, id) = *self.textures.entry(image.asset_id()).or_insert_with(|| {
let id = self
.free_list
.pop()
.expect("free list must contain at least 1 element");
log::debug!("Add a new image (id: {}, handle: {:?})", id, image);
if self.free_list.is_empty() {
self.free_list.push(id.checked_add(1).expect("out of ids"));
}
(image, id)
});
egui::TextureId::User(id)
}
pub fn remove_image(&mut self, image: impl Into<AssetId<Image>>) -> Option<egui::TextureId> {
let image = image.into();
let id = self.textures.remove(&image);
log::debug!("Remove image (id: {:?}, handle: {:?})", id, image);
if let Some((_, id)) = id {
self.free_list.push(id);
}
id.map(|(_, id)| egui::TextureId::User(id))
}
#[must_use]
pub fn image_id(&self, image: impl Into<AssetId<Image>>) -> Option<egui::TextureId> {
let image = image.into();
self.textures
.get(&image)
.map(|&(_, id)| egui::TextureId::User(id))
}
}
#[cfg(feature = "render")]
#[derive(Clone, Debug)]
pub enum EguiTextureHandle {
Strong(Handle<Image>),
Weak(AssetId<Image>),
}
#[cfg(feature = "render")]
impl EguiTextureHandle {
pub fn asset_id(&self) -> AssetId<Image> {
match self {
EguiTextureHandle::Strong(handle) => handle.id(),
EguiTextureHandle::Weak(asset_id) => *asset_id,
}
}
}
#[cfg(feature = "render")]
impl From<EguiTextureHandle> for AssetId<Image> {
fn from(value: EguiTextureHandle) -> Self {
value.asset_id()
}
}
#[derive(Component, Debug, Default, Clone, Copy, PartialEq)]
pub struct RenderComputedScaleFactor {
pub scale_factor: f32,
}
pub mod node {
pub const EGUI_PASS: &str = "egui_pass";
}
#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
pub enum EguiStartupSet {
InitContexts,
}
#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
pub enum EguiPreUpdateSet {
InitContexts,
ProcessInput,
BeginPass,
}
#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
pub enum EguiInputSet {
InitReading,
FocusContext,
ReadBevyMessages,
WriteEguiEvents,
}
#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
pub enum EguiPostUpdateSet {
EndPass,
ProcessOutput,
PostProcessOutput,
}
impl Plugin for EguiPlugin {
fn build(&self, app: &mut App) {
app.register_type::<EguiGlobalSettings>();
app.register_type::<EguiContextSettings>();
app.init_resource::<EguiGlobalSettings>();
app.init_resource::<ModifierKeysState>();
app.init_resource::<EguiWantsInput>();
app.init_resource::<WindowToEguiContextMap>();
app.add_message::<EguiInputEvent>();
app.add_message::<EguiFileDragAndDropMessage>();
#[allow(deprecated)]
if self.enable_multipass_for_primary_context {
app.insert_resource(EnableMultipassForPrimaryContext);
}
#[cfg(feature = "render")]
{
app.init_resource::<EguiManagedTextures>();
app.init_resource::<EguiUserTextures>();
app.add_plugins(ExtractResourcePlugin::<EguiUserTextures>::default());
app.add_plugins(ExtractResourcePlugin::<
render::systems::ExtractedEguiManagedTextures,
>::default());
}
#[cfg(target_arch = "wasm32")]
app.init_non_send_resource::<SubscribedEvents>();
#[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
app.init_resource::<EguiClipboard>();
app.configure_sets(
PreUpdate,
(
EguiPreUpdateSet::InitContexts,
EguiPreUpdateSet::ProcessInput.after(InputSystems),
EguiPreUpdateSet::BeginPass,
)
.chain(),
);
app.configure_sets(
PreUpdate,
(
EguiInputSet::InitReading,
EguiInputSet::FocusContext,
EguiInputSet::ReadBevyMessages,
EguiInputSet::WriteEguiEvents,
)
.chain(),
);
#[cfg(not(feature = "accesskit"))]
app.configure_sets(
PostUpdate,
(
EguiPostUpdateSet::EndPass,
EguiPostUpdateSet::ProcessOutput,
EguiPostUpdateSet::PostProcessOutput,
)
.chain(),
);
#[cfg(feature = "accesskit")]
app.configure_sets(
PostUpdate,
(
EguiPostUpdateSet::EndPass,
EguiPostUpdateSet::ProcessOutput,
EguiPostUpdateSet::PostProcessOutput
.before(bevy_a11y::AccessibilitySystems::Update),
)
.chain(),
);
#[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
{
app.add_systems(PreStartup, web_clipboard::startup_setup_web_events_system);
}
app.add_systems(
PreStartup,
(
(setup_primary_egui_context_system, ApplyDeferred)
.run_if(|s: Res<EguiGlobalSettings>| s.auto_create_primary_context),
update_ui_size_and_scale_system,
)
.chain()
.in_set(EguiStartupSet::InitContexts),
);
app.add_systems(
PreUpdate,
(
setup_primary_egui_context_system
.run_if(|s: Res<EguiGlobalSettings>| s.auto_create_primary_context),
WindowToEguiContextMap::on_egui_context_added_system,
WindowToEguiContextMap::on_egui_context_removed_system,
ApplyDeferred,
#[cfg(feature = "accesskit")]
setup_accesskit_system,
update_ui_size_and_scale_system,
)
.chain()
.in_set(EguiPreUpdateSet::InitContexts),
);
app.add_systems(
PreUpdate,
(
(
write_modifiers_keys_state_system.run_if(input_system_is_enabled(|s| {
s.run_write_modifiers_keys_state_system
})),
write_window_pointer_moved_messages_system.run_if(input_system_is_enabled(
|s| s.run_write_window_pointer_moved_messages_system,
)),
)
.in_set(EguiInputSet::InitReading),
(
write_pointer_button_messages_system.run_if(input_system_is_enabled(|s| {
s.run_write_pointer_button_messages_system
})),
write_window_touch_messages_system.run_if(input_system_is_enabled(|s| {
s.run_write_window_touch_messages_system
})),
)
.in_set(EguiInputSet::FocusContext),
(
write_non_window_pointer_moved_messages_system.run_if(input_system_is_enabled(
|s| s.run_write_non_window_pointer_moved_messages_system,
)),
write_non_window_touch_messages_system.run_if(input_system_is_enabled(|s| {
s.run_write_non_window_touch_messages_system
})),
write_mouse_wheel_messages_system.run_if(input_system_is_enabled(|s| {
s.run_write_mouse_wheel_messages_system
})),
write_keyboard_input_messages_system.run_if(input_system_is_enabled(|s| {
s.run_write_keyboard_input_messages_system
})),
write_ime_messages_system
.run_if(input_system_is_enabled(|s| s.run_write_ime_messages_system))
.run_if(|s: Res<EguiGlobalSettings>| s.enable_ime),
write_file_dnd_messages_system.run_if(input_system_is_enabled(|s| {
s.run_write_file_dnd_messages_system
})),
)
.in_set(EguiInputSet::ReadBevyMessages),
(
write_egui_input_system,
absorb_bevy_input_system.run_if(|settings: Res<EguiGlobalSettings>| {
settings.enable_absorb_bevy_input_system
}),
)
.in_set(EguiInputSet::WriteEguiEvents),
)
.chain()
.in_set(EguiPreUpdateSet::ProcessInput),
);
app.add_systems(
PreUpdate,
begin_pass_system.in_set(EguiPreUpdateSet::BeginPass),
);
#[cfg(target_arch = "wasm32")]
{
use std::sync::{LazyLock, Mutex};
let maybe_window_plugin = app.get_added_plugins::<bevy_window::WindowPlugin>();
if !maybe_window_plugin.is_empty()
&& maybe_window_plugin[0].primary_window.is_some()
&& maybe_window_plugin[0]
.primary_window
.as_ref()
.unwrap()
.prevent_default_event_handling
{
app.init_resource::<TextAgentChannel>();
let (sender, receiver) = crossbeam_channel::unbounded();
static TOUCH_INFO: LazyLock<Mutex<VirtualTouchInfo>> =
LazyLock::new(|| Mutex::new(VirtualTouchInfo::default()));
app.insert_resource(SafariVirtualKeyboardTouchState {
sender,
receiver,
touch_info: &TOUCH_INFO,
});
app.add_systems(
PreStartup,
install_text_agent_system.in_set(EguiStartupSet::InitContexts),
);
app.add_systems(
PreUpdate,
write_text_agent_channel_events_system
.run_if(input_system_is_enabled(|s| {
s.run_write_text_agent_channel_messages_system
}))
.in_set(EguiPreUpdateSet::ProcessInput)
.in_set(EguiInputSet::ReadBevyMessages),
);
if is_mobile_safari() {
app.add_systems(
PostUpdate,
process_safari_virtual_keyboard_system
.in_set(EguiPostUpdateSet::PostProcessOutput),
);
}
}
#[cfg(feature = "manage_clipboard")]
app.add_systems(
PreUpdate,
web_clipboard::write_web_clipboard_events_system
.run_if(input_system_is_enabled(|s| {
s.run_write_web_clipboard_messages_system
}))
.in_set(EguiPreUpdateSet::ProcessInput)
.in_set(EguiInputSet::ReadBevyMessages),
);
}
app.add_systems(
PostUpdate,
(run_egui_context_pass_loop_system, end_pass_system)
.chain()
.in_set(EguiPostUpdateSet::EndPass),
);
app.add_systems(
PostUpdate,
(
process_output_system,
write_egui_wants_input_system,
process_ime_system
.run_if(|s: Res<EguiGlobalSettings>| s.enable_ime)
.after(process_output_system),
)
.in_set(EguiPostUpdateSet::ProcessOutput),
);
#[cfg(feature = "picking")]
if app.is_plugin_added::<bevy_picking::PickingPlugin>() {
app.add_systems(PostUpdate, capture_pointer_input_system);
} else {
log::warn!(
"The `bevy_egui/picking` feature is enabled, but `PickingPlugin` is not added (if you use Bevy's `DefaultPlugins`, make sure the `bevy/bevy_picking` feature is enabled too)"
);
}
#[cfg(all(feature = "bevy_ui", feature = "bevy_picking"))]
match self.ui_render_order {
UiRenderOrder::EguiAboveBevyUi => {
app.insert_resource(EguiPickingOrder(0.6));
}
UiRenderOrder::BevyUiAboveEgui => {
app.insert_resource(EguiPickingOrder(0.4));
}
}
#[cfg(all(not(feature = "bevy_ui"), feature = "bevy_picking"))]
app.insert_resource(crate::EguiPickingOrder(0.6));
#[cfg(feature = "render")]
app.add_systems(
PostUpdate,
update_egui_textures_system.in_set(EguiPostUpdateSet::PostProcessOutput),
)
.add_systems(
Render,
render::systems::prepare_egui_transforms_system.in_set(RenderSystems::Prepare),
)
.add_systems(
Render,
render::systems::queue_bind_groups_system.in_set(RenderSystems::Queue),
)
.add_systems(
Render,
render::systems::queue_pipelines_system.in_set(RenderSystems::Queue),
)
.add_systems(Last, free_egui_textures_system);
#[cfg(feature = "render")]
{
load_internal_asset!(
app,
render::EGUI_SHADER_HANDLE,
"render/egui.wgsl",
bevy_shader::Shader::from_wgsl
);
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
let egui_graph_2d = render::get_egui_graph(render_app);
let egui_graph_3d = render::get_egui_graph(render_app);
let mut graph = render_app
.world_mut()
.resource_mut::<bevy_render::render_graph::RenderGraph>();
if let Some(graph_2d) =
graph.get_sub_graph_mut(bevy_core_pipeline::core_2d::graph::Core2d)
{
graph_2d.add_sub_graph(render::graph::SubGraphEgui, egui_graph_2d);
graph_2d.add_node(
render::graph::NodeEgui::EguiPass,
render::RunEguiSubgraphOnEguiViewNode,
);
graph_2d.add_node_edge(
bevy_core_pipeline::core_2d::graph::Node2d::EndMainPass,
render::graph::NodeEgui::EguiPass,
);
graph_2d.add_node_edge(
bevy_core_pipeline::core_2d::graph::Node2d::EndMainPassPostProcessing,
render::graph::NodeEgui::EguiPass,
);
graph_2d.add_node_edge(
render::graph::NodeEgui::EguiPass,
bevy_core_pipeline::core_2d::graph::Node2d::Upscaling,
);
}
if let Some(graph_3d) =
graph.get_sub_graph_mut(bevy_core_pipeline::core_3d::graph::Core3d)
{
graph_3d.add_sub_graph(render::graph::SubGraphEgui, egui_graph_3d);
graph_3d.add_node(
render::graph::NodeEgui::EguiPass,
render::RunEguiSubgraphOnEguiViewNode,
);
graph_3d.add_node_edge(
bevy_core_pipeline::core_3d::graph::Node3d::EndMainPass,
render::graph::NodeEgui::EguiPass,
);
graph_3d.add_node_edge(
bevy_core_pipeline::core_3d::graph::Node3d::EndMainPassPostProcessing,
render::graph::NodeEgui::EguiPass,
);
graph_3d.add_node_edge(
render::graph::NodeEgui::EguiPass,
bevy_core_pipeline::core_3d::graph::Node3d::Upscaling,
);
}
}
#[cfg(feature = "accesskit")]
app.add_systems(
PostUpdate,
update_accessibility_system.in_set(EguiPostUpdateSet::PostProcessOutput),
);
}
#[cfg(feature = "render")]
fn finish(&self, app: &mut App) {
#[cfg(feature = "bevy_ui")]
let bevy_ui_is_enabled = app.is_plugin_added::<bevy_ui_render::UiRenderPlugin>();
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.insert_resource(render::EguiRenderSettings {
bindless_mode_array_size: self.bindless_mode_array_size,
})
.init_resource::<render::EguiPipeline>()
.init_resource::<SpecializedRenderPipelines<render::EguiPipeline>>()
.init_resource::<render::systems::EguiTransforms>()
.init_resource::<render::systems::EguiRenderData>()
.add_systems(
ExtractSchedule,
render::extract_egui_camera_view_system,
)
.add_systems(
Render,
render::systems::prepare_egui_transforms_system.in_set(RenderSystems::Prepare),
)
.add_systems(
Render,
render::systems::prepare_egui_render_target_data_system
.in_set(RenderSystems::Prepare),
)
.add_systems(
Render,
render::systems::queue_bind_groups_system.in_set(RenderSystems::Queue),
)
.add_systems(
Render,
render::systems::queue_pipelines_system.in_set(RenderSystems::Queue),
);
#[cfg(feature = "bevy_ui")]
if bevy_ui_is_enabled {
use bevy_render::render_graph::RenderLabel;
let mut graph = render_app
.world_mut()
.resource_mut::<bevy_render::render_graph::RenderGraph>();
let (below, above) = match self.ui_render_order {
UiRenderOrder::EguiAboveBevyUi => (
bevy_ui_render::graph::NodeUi::UiPass.intern(),
render::graph::NodeEgui::EguiPass.intern(),
),
UiRenderOrder::BevyUiAboveEgui => (
render::graph::NodeEgui::EguiPass.intern(),
bevy_ui_render::graph::NodeUi::UiPass.intern(),
),
};
if let Some(graph_2d) =
graph.get_sub_graph_mut(bevy_core_pipeline::core_2d::graph::Core2d)
{
match graph_2d.get_node_state(bevy_ui_render::graph::NodeUi::UiPass) {
Ok(_) => {
graph_2d.add_node_edge(below, above);
}
Err(err) => log::warn!(
error = &err as &dyn std::error::Error,
"bevy_ui::UiPlugin is enabled but could not be found in 2D render graph, rendering order will be inconsistent",
),
}
}
if let Some(graph_3d) =
graph.get_sub_graph_mut(bevy_core_pipeline::core_3d::graph::Core3d)
{
match graph_3d.get_node_state(bevy_ui_render::graph::NodeUi::UiPass) {
Ok(_) => {
graph_3d.add_node_edge(below, above);
}
Err(err) => log::warn!(
error = &err as &dyn std::error::Error,
"bevy_ui::UiPlugin is enabled but could not be found in 3D render graph, rendering order will be inconsistent",
),
}
}
} else {
log::debug!(
"bevy_ui feature is enabled, but bevy_ui::UiPlugin is disabled, not applying configured rendering order"
)
}
}
}
}
fn input_system_is_enabled(
test: impl Fn(&EguiInputSystemSettings) -> bool,
) -> impl Fn(Res<EguiGlobalSettings>) -> bool {
move |settings| test(&settings.input_system_settings)
}
#[cfg(feature = "render")]
#[derive(Resource, Deref, DerefMut, Default)]
pub struct EguiManagedTextures(pub HashMap<(Entity, u64), EguiManagedTexture>);
#[cfg(feature = "render")]
pub struct EguiManagedTexture {
pub handle: Handle<Image>,
pub color_image: egui::ColorImage,
}
pub fn setup_primary_egui_context_system(
mut commands: Commands,
new_cameras: Query<(Entity, Option<&EguiContext>), Added<bevy_camera::Camera>>,
mut egui_context_exists: Local<bool>,
) -> Result {
for (camera_entity, context) in new_cameras {
if context.is_some() || *egui_context_exists {
*egui_context_exists = true;
return Ok(());
}
let context = EguiContext::default();
log::debug!("Creating a primary Egui context");
let mut camera_commands = commands.get_entity(camera_entity)?;
camera_commands.insert((context, PrimaryEguiContext));
*egui_context_exists = true;
}
Ok(())
}
#[cfg(feature = "accesskit")]
pub fn setup_accesskit_system(
new_contexts: Query<(Entity, &mut EguiContext), Added<EguiContext>>,
window_to_egui_context_map: Res<WindowToEguiContextMap>,
mut manage_accessibility_updates: ResMut<bevy_a11y::ManageAccessibilityUpdates>,
_non_send_marker: bevy_ecs::system::NonSendMarker,
) {
bevy_winit::accessibility::ACCESS_KIT_ADAPTERS.with_borrow(|adapters| {
for (new_context_entity, context) in new_contexts.iter() {
if let Some(window_entity) = window_to_egui_context_map
.context_to_window
.get(&new_context_entity)
{
if adapters.contains_key(window_entity) {
context.ctx.enable_accesskit();
**manage_accessibility_updates = false;
}
}
}
});
}
#[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
impl EguiClipboard {
pub fn set_text(&mut self, contents: &str) {
self.set_text_impl(contents);
}
#[cfg(target_arch = "wasm32")]
pub fn set_text_internal(&mut self, text: &str) {
self.clipboard.set_text_internal(text);
}
#[must_use]
pub fn get_text(&mut self) -> Option<String> {
self.get_text_impl()
}
pub fn set_image(&mut self, image: &egui::ColorImage) {
self.set_image_impl(image);
}
#[cfg(target_arch = "wasm32")]
pub fn try_receive_clipboard_event(&self) -> Option<web_clipboard::WebClipboardEvent> {
self.clipboard.try_receive_clipboard_event()
}
#[cfg(not(target_arch = "wasm32"))]
fn set_text_impl(&mut self, contents: &str) {
if let Some(mut clipboard) = self.get()
&& let Err(err) = clipboard.set_text(contents.to_owned())
{
log::error!("Failed to set clipboard contents: {:?}", err);
}
}
#[cfg(target_arch = "wasm32")]
fn set_text_impl(&mut self, contents: &str) {
self.clipboard.set_text(contents);
}
#[cfg(not(target_arch = "wasm32"))]
fn get_text_impl(&mut self) -> Option<String> {
if let Some(mut clipboard) = self.get() {
match clipboard.get_text() {
Ok(contents) => return Some(contents),
Err(arboard::Error::ContentNotAvailable) => return Some("".to_string()),
Err(err) => log::error!("Failed to get clipboard contents: {:?}", err),
}
};
None
}
#[cfg(target_arch = "wasm32")]
#[allow(clippy::unnecessary_wraps)]
fn get_text_impl(&mut self) -> Option<String> {
self.clipboard.get_text()
}
#[cfg(not(target_arch = "wasm32"))]
fn set_image_impl(&mut self, image: &egui::ColorImage) {
if let Some(mut clipboard) = self.get()
&& let Err(err) = clipboard.set_image(arboard::ImageData {
width: image.width(),
height: image.height(),
bytes: std::borrow::Cow::Borrowed(bytemuck::cast_slice(&image.pixels)),
})
{
log::error!("Failed to set clipboard contents: {:?}", err);
}
}
#[cfg(target_arch = "wasm32")]
fn set_image_impl(&mut self, image: &egui::ColorImage) {
self.clipboard.set_image(image);
}
#[cfg(not(target_arch = "wasm32"))]
fn get(&self) -> Option<RefMut<'_, Clipboard>> {
self.clipboard
.get_or(|| {
Clipboard::new()
.map(RefCell::new)
.map_err(|err| {
log::error!("Failed to initialize clipboard: {:?}", err);
})
.ok()
})
.as_ref()
.map(|cell| cell.borrow_mut())
}
}
#[cfg(feature = "picking")]
#[derive(Resource, Debug, Deref)]
pub struct EguiPickingOrder(pub f32);
#[cfg(feature = "picking")]
pub fn capture_pointer_input_system(
pointers: Query<(&PointerId, &PointerLocation)>,
mut egui_context: Query<(
Entity,
&mut EguiContext,
&EguiContextSettings,
&bevy_camera::Camera,
)>,
mut output: MessageWriter<PointerHits>,
window_to_egui_context_map: Res<WindowToEguiContextMap>,
picking_order: Res<EguiPickingOrder>,
) {
use helpers::QueryHelper;
for (pointer, location) in pointers
.iter()
.filter_map(|(i, p)| p.location.as_ref().map(|l| (i, l)))
{
if let NormalizedRenderTarget::Window(window) = location.target {
for window_context_entity in window_to_egui_context_map
.window_to_contexts
.get(&window.entity())
.cloned()
.unwrap_or_default()
{
let Some((entity, mut ctx, settings, camera)) =
egui_context.get_some_mut(window_context_entity)
else {
continue;
};
if !camera
.physical_viewport_rect()
.is_some_and(|rect| rect.as_rect().contains(location.position))
{
continue;
}
if settings.capture_pointer_input && ctx.get_mut().wants_pointer_input() {
let entry = (entity, HitData::new(entity, 0.0, None, None));
output.write(PointerHits::new(
*pointer,
Vec::from([entry]),
camera.order as f32 + **picking_order,
));
}
}
}
}
}
#[cfg(feature = "render")]
pub fn update_egui_textures_system(
mut egui_render_output: Query<(Entity, &EguiRenderOutput)>,
mut egui_managed_textures: ResMut<EguiManagedTextures>,
mut image_assets: ResMut<Assets<Image>>,
) {
use bevy_image::TextureAccessError;
for (entity, egui_render_output) in egui_render_output.iter_mut() {
for (texture_id, image_delta) in &egui_render_output.textures_delta.set {
let color_image = render::as_color_image(&image_delta.image);
let texture_id = match texture_id {
egui::TextureId::Managed(texture_id) => *texture_id,
egui::TextureId::User(_) => continue,
};
let sampler = ImageSampler::Descriptor(render::texture_options_as_sampler_descriptor(
&image_delta.options,
));
if let Some(pos) = image_delta.pos {
if let Some(managed_texture) = egui_managed_textures.get_mut(&(entity, texture_id))
&& let Some(image) = image_assets.get_mut(managed_texture.handle.id())
{
if update_image_rect(image, pos, &color_image).is_err() {
log::error!(
"Failed to write into texture (id: {:?}) for partial update",
texture_id
);
}
} else {
log::warn!("Partial update of a missing texture (id: {:?})", texture_id);
}
} else {
let image = render::color_image_as_bevy_image(&color_image, sampler);
let handle = image_assets.add(image);
egui_managed_textures.insert(
(entity, texture_id),
EguiManagedTexture {
handle,
color_image,
},
);
}
}
}
fn update_image_rect(
dest: &mut Image,
[x, y]: [usize; 2],
src: &egui::ColorImage,
) -> Result<(), TextureAccessError> {
for sy in 0..src.height() {
for sx in 0..src.width() {
let px = src[(sx, sy)];
dest.set_color_at(
(x + sx) as u32,
(y + sy) as u32,
bevy_color::Color::srgba_u8(px.r(), px.g(), px.b(), px.a()),
)?;
}
}
Ok(())
}
}
#[cfg(feature = "render")]
pub fn free_egui_textures_system(
mut egui_user_textures: ResMut<EguiUserTextures>,
egui_render_output: Query<(Entity, &EguiRenderOutput)>,
mut egui_managed_textures: ResMut<EguiManagedTextures>,
mut image_assets: ResMut<Assets<Image>>,
mut image_event_reader: MessageReader<AssetEvent<Image>>,
) {
for (entity, egui_render_output) in egui_render_output.iter() {
for &texture_id in &egui_render_output.textures_delta.free {
if let egui::TextureId::Managed(texture_id) = texture_id {
let managed_texture = egui_managed_textures.remove(&(entity, texture_id));
if let Some(managed_texture) = managed_texture {
image_assets.remove(&managed_texture.handle);
}
}
}
}
for message in image_event_reader.read() {
if let AssetEvent::Removed { id } = message {
egui_user_textures.remove_image(EguiTextureHandle::Weak(*id));
}
}
}
#[cfg(target_arch = "wasm32")]
pub fn string_from_js_value(value: &JsValue) -> String {
value.as_string().unwrap_or_else(|| format!("{value:#?}"))
}
#[cfg(target_arch = "wasm32")]
struct EventClosure<T> {
target: web_sys::EventTarget,
event_name: String,
closure: wasm_bindgen::closure::Closure<dyn FnMut(T)>,
}
#[cfg(target_arch = "wasm32")]
#[derive(Default)]
pub struct SubscribedEvents {
#[cfg(feature = "manage_clipboard")]
clipboard_event_closures: Vec<EventClosure<web_sys::ClipboardEvent>>,
composition_event_closures: Vec<EventClosure<web_sys::CompositionEvent>>,
keyboard_event_closures: Vec<EventClosure<web_sys::KeyboardEvent>>,
input_event_closures: Vec<EventClosure<web_sys::InputEvent>>,
touch_event_closures: Vec<EventClosure<web_sys::TouchEvent>>,
}
#[cfg(target_arch = "wasm32")]
impl SubscribedEvents {
pub fn unsubscribe_from_all_events(&mut self) {
#[cfg(feature = "manage_clipboard")]
Self::unsubscribe_from_events(&mut self.clipboard_event_closures);
Self::unsubscribe_from_events(&mut self.composition_event_closures);
Self::unsubscribe_from_events(&mut self.keyboard_event_closures);
Self::unsubscribe_from_events(&mut self.input_event_closures);
Self::unsubscribe_from_events(&mut self.touch_event_closures);
}
fn unsubscribe_from_events<T>(events: &mut Vec<EventClosure<T>>) {
let events_to_unsubscribe = std::mem::take(events);
if !events_to_unsubscribe.is_empty() {
for event in events_to_unsubscribe {
if let Err(err) = event.target.remove_event_listener_with_callback(
event.event_name.as_str(),
event.closure.as_ref().unchecked_ref(),
) {
log::error!(
"Failed to unsubscribe from event: {}",
string_from_js_value(&err)
);
}
}
}
}
}
#[derive(QueryData)]
#[query_data(mutable)]
#[allow(missing_docs)]
pub struct UpdateUiSizeAndScaleQuery {
ctx: &'static mut EguiContext,
egui_input: &'static mut EguiInput,
egui_settings: &'static EguiContextSettings,
camera: &'static bevy_camera::Camera,
}
pub fn update_ui_size_and_scale_system(mut contexts: Query<UpdateUiSizeAndScaleQuery>) {
for mut context in contexts.iter_mut() {
let Some((scale_factor, viewport_rect)) = context
.camera
.target_scaling_factor()
.map(|scale_factor| scale_factor * context.egui_settings.scale_factor)
.zip(context.camera.physical_viewport_rect())
else {
continue;
};
let viewport_rect = egui::Rect {
min: helpers::vec2_into_egui_pos2(viewport_rect.min.as_vec2() / scale_factor),
max: helpers::vec2_into_egui_pos2(viewport_rect.max.as_vec2() / scale_factor),
};
if viewport_rect.width() < 1.0 || viewport_rect.height() < 1.0 {
continue;
}
context.egui_input.screen_rect = Some(viewport_rect);
context.ctx.get_mut().set_pixels_per_point(scale_factor);
}
}
pub fn begin_pass_system(
mut contexts: Query<
(&mut EguiContext, &EguiContextSettings, &mut EguiInput),
Without<EguiMultipassSchedule>,
>,
) {
for (mut ctx, egui_settings, mut egui_input) in contexts.iter_mut() {
if !egui_settings.run_manually {
ctx.get_mut().begin_pass(egui_input.take());
}
}
}
pub fn end_pass_system(
mut contexts: Query<
(&mut EguiContext, &EguiContextSettings, &mut EguiFullOutput),
Without<EguiMultipassSchedule>,
>,
) {
for (mut ctx, egui_settings, mut full_output) in contexts.iter_mut() {
if !egui_settings.run_manually {
**full_output = Some(ctx.get_mut().end_pass());
}
}
}
#[cfg(feature = "accesskit")]
pub fn update_accessibility_system(
requested: Res<bevy_a11y::AccessibilityRequested>,
mut manage_accessibility_updates: ResMut<bevy_a11y::ManageAccessibilityUpdates>,
window_to_egui_context_map: Res<WindowToEguiContextMap>,
outputs: Query<(Entity, &EguiOutput)>,
_non_send_marker: bevy_ecs::system::NonSendMarker,
) {
if requested.get() {
bevy_winit::accessibility::ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| {
for (entity, output) in &outputs {
if let Some(window_entity) =
window_to_egui_context_map.context_to_window.get(&entity)
&& let Some(adapter) = adapters.get_mut(window_entity)
{
if let Some(update) = &output.platform_output.accesskit_update {
**manage_accessibility_updates = false;
adapter.update_if_active(|| update.clone());
} else if !**manage_accessibility_updates {
**manage_accessibility_updates = true;
}
}
}
});
}
}
#[derive(QueryData)]
#[query_data(mutable)]
#[allow(missing_docs)]
pub struct MultiPassEguiQuery {
entity: Entity,
context: &'static mut EguiContext,
input: &'static mut EguiInput,
output: &'static mut EguiFullOutput,
multipass_schedule: &'static EguiMultipassSchedule,
settings: &'static EguiContextSettings,
}
pub fn run_egui_context_pass_loop_system(world: &mut World) {
let mut contexts_query = world.query::<MultiPassEguiQuery>();
let mut used_schedules = HashSet::<InternedScheduleLabel>::default();
let mut multipass_contexts: Vec<_> = contexts_query
.iter_mut(world)
.filter_map(|mut egui_context| {
if egui_context.settings.run_manually {
return None;
}
Some((
egui_context.entity,
egui_context.context.get_mut().clone(),
egui_context.input.take(),
egui_context.multipass_schedule.clone(),
))
})
.collect();
for (entity, ctx, input, EguiMultipassSchedule(multipass_schedule)) in &mut multipass_contexts {
if !used_schedules.insert(*multipass_schedule) {
panic!(
"Each Egui context running in the multi-pass mode must have a unique schedule (attempted to reuse schedule {multipass_schedule:?})"
);
}
let output = ctx.run(input.take(), |_| {
let _ = world.try_run_schedule(*multipass_schedule);
});
**contexts_query
.get_mut(world, *entity)
.expect("previously queried context")
.output = Some(output);
}
if world
.query_filtered::<Entity, (With<EguiContext>, With<PrimaryEguiContext>)>()
.iter(world)
.next()
.is_none()
{
return;
}
if !used_schedules.contains(&ScheduleLabel::intern(&EguiPrimaryContextPass)) {
let _ = world.try_run_schedule(EguiPrimaryContextPass);
}
}
#[cfg(feature = "picking")]
pub trait BevyEguiEntityCommandsExt {
fn add_picking_observers_for_context(&mut self, context: Entity) -> &mut Self;
}
#[cfg(feature = "picking")]
impl<'a> BevyEguiEntityCommandsExt for EntityCommands<'a> {
fn add_picking_observers_for_context(&mut self, context: Entity) -> &mut Self {
self.insert(picking::PickableEguiContext(context))
.observe(picking::handle_over_system)
.observe(picking::handle_out_system)
.observe(picking::handle_move_system)
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_readme_deps() {
version_sync::assert_markdown_deps_updated!("README.md");
}
}