1#![warn(missing_docs)]
2#![allow(clippy::type_complexity)]
3
4pub mod helpers;
146pub mod input;
148pub mod output;
150#[cfg(feature = "picking")]
152pub mod picking;
153#[cfg(feature = "render")]
155pub mod render;
156#[cfg(target_arch = "wasm32")]
158pub mod text_agent;
159#[cfg(all(feature = "manage_clipboard", target_arch = "wasm32",))]
161pub mod web_clipboard;
162
163pub use egui;
164
165use crate::input::*;
166#[cfg(target_arch = "wasm32")]
167use crate::text_agent::{
168 install_text_agent_system, is_mobile_safari, process_safari_virtual_keyboard_system,
169 write_text_agent_channel_events_system, SafariVirtualKeyboardTouchState, TextAgentChannel,
170 VirtualTouchInfo,
171};
172#[cfg(all(
173 feature = "manage_clipboard",
174 not(any(target_arch = "wasm32", target_os = "android"))
175))]
176use arboard::Clipboard;
177use bevy_app::prelude::*;
178#[cfg(feature = "render")]
179use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle};
180use bevy_derive::{Deref, DerefMut};
181use bevy_ecs::{
182 prelude::*,
183 query::{QueryData, QueryEntityError, QuerySingleError},
184 schedule::{InternedScheduleLabel, ScheduleLabel},
185 system::SystemParam,
186};
187#[cfg(feature = "render")]
188use bevy_image::{Image, ImageSampler};
189use bevy_input::InputSystem;
190#[allow(unused_imports)]
191use bevy_log as log;
192#[cfg(feature = "picking")]
193use bevy_picking::{
194 backend::{HitData, PointerHits},
195 pointer::{PointerId, PointerLocation},
196};
197#[cfg(feature = "render")]
198use bevy_platform::collections::HashMap;
199use bevy_platform::collections::HashSet;
200use bevy_reflect::Reflect;
201#[cfg(feature = "picking")]
202use bevy_render::camera::NormalizedRenderTarget;
203#[cfg(feature = "render")]
204use bevy_render::{
205 extract_resource::{ExtractResource, ExtractResourcePlugin},
206 render_resource::SpecializedRenderPipelines,
207 ExtractSchedule, Render, RenderApp, RenderSet,
208};
209use bevy_winit::cursor::CursorIcon;
210use output::process_output_system;
211#[cfg(all(
212 feature = "manage_clipboard",
213 not(any(target_arch = "wasm32", target_os = "android"))
214))]
215use std::cell::{RefCell, RefMut};
216#[cfg(target_arch = "wasm32")]
217use wasm_bindgen::prelude::*;
218
219pub struct EguiPlugin {
221 #[deprecated(
324 note = "The option to disable the multi-pass mode is now deprecated, use `EguiPlugin::default` instead"
325 )]
326 pub enable_multipass_for_primary_context: bool,
327}
328
329impl Default for EguiPlugin {
330 fn default() -> Self {
331 Self {
332 #[allow(deprecated)]
333 enable_multipass_for_primary_context: true,
334 }
335 }
336}
337
338#[derive(Clone, Debug, Resource, Reflect)]
340pub struct EguiGlobalSettings {
341 pub auto_create_primary_context: bool,
345 pub enable_focused_non_window_context_updates: bool,
350 pub input_system_settings: EguiInputSystemSettings,
352 pub enable_absorb_bevy_input_system: bool,
366 pub enable_cursor_icon_updates: bool,
371}
372
373impl Default for EguiGlobalSettings {
374 fn default() -> Self {
375 Self {
376 auto_create_primary_context: true,
377 enable_focused_non_window_context_updates: true,
378 input_system_settings: EguiInputSystemSettings::default(),
379 enable_absorb_bevy_input_system: false,
380 enable_cursor_icon_updates: true,
381 }
382 }
383}
384
385#[derive(Resource)]
387pub struct EnableMultipassForPrimaryContext;
388
389#[derive(Clone, Debug, Component, Reflect)]
391pub struct EguiContextSettings {
392 pub run_manually: bool,
394 pub scale_factor: f32,
408 #[cfg(feature = "open_url")]
411 pub default_open_url_target: Option<String>,
412 #[cfg(feature = "picking")]
414 pub capture_pointer_input: bool,
415 pub input_system_settings: EguiInputSystemSettings,
417 pub enable_cursor_icon_updates: bool,
422}
423
424impl PartialEq for EguiContextSettings {
426 #[allow(clippy::let_and_return)]
427 fn eq(&self, other: &Self) -> bool {
428 let eq = self.scale_factor == other.scale_factor;
429 #[cfg(feature = "open_url")]
430 let eq = eq && self.default_open_url_target == other.default_open_url_target;
431 eq
432 }
433}
434
435impl Default for EguiContextSettings {
436 fn default() -> Self {
437 Self {
438 run_manually: false,
439 scale_factor: 1.0,
440 #[cfg(feature = "open_url")]
441 default_open_url_target: None,
442 #[cfg(feature = "picking")]
443 capture_pointer_input: true,
444 input_system_settings: EguiInputSystemSettings::default(),
445 enable_cursor_icon_updates: true,
446 }
447 }
448}
449
450#[derive(Clone, Debug, Reflect, PartialEq, Eq)]
451pub struct EguiInputSystemSettings {
453 pub run_write_modifiers_keys_state_system: bool,
455 pub run_write_window_pointer_moved_events_system: bool,
457 pub run_write_pointer_button_events_system: bool,
459 pub run_write_window_touch_events_system: bool,
461 pub run_write_non_window_pointer_moved_events_system: bool,
463 pub run_write_mouse_wheel_events_system: bool,
465 pub run_write_non_window_touch_events_system: bool,
467 pub run_write_keyboard_input_events_system: bool,
469 pub run_write_ime_events_system: bool,
471 pub run_write_file_dnd_events_system: bool,
473 #[cfg(target_arch = "wasm32")]
475 pub run_write_text_agent_channel_events_system: bool,
476 #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
478 pub run_write_web_clipboard_events_system: bool,
479}
480
481impl Default for EguiInputSystemSettings {
482 fn default() -> Self {
483 Self {
484 run_write_modifiers_keys_state_system: true,
485 run_write_window_pointer_moved_events_system: true,
486 run_write_pointer_button_events_system: true,
487 run_write_window_touch_events_system: true,
488 run_write_non_window_pointer_moved_events_system: true,
489 run_write_mouse_wheel_events_system: true,
490 run_write_non_window_touch_events_system: true,
491 run_write_keyboard_input_events_system: true,
492 run_write_ime_events_system: true,
493 run_write_file_dnd_events_system: true,
494 #[cfg(target_arch = "wasm32")]
495 run_write_text_agent_channel_events_system: true,
496 #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
497 run_write_web_clipboard_events_system: true,
498 }
499 }
500}
501
502#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
505pub struct EguiPrimaryContextPass;
506
507#[derive(Component, Clone)]
509#[require(EguiMultipassSchedule::new(EguiPrimaryContextPass))]
510pub struct PrimaryEguiContext;
511
512#[derive(Component, Clone)]
515#[require(EguiContext)]
516pub struct EguiMultipassSchedule(pub InternedScheduleLabel);
517
518impl EguiMultipassSchedule {
519 pub fn new(schedule: impl ScheduleLabel) -> Self {
521 Self(schedule.intern())
522 }
523}
524
525#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
529pub struct EguiInput(pub egui::RawInput);
530
531#[derive(Component, Clone, Default, Deref, DerefMut)]
533pub struct EguiFullOutput(pub Option<egui::FullOutput>);
534
535#[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
539#[derive(Default, Resource)]
540pub struct EguiClipboard {
541 #[cfg(not(target_arch = "wasm32"))]
542 clipboard: thread_local::ThreadLocal<Option<RefCell<Clipboard>>>,
543 #[cfg(target_arch = "wasm32")]
544 clipboard: web_clipboard::WebClipboard,
545}
546
547#[derive(Component, Clone, Default, Debug)]
549pub struct EguiRenderOutput {
550 pub paint_jobs: Vec<egui::ClippedPrimitive>,
555 pub textures_delta: egui::TexturesDelta,
557}
558
559impl EguiRenderOutput {
560 pub fn is_empty(&self) -> bool {
562 self.paint_jobs.is_empty() && self.textures_delta.is_empty()
563 }
564}
565
566#[derive(Component, Clone, Default)]
568pub struct EguiOutput {
569 pub platform_output: egui::PlatformOutput,
571}
572
573#[derive(Clone, Component, Default)]
575#[require(
576 EguiContextSettings,
577 EguiInput,
578 EguiContextPointerPosition,
579 EguiContextPointerTouchId,
580 EguiContextImeState,
581 EguiFullOutput,
582 EguiRenderOutput,
583 EguiOutput,
584 CursorIcon
585)]
586pub struct EguiContext {
587 ctx: egui::Context,
588}
589
590impl EguiContext {
591 #[cfg(feature = "immutable_ctx")]
601 #[must_use]
602 pub fn get(&self) -> &egui::Context {
603 &self.ctx
604 }
605
606 #[must_use]
616 pub fn get_mut(&mut self) -> &mut egui::Context {
617 &mut self.ctx
618 }
619}
620
621type EguiContextsPrimaryQuery<'w, 's> =
623 Query<'w, 's, &'static mut EguiContext, With<PrimaryEguiContext>>;
624
625type EguiContextsQuery<'w, 's> = Query<
626 'w,
627 's,
628 (
629 &'static mut EguiContext,
630 Option<&'static PrimaryEguiContext>,
631 ),
632>;
633
634#[derive(SystemParam)]
635pub struct EguiContexts<'w, 's> {
638 q: EguiContextsQuery<'w, 's>,
639 #[cfg(feature = "render")]
640 user_textures: ResMut<'w, EguiUserTextures>,
641}
642
643#[allow(clippy::manual_try_fold)]
644impl EguiContexts<'_, '_> {
645 #[inline]
647 pub fn ctx_mut(&mut self) -> Result<&mut egui::Context, QuerySingleError> {
648 self.q.iter_mut().fold(
649 Err(QuerySingleError::NoEntities(core::any::type_name::<
650 EguiContextsPrimaryQuery,
651 >())),
652 |result, (ctx, primary)| match (&result, primary) {
653 (Err(QuerySingleError::MultipleEntities(_)), _) => result,
654 (Err(QuerySingleError::NoEntities(_)), Some(_)) => Ok(ctx.into_inner().get_mut()),
655 (Err(QuerySingleError::NoEntities(_)), None) => result,
656 (Ok(_), Some(_)) => {
657 Err(QuerySingleError::MultipleEntities(core::any::type_name::<
658 EguiContextsPrimaryQuery,
659 >()))
660 }
661 (Ok(_), None) => result,
662 },
663 )
664 }
665
666 #[inline]
668 pub fn ctx_for_entity_mut(
669 &mut self,
670 entity: Entity,
671 ) -> Result<&mut egui::Context, QueryEntityError> {
672 self.q
673 .get_mut(entity)
674 .map(|(context, _primary)| context.into_inner().get_mut())
675 }
676
677 #[inline]
680 pub fn ctx_for_entities_mut<const N: usize>(
681 &mut self,
682 ids: [Entity; N],
683 ) -> Result<[&mut egui::Context; N], QueryEntityError> {
684 self.q
685 .get_many_mut(ids)
686 .map(|arr| arr.map(|(ctx, _primary_window)| ctx.into_inner().get_mut()))
687 }
688
689 #[cfg(feature = "immutable_ctx")]
699 #[inline]
700 pub fn ctx(&self) -> Result<&egui::Context, QuerySingleError> {
701 self.q.iter().fold(
702 Err(QuerySingleError::NoEntities(core::any::type_name::<
703 EguiContextsPrimaryQuery,
704 >())),
705 |result, (ctx, primary)| match (&result, primary) {
706 (Err(QuerySingleError::MultipleEntities(_)), _) => result,
707 (Err(QuerySingleError::NoEntities(_)), Some(_)) => Ok(ctx.get()),
708 (Err(QuerySingleError::NoEntities(_)), None) => result,
709 (Ok(_), Some(_)) => {
710 Err(QuerySingleError::MultipleEntities(core::any::type_name::<
711 EguiContextsPrimaryQuery,
712 >()))
713 }
714 (Ok(_), None) => result,
715 },
716 )
717 }
718
719 #[inline]
729 #[cfg(feature = "immutable_ctx")]
730 pub fn ctx_for_entity(&self, entity: Entity) -> Result<&egui::Context, QueryEntityError> {
731 self.q.get(entity).map(|(context, _primary)| context.get())
732 }
733
734 #[cfg(feature = "render")]
743 pub fn add_image(&mut self, image: Handle<Image>) -> egui::TextureId {
744 self.user_textures.add_image(image)
745 }
746
747 #[cfg(feature = "render")]
749 #[track_caller]
750 pub fn remove_image(&mut self, image: &Handle<Image>) -> Option<egui::TextureId> {
751 self.user_textures.remove_image(image)
752 }
753
754 #[cfg(feature = "render")]
756 #[must_use]
757 #[track_caller]
758 pub fn image_id(&self, image: &Handle<Image>) -> Option<egui::TextureId> {
759 self.user_textures.image_id(image)
760 }
761}
762
763#[derive(Clone, Resource, ExtractResource)]
765#[cfg(feature = "render")]
766pub struct EguiUserTextures {
767 textures: HashMap<Handle<Image>, u64>,
768 free_list: Vec<u64>,
769}
770
771#[cfg(feature = "render")]
772impl Default for EguiUserTextures {
773 fn default() -> Self {
774 Self {
775 textures: HashMap::default(),
776 free_list: vec![0],
777 }
778 }
779}
780
781#[cfg(feature = "render")]
782impl EguiUserTextures {
783 pub fn add_image(&mut self, image: Handle<Image>) -> egui::TextureId {
792 let id = *self.textures.entry(image.clone()).or_insert_with(|| {
793 let id = self
794 .free_list
795 .pop()
796 .expect("free list must contain at least 1 element");
797 log::debug!("Add a new image (id: {}, handle: {:?})", id, image);
798 if self.free_list.is_empty() {
799 self.free_list.push(id.checked_add(1).expect("out of ids"));
800 }
801 id
802 });
803 egui::TextureId::User(id)
804 }
805
806 pub fn remove_image(&mut self, image: &Handle<Image>) -> Option<egui::TextureId> {
808 let id = self.textures.remove(image);
809 log::debug!("Remove image (id: {:?}, handle: {:?})", id, image);
810 if let Some(id) = id {
811 self.free_list.push(id);
812 }
813 id.map(egui::TextureId::User)
814 }
815
816 #[must_use]
818 pub fn image_id(&self, image: &Handle<Image>) -> Option<egui::TextureId> {
819 self.textures
820 .get(image)
821 .map(|&id| egui::TextureId::User(id))
822 }
823}
824
825#[derive(Component, Debug, Default, Clone, Copy, PartialEq)]
828pub struct RenderComputedScaleFactor {
829 pub scale_factor: f32,
831}
832
833pub mod node {
835 pub const EGUI_PASS: &str = "egui_pass";
837}
838
839#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
840pub enum EguiStartupSet {
842 InitContexts,
844}
845
846#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
848pub enum EguiPreUpdateSet {
849 InitContexts,
851 ProcessInput,
857 BeginPass,
859}
860
861#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
863pub enum EguiInputSet {
864 InitReading,
868 FocusContext,
870 ReadBevyEvents,
872 WriteEguiEvents,
874}
875
876#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
878pub enum EguiPostUpdateSet {
879 EndPass,
881 ProcessOutput,
883 PostProcessOutput,
885}
886
887impl Plugin for EguiPlugin {
888 fn build(&self, app: &mut App) {
889 app.register_type::<EguiGlobalSettings>();
890 app.register_type::<EguiContextSettings>();
891 app.init_resource::<EguiGlobalSettings>();
892 app.init_resource::<ModifierKeysState>();
893 app.init_resource::<EguiWantsInput>();
894 app.init_resource::<WindowToEguiContextMap>();
895 app.add_event::<EguiInputEvent>();
896 app.add_event::<EguiFileDragAndDropEvent>();
897
898 #[allow(deprecated)]
899 if self.enable_multipass_for_primary_context {
900 app.insert_resource(EnableMultipassForPrimaryContext);
901 }
902
903 #[cfg(feature = "render")]
904 {
905 app.init_resource::<EguiManagedTextures>();
906 app.init_resource::<EguiUserTextures>();
907 app.add_plugins(ExtractResourcePlugin::<EguiUserTextures>::default());
908 app.add_plugins(ExtractResourcePlugin::<
909 render::systems::ExtractedEguiManagedTextures,
910 >::default());
911 }
912
913 #[cfg(target_arch = "wasm32")]
914 app.init_non_send_resource::<SubscribedEvents>();
915
916 #[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
917 app.init_resource::<EguiClipboard>();
918
919 app.configure_sets(
920 PreUpdate,
921 (
922 EguiPreUpdateSet::InitContexts,
923 EguiPreUpdateSet::ProcessInput.after(InputSystem),
924 EguiPreUpdateSet::BeginPass,
925 )
926 .chain(),
927 );
928 app.configure_sets(
929 PreUpdate,
930 (
931 EguiInputSet::InitReading,
932 EguiInputSet::FocusContext,
933 EguiInputSet::ReadBevyEvents,
934 EguiInputSet::WriteEguiEvents,
935 )
936 .chain(),
937 );
938 #[cfg(not(feature = "accesskit_placeholder"))]
939 app.configure_sets(
940 PostUpdate,
941 (
942 EguiPostUpdateSet::EndPass,
943 EguiPostUpdateSet::ProcessOutput,
944 EguiPostUpdateSet::PostProcessOutput,
945 )
946 .chain(),
947 );
948 #[cfg(feature = "accesskit_placeholder")]
949 app.configure_sets(
950 PostUpdate,
951 (
952 EguiPostUpdateSet::EndPass,
953 EguiPostUpdateSet::ProcessOutput,
954 EguiPostUpdateSet::PostProcessOutput.before(bevy_a11y::AccessibilitySystem::Update),
955 )
956 .chain(),
957 );
958
959 #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
961 {
962 app.add_systems(PreStartup, web_clipboard::startup_setup_web_events_system);
963 }
964 #[cfg(feature = "render")]
965 app.add_systems(
966 PreStartup,
967 (
968 (setup_primary_egui_context_system, ApplyDeferred)
969 .run_if(|s: Res<EguiGlobalSettings>| s.auto_create_primary_context),
970 update_ui_size_and_scale_system,
971 )
972 .chain()
973 .in_set(EguiStartupSet::InitContexts),
974 );
975
976 #[cfg(feature = "render")]
978 app.add_systems(
979 PreUpdate,
980 (
981 setup_primary_egui_context_system
982 .run_if(|s: Res<EguiGlobalSettings>| s.auto_create_primary_context),
983 WindowToEguiContextMap::on_egui_context_added_system,
984 WindowToEguiContextMap::on_egui_context_removed_system,
985 ApplyDeferred,
986 update_ui_size_and_scale_system,
987 )
988 .chain()
989 .in_set(EguiPreUpdateSet::InitContexts),
990 );
991 app.add_systems(
992 PreUpdate,
993 (
994 (
995 write_modifiers_keys_state_system.run_if(input_system_is_enabled(|s| {
996 s.run_write_modifiers_keys_state_system
997 })),
998 write_window_pointer_moved_events_system.run_if(input_system_is_enabled(|s| {
999 s.run_write_window_pointer_moved_events_system
1000 })),
1001 )
1002 .in_set(EguiInputSet::InitReading),
1003 (
1004 write_pointer_button_events_system.run_if(input_system_is_enabled(|s| {
1005 s.run_write_pointer_button_events_system
1006 })),
1007 write_window_touch_events_system.run_if(input_system_is_enabled(|s| {
1008 s.run_write_window_touch_events_system
1009 })),
1010 )
1011 .in_set(EguiInputSet::FocusContext),
1012 (
1013 write_non_window_pointer_moved_events_system.run_if(input_system_is_enabled(
1014 |s| s.run_write_non_window_pointer_moved_events_system,
1015 )),
1016 write_non_window_touch_events_system.run_if(input_system_is_enabled(|s| {
1017 s.run_write_non_window_touch_events_system
1018 })),
1019 write_mouse_wheel_events_system.run_if(input_system_is_enabled(|s| {
1020 s.run_write_mouse_wheel_events_system
1021 })),
1022 write_keyboard_input_events_system.run_if(input_system_is_enabled(|s| {
1023 s.run_write_keyboard_input_events_system
1024 })),
1025 write_ime_events_system
1026 .run_if(input_system_is_enabled(|s| s.run_write_ime_events_system)),
1027 write_file_dnd_events_system.run_if(input_system_is_enabled(|s| {
1028 s.run_write_file_dnd_events_system
1029 })),
1030 )
1031 .in_set(EguiInputSet::ReadBevyEvents),
1032 (
1033 write_egui_input_system,
1034 absorb_bevy_input_system.run_if(|settings: Res<EguiGlobalSettings>| {
1035 settings.enable_absorb_bevy_input_system
1036 }),
1037 )
1038 .in_set(EguiInputSet::WriteEguiEvents),
1039 )
1040 .chain()
1041 .in_set(EguiPreUpdateSet::ProcessInput),
1042 );
1043 app.add_systems(
1044 PreUpdate,
1045 begin_pass_system.in_set(EguiPreUpdateSet::BeginPass),
1046 );
1047
1048 #[cfg(target_arch = "wasm32")]
1050 {
1051 use std::sync::{LazyLock, Mutex};
1052
1053 let maybe_window_plugin = app.get_added_plugins::<bevy_window::WindowPlugin>();
1054
1055 if !maybe_window_plugin.is_empty()
1056 && maybe_window_plugin[0].primary_window.is_some()
1057 && maybe_window_plugin[0]
1058 .primary_window
1059 .as_ref()
1060 .unwrap()
1061 .prevent_default_event_handling
1062 {
1063 app.init_resource::<TextAgentChannel>();
1064
1065 let (sender, receiver) = crossbeam_channel::unbounded();
1066 static TOUCH_INFO: LazyLock<Mutex<VirtualTouchInfo>> =
1067 LazyLock::new(|| Mutex::new(VirtualTouchInfo::default()));
1068
1069 app.insert_resource(SafariVirtualKeyboardTouchState {
1070 sender,
1071 receiver,
1072 touch_info: &TOUCH_INFO,
1073 });
1074
1075 app.add_systems(
1076 PreStartup,
1077 install_text_agent_system.in_set(EguiStartupSet::InitContexts),
1078 );
1079
1080 app.add_systems(
1081 PreUpdate,
1082 write_text_agent_channel_events_system
1083 .run_if(input_system_is_enabled(|s| {
1084 s.run_write_text_agent_channel_events_system
1085 }))
1086 .in_set(EguiPreUpdateSet::ProcessInput)
1087 .in_set(EguiInputSet::ReadBevyEvents),
1088 );
1089
1090 if is_mobile_safari() {
1091 app.add_systems(
1092 PostUpdate,
1093 process_safari_virtual_keyboard_system
1094 .in_set(EguiPostUpdateSet::PostProcessOutput),
1095 );
1096 }
1097 }
1098
1099 #[cfg(feature = "manage_clipboard")]
1100 app.add_systems(
1101 PreUpdate,
1102 web_clipboard::write_web_clipboard_events_system
1103 .run_if(input_system_is_enabled(|s| {
1104 s.run_write_web_clipboard_events_system
1105 }))
1106 .in_set(EguiPreUpdateSet::ProcessInput)
1107 .in_set(EguiInputSet::ReadBevyEvents),
1108 );
1109 }
1110
1111 app.add_systems(
1113 PostUpdate,
1114 (run_egui_context_pass_loop_system, end_pass_system)
1115 .chain()
1116 .in_set(EguiPostUpdateSet::EndPass),
1117 );
1118 app.add_systems(
1119 PostUpdate,
1120 (
1121 process_output_system,
1122 write_egui_wants_input_system,
1123 #[cfg(any(target_os = "ios", target_os = "android"))]
1124 set_ime_allowed_system,
1126 )
1127 .in_set(EguiPostUpdateSet::ProcessOutput),
1128 );
1129 #[cfg(feature = "picking")]
1130 if app.is_plugin_added::<bevy_picking::PickingPlugin>() {
1131 app.add_systems(PostUpdate, capture_pointer_input_system);
1132 } else {
1133 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)");
1134 }
1135
1136 #[cfg(feature = "render")]
1137 app.add_systems(
1138 PostUpdate,
1139 update_egui_textures_system.in_set(EguiPostUpdateSet::PostProcessOutput),
1140 )
1141 .add_systems(
1142 Render,
1143 render::systems::prepare_egui_transforms_system.in_set(RenderSet::Prepare),
1144 )
1145 .add_systems(
1146 Render,
1147 render::systems::queue_bind_groups_system.in_set(RenderSet::Queue),
1148 )
1149 .add_systems(
1150 Render,
1151 render::systems::queue_pipelines_system.in_set(RenderSet::Queue),
1152 )
1153 .add_systems(Last, free_egui_textures_system);
1154
1155 #[cfg(feature = "render")]
1156 {
1157 load_internal_asset!(
1158 app,
1159 render::EGUI_SHADER_HANDLE,
1160 "render/egui.wgsl",
1161 bevy_render::render_resource::Shader::from_wgsl
1162 );
1163
1164 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
1165 return;
1166 };
1167
1168 let egui_graph_2d = render::get_egui_graph(render_app);
1169 let egui_graph_3d = render::get_egui_graph(render_app);
1170 let mut graph = render_app
1171 .world_mut()
1172 .resource_mut::<bevy_render::render_graph::RenderGraph>();
1173
1174 if let Some(graph_2d) =
1175 graph.get_sub_graph_mut(bevy_core_pipeline::core_2d::graph::Core2d)
1176 {
1177 graph_2d.add_sub_graph(render::graph::SubGraphEgui, egui_graph_2d);
1178 graph_2d.add_node(
1179 render::graph::NodeEgui::EguiPass,
1180 render::RunEguiSubgraphOnEguiViewNode,
1181 );
1182 graph_2d.add_node_edge(
1183 bevy_core_pipeline::core_2d::graph::Node2d::EndMainPass,
1184 render::graph::NodeEgui::EguiPass,
1185 );
1186 graph_2d.add_node_edge(
1187 bevy_core_pipeline::core_2d::graph::Node2d::EndMainPassPostProcessing,
1188 render::graph::NodeEgui::EguiPass,
1189 );
1190 graph_2d.add_node_edge(
1191 render::graph::NodeEgui::EguiPass,
1192 bevy_core_pipeline::core_2d::graph::Node2d::Upscaling,
1193 );
1194 }
1195
1196 if let Some(graph_3d) =
1197 graph.get_sub_graph_mut(bevy_core_pipeline::core_3d::graph::Core3d)
1198 {
1199 graph_3d.add_sub_graph(render::graph::SubGraphEgui, egui_graph_3d);
1200 graph_3d.add_node(
1201 render::graph::NodeEgui::EguiPass,
1202 render::RunEguiSubgraphOnEguiViewNode,
1203 );
1204 graph_3d.add_node_edge(
1205 bevy_core_pipeline::core_3d::graph::Node3d::EndMainPass,
1206 render::graph::NodeEgui::EguiPass,
1207 );
1208 graph_3d.add_node_edge(
1209 bevy_core_pipeline::core_3d::graph::Node3d::EndMainPassPostProcessing,
1210 render::graph::NodeEgui::EguiPass,
1211 );
1212 graph_3d.add_node_edge(
1213 render::graph::NodeEgui::EguiPass,
1214 bevy_core_pipeline::core_3d::graph::Node3d::Upscaling,
1215 );
1216 }
1217 }
1218
1219 #[cfg(feature = "accesskit_placeholder")]
1220 app.add_systems(
1221 PostUpdate,
1222 update_accessibility_system.in_set(EguiPostUpdateSet::PostProcessOutput),
1223 );
1224 }
1225
1226 #[cfg(feature = "render")]
1227 fn finish(&self, app: &mut App) {
1228 if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
1229 render_app
1230 .init_resource::<render::EguiPipeline>()
1231 .init_resource::<SpecializedRenderPipelines<render::EguiPipeline>>()
1232 .init_resource::<render::systems::EguiTransforms>()
1233 .init_resource::<render::systems::EguiRenderData>()
1234 .add_systems(
1235 ExtractSchedule,
1238 render::extract_egui_camera_view_system,
1239 )
1240 .add_systems(
1241 Render,
1242 render::systems::prepare_egui_transforms_system.in_set(RenderSet::Prepare),
1243 )
1244 .add_systems(
1245 Render,
1246 render::systems::prepare_egui_render_target_data_system
1247 .in_set(RenderSet::Prepare),
1248 )
1249 .add_systems(
1250 Render,
1251 render::systems::queue_bind_groups_system.in_set(RenderSet::Queue),
1252 )
1253 .add_systems(
1254 Render,
1255 render::systems::queue_pipelines_system.in_set(RenderSet::Queue),
1256 );
1257 }
1258 }
1259}
1260
1261fn input_system_is_enabled(
1262 test: impl Fn(&EguiInputSystemSettings) -> bool,
1263) -> impl Fn(Res<EguiGlobalSettings>) -> bool {
1264 move |settings| test(&settings.input_system_settings)
1265}
1266
1267#[cfg(feature = "render")]
1269#[derive(Resource, Deref, DerefMut, Default)]
1270pub struct EguiManagedTextures(pub HashMap<(Entity, u64), EguiManagedTexture>);
1271
1272#[cfg(feature = "render")]
1274pub struct EguiManagedTexture {
1275 pub handle: Handle<Image>,
1277 pub color_image: egui::ColorImage,
1279}
1280
1281#[cfg(feature = "render")]
1286pub fn setup_primary_egui_context_system(
1287 mut commands: Commands,
1288 new_cameras: Query<(Entity, Option<&EguiContext>), Added<bevy_render::camera::Camera>>,
1289 #[cfg(feature = "accesskit_placeholder")] adapters: Option<
1290 NonSend<bevy_winit::accessibility::AccessKitAdapters>,
1291 >,
1292 #[cfg(feature = "accesskit_placeholder")] mut manage_accessibility_updates: ResMut<
1293 bevy_a11y::ManageAccessibilityUpdates,
1294 >,
1295 enable_multipass_for_primary_context: Option<Res<EnableMultipassForPrimaryContext>>,
1296 mut egui_context_exists: Local<bool>,
1297) -> Result {
1298 for (camera_entity, context) in new_cameras {
1299 if context.is_some() || *egui_context_exists {
1300 *egui_context_exists = true;
1301 return Ok(());
1302 }
1303
1304 let context = EguiContext::default();
1305 #[cfg(feature = "accesskit_placeholder")]
1306 if let Some(adapters) = &adapters {
1307 if adapters.get(&camera_entity).is_some() {
1309 context.ctx.enable_accesskit();
1310 **manage_accessibility_updates = false;
1311 }
1312 }
1313
1314 log::debug!("Creating a primary Egui context");
1315 let mut camera_commands = commands.get_entity(camera_entity)?;
1317 camera_commands.insert(context).insert(PrimaryEguiContext);
1318 if enable_multipass_for_primary_context.is_some() {
1319 camera_commands.insert(EguiMultipassSchedule::new(EguiPrimaryContextPass));
1320 }
1321 *egui_context_exists = true;
1322 }
1323
1324 Ok(())
1325}
1326
1327#[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
1328impl EguiClipboard {
1329 pub fn set_text(&mut self, contents: &str) {
1331 self.set_text_impl(contents);
1332 }
1333
1334 #[cfg(target_arch = "wasm32")]
1337 pub fn set_text_internal(&mut self, text: &str) {
1338 self.clipboard.set_text_internal(text);
1339 }
1340
1341 #[must_use]
1343 pub fn get_text(&mut self) -> Option<String> {
1344 self.get_text_impl()
1345 }
1346
1347 pub fn set_image(&mut self, image: &egui::ColorImage) {
1349 self.set_image_impl(image);
1350 }
1351
1352 #[cfg(target_arch = "wasm32")]
1354 pub fn try_receive_clipboard_event(&self) -> Option<web_clipboard::WebClipboardEvent> {
1355 self.clipboard.try_receive_clipboard_event()
1356 }
1357
1358 #[cfg(not(target_arch = "wasm32"))]
1359 fn set_text_impl(&mut self, contents: &str) {
1360 if let Some(mut clipboard) = self.get() {
1361 if let Err(err) = clipboard.set_text(contents.to_owned()) {
1362 log::error!("Failed to set clipboard contents: {:?}", err);
1363 }
1364 }
1365 }
1366
1367 #[cfg(target_arch = "wasm32")]
1368 fn set_text_impl(&mut self, contents: &str) {
1369 self.clipboard.set_text(contents);
1370 }
1371
1372 #[cfg(not(target_arch = "wasm32"))]
1373 fn get_text_impl(&mut self) -> Option<String> {
1374 if let Some(mut clipboard) = self.get() {
1375 match clipboard.get_text() {
1376 Ok(contents) => return Some(contents),
1377 Err(arboard::Error::ContentNotAvailable) => return Some("".to_string()),
1379 Err(err) => log::error!("Failed to get clipboard contents: {:?}", err),
1380 }
1381 };
1382 None
1383 }
1384
1385 #[cfg(target_arch = "wasm32")]
1386 #[allow(clippy::unnecessary_wraps)]
1387 fn get_text_impl(&mut self) -> Option<String> {
1388 self.clipboard.get_text()
1389 }
1390
1391 #[cfg(not(target_arch = "wasm32"))]
1392 fn set_image_impl(&mut self, image: &egui::ColorImage) {
1393 if let Some(mut clipboard) = self.get() {
1394 if let Err(err) = clipboard.set_image(arboard::ImageData {
1395 width: image.width(),
1396 height: image.height(),
1397 bytes: std::borrow::Cow::Borrowed(bytemuck::cast_slice(&image.pixels)),
1398 }) {
1399 log::error!("Failed to set clipboard contents: {:?}", err);
1400 }
1401 }
1402 }
1403
1404 #[cfg(target_arch = "wasm32")]
1405 fn set_image_impl(&mut self, image: &egui::ColorImage) {
1406 self.clipboard.set_image(image);
1407 }
1408
1409 #[cfg(not(target_arch = "wasm32"))]
1410 fn get(&self) -> Option<RefMut<Clipboard>> {
1411 self.clipboard
1412 .get_or(|| {
1413 Clipboard::new()
1414 .map(RefCell::new)
1415 .map_err(|err| {
1416 log::error!("Failed to initialize clipboard: {:?}", err);
1417 })
1418 .ok()
1419 })
1420 .as_ref()
1421 .map(|cell| cell.borrow_mut())
1422 }
1423}
1424
1425#[cfg(feature = "picking")]
1427pub const PICKING_ORDER: f32 = 1_000_000.0;
1428
1429#[cfg(feature = "picking")]
1431pub fn capture_pointer_input_system(
1432 pointers: Query<(&PointerId, &PointerLocation)>,
1433 mut egui_context: Query<(
1434 Entity,
1435 &mut EguiContext,
1436 &EguiContextSettings,
1437 &bevy_render::camera::Camera,
1438 )>,
1439 mut output: EventWriter<PointerHits>,
1440 window_to_egui_context_map: Res<WindowToEguiContextMap>,
1441) {
1442 use helpers::QueryHelper;
1443
1444 for (pointer, location) in pointers
1445 .iter()
1446 .filter_map(|(i, p)| p.location.as_ref().map(|l| (i, l)))
1447 {
1448 if let NormalizedRenderTarget::Window(window) = location.target {
1449 for window_context_entity in window_to_egui_context_map
1450 .window_to_contexts
1451 .get(&window.entity())
1452 .cloned()
1453 .unwrap_or_default()
1454 {
1455 let Some((entity, mut ctx, settings, camera)) =
1456 egui_context.get_some_mut(window_context_entity)
1457 else {
1458 continue;
1459 };
1460 if !camera
1461 .physical_viewport_rect()
1462 .is_some_and(|rect| rect.as_rect().contains(location.position))
1463 {
1464 continue;
1465 }
1466
1467 if settings.capture_pointer_input && ctx.get_mut().wants_pointer_input() {
1468 let entry = (entity, HitData::new(entity, 0.0, None, None));
1469 output.write(PointerHits::new(
1470 *pointer,
1471 Vec::from([entry]),
1472 PICKING_ORDER,
1473 ));
1474 }
1475 }
1476 }
1477 }
1478}
1479
1480#[cfg(feature = "render")]
1482pub fn update_egui_textures_system(
1483 mut egui_render_output: Query<(Entity, &EguiRenderOutput)>,
1484 mut egui_managed_textures: ResMut<EguiManagedTextures>,
1485 mut image_assets: ResMut<Assets<Image>>,
1486) {
1487 for (entity, egui_render_output) in egui_render_output.iter_mut() {
1488 for (texture_id, image_delta) in &egui_render_output.textures_delta.set {
1489 let color_image = render::as_color_image(&image_delta.image);
1490
1491 let texture_id = match texture_id {
1492 egui::TextureId::Managed(texture_id) => *texture_id,
1493 egui::TextureId::User(_) => continue,
1494 };
1495
1496 let sampler = ImageSampler::Descriptor(render::texture_options_as_sampler_descriptor(
1497 &image_delta.options,
1498 ));
1499 if let Some(pos) = image_delta.pos {
1500 if let Some(managed_texture) = egui_managed_textures.get_mut(&(entity, texture_id))
1502 {
1503 update_image_rect(&mut managed_texture.color_image, pos, &color_image);
1505 let image =
1506 render::color_image_as_bevy_image(&managed_texture.color_image, sampler);
1507 managed_texture.handle = image_assets.add(image);
1508 } else {
1509 log::warn!("Partial update of a missing texture (id: {:?})", texture_id);
1510 }
1511 } else {
1512 let image = render::color_image_as_bevy_image(&color_image, sampler);
1514 let handle = image_assets.add(image);
1515 egui_managed_textures.insert(
1516 (entity, texture_id),
1517 EguiManagedTexture {
1518 handle,
1519 color_image,
1520 },
1521 );
1522 }
1523 }
1524 }
1525
1526 fn update_image_rect(dest: &mut egui::ColorImage, [x, y]: [usize; 2], src: &egui::ColorImage) {
1527 for sy in 0..src.height() {
1528 for sx in 0..src.width() {
1529 dest[(x + sx, y + sy)] = src[(sx, sy)];
1530 }
1531 }
1532 }
1533}
1534
1535#[cfg(feature = "render")]
1540pub fn free_egui_textures_system(
1541 mut egui_user_textures: ResMut<EguiUserTextures>,
1542 egui_render_output: Query<(Entity, &EguiRenderOutput)>,
1543 mut egui_managed_textures: ResMut<EguiManagedTextures>,
1544 mut image_assets: ResMut<Assets<Image>>,
1545 mut image_events: EventReader<AssetEvent<Image>>,
1546) {
1547 for (entity, egui_render_output) in egui_render_output.iter() {
1548 for &texture_id in &egui_render_output.textures_delta.free {
1549 if let egui::TextureId::Managed(texture_id) = texture_id {
1550 let managed_texture = egui_managed_textures.remove(&(entity, texture_id));
1551 if let Some(managed_texture) = managed_texture {
1552 image_assets.remove(&managed_texture.handle);
1553 }
1554 }
1555 }
1556 }
1557
1558 for image_event in image_events.read() {
1559 if let AssetEvent::Removed { id } = image_event {
1560 egui_user_textures.remove_image(&Handle::<Image>::Weak(*id));
1561 }
1562 }
1563}
1564
1565#[cfg(target_arch = "wasm32")]
1567pub fn string_from_js_value(value: &JsValue) -> String {
1568 value.as_string().unwrap_or_else(|| format!("{value:#?}"))
1569}
1570
1571#[cfg(target_arch = "wasm32")]
1572struct EventClosure<T> {
1573 target: web_sys::EventTarget,
1574 event_name: String,
1575 closure: wasm_bindgen::closure::Closure<dyn FnMut(T)>,
1576}
1577
1578#[cfg(target_arch = "wasm32")]
1580#[derive(Default)]
1581pub struct SubscribedEvents {
1582 #[cfg(feature = "manage_clipboard")]
1583 clipboard_event_closures: Vec<EventClosure<web_sys::ClipboardEvent>>,
1584 composition_event_closures: Vec<EventClosure<web_sys::CompositionEvent>>,
1585 keyboard_event_closures: Vec<EventClosure<web_sys::KeyboardEvent>>,
1586 input_event_closures: Vec<EventClosure<web_sys::InputEvent>>,
1587 touch_event_closures: Vec<EventClosure<web_sys::TouchEvent>>,
1588}
1589
1590#[cfg(target_arch = "wasm32")]
1591impl SubscribedEvents {
1592 pub fn unsubscribe_from_all_events(&mut self) {
1595 #[cfg(feature = "manage_clipboard")]
1596 Self::unsubscribe_from_events(&mut self.clipboard_event_closures);
1597 Self::unsubscribe_from_events(&mut self.composition_event_closures);
1598 Self::unsubscribe_from_events(&mut self.keyboard_event_closures);
1599 Self::unsubscribe_from_events(&mut self.input_event_closures);
1600 Self::unsubscribe_from_events(&mut self.touch_event_closures);
1601 }
1602
1603 fn unsubscribe_from_events<T>(events: &mut Vec<EventClosure<T>>) {
1604 let events_to_unsubscribe = std::mem::take(events);
1605
1606 if !events_to_unsubscribe.is_empty() {
1607 for event in events_to_unsubscribe {
1608 if let Err(err) = event.target.remove_event_listener_with_callback(
1609 event.event_name.as_str(),
1610 event.closure.as_ref().unchecked_ref(),
1611 ) {
1612 log::error!(
1613 "Failed to unsubscribe from event: {}",
1614 string_from_js_value(&err)
1615 );
1616 }
1617 }
1618 }
1619 }
1620}
1621
1622#[derive(QueryData)]
1623#[query_data(mutable)]
1624#[allow(missing_docs)]
1625#[cfg(feature = "render")]
1626pub struct UpdateUiSizeAndScaleQuery {
1627 ctx: &'static mut EguiContext,
1628 egui_input: &'static mut EguiInput,
1629 egui_settings: &'static EguiContextSettings,
1630 camera: &'static bevy_render::camera::Camera,
1631}
1632
1633#[cfg(feature = "render")]
1634pub fn update_ui_size_and_scale_system(mut contexts: Query<UpdateUiSizeAndScaleQuery>) {
1636 for mut context in contexts.iter_mut() {
1637 let Some((scale_factor, viewport_rect)) = context
1638 .camera
1639 .target_scaling_factor()
1640 .map(|scale_factor| scale_factor * context.egui_settings.scale_factor)
1641 .zip(context.camera.physical_viewport_rect())
1642 else {
1643 continue;
1644 };
1645
1646 let viewport_rect = egui::Rect {
1647 min: helpers::vec2_into_egui_pos2(viewport_rect.min.as_vec2() / scale_factor),
1648 max: helpers::vec2_into_egui_pos2(viewport_rect.max.as_vec2() / scale_factor),
1649 };
1650 if viewport_rect.width() < 1.0 || viewport_rect.height() < 1.0 {
1651 continue;
1652 }
1653 context.egui_input.screen_rect = Some(viewport_rect);
1654 context.ctx.get_mut().set_pixels_per_point(scale_factor);
1655 }
1656}
1657
1658pub fn begin_pass_system(
1660 mut contexts: Query<
1661 (&mut EguiContext, &EguiContextSettings, &mut EguiInput),
1662 Without<EguiMultipassSchedule>,
1663 >,
1664) {
1665 for (mut ctx, egui_settings, mut egui_input) in contexts.iter_mut() {
1666 if !egui_settings.run_manually {
1667 ctx.get_mut().begin_pass(egui_input.take());
1668 }
1669 }
1670}
1671
1672pub fn end_pass_system(
1674 mut contexts: Query<
1675 (&mut EguiContext, &EguiContextSettings, &mut EguiFullOutput),
1676 Without<EguiMultipassSchedule>,
1677 >,
1678) {
1679 for (mut ctx, egui_settings, mut full_output) in contexts.iter_mut() {
1680 if !egui_settings.run_manually {
1681 **full_output = Some(ctx.get_mut().end_pass());
1682 }
1683 }
1684}
1685
1686#[cfg(feature = "accesskit_placeholder")]
1688pub fn update_accessibility_system(
1689 requested: Res<bevy_a11y::AccessibilityRequested>,
1690 mut manage_accessibility_updates: ResMut<bevy_a11y::ManageAccessibilityUpdates>,
1691 outputs: Query<(Entity, &EguiOutput)>,
1692 mut adapters: NonSendMut<bevy_winit::accessibility::AccessKitAdapters>,
1693) {
1694 if requested.get() {
1695 for (entity, output) in &outputs {
1696 if let Some(adapter) = adapters.get_mut(&entity) {
1697 if let Some(update) = &output.platform_output.accesskit_update {
1698 **manage_accessibility_updates = false;
1699 adapter.update_if_active(|| update.clone());
1700 } else if !**manage_accessibility_updates {
1701 **manage_accessibility_updates = true;
1702 }
1703 }
1704 }
1705 }
1706}
1707
1708#[derive(QueryData)]
1709#[query_data(mutable)]
1710#[allow(missing_docs)]
1711pub struct MultiPassEguiQuery {
1712 entity: Entity,
1713 context: &'static mut EguiContext,
1714 input: &'static mut EguiInput,
1715 output: &'static mut EguiFullOutput,
1716 multipass_schedule: &'static EguiMultipassSchedule,
1717 settings: &'static EguiContextSettings,
1718}
1719
1720pub fn run_egui_context_pass_loop_system(world: &mut World) {
1723 let mut contexts_query = world.query::<MultiPassEguiQuery>();
1724 let mut used_schedules = HashSet::<InternedScheduleLabel>::default();
1725
1726 let mut multipass_contexts: Vec<_> = contexts_query
1727 .iter_mut(world)
1728 .filter_map(|mut egui_context| {
1729 if egui_context.settings.run_manually {
1730 return None;
1731 }
1732
1733 Some((
1734 egui_context.entity,
1735 egui_context.context.get_mut().clone(),
1736 egui_context.input.take(),
1737 egui_context.multipass_schedule.clone(),
1738 ))
1739 })
1740 .collect();
1741
1742 for (entity, ctx, ref mut input, EguiMultipassSchedule(multipass_schedule)) in
1743 &mut multipass_contexts
1744 {
1745 if !used_schedules.insert(*multipass_schedule) {
1746 panic!("Each Egui context running in the multi-pass mode must have a unique schedule (attempted to reuse schedule {multipass_schedule:?})");
1747 }
1748
1749 let output = ctx.run(input.take(), |_| {
1750 let _ = world.try_run_schedule(*multipass_schedule);
1751 });
1752
1753 **contexts_query
1754 .get_mut(world, *entity)
1755 .expect("previously queried context")
1756 .output = Some(output);
1757 }
1758
1759 if world
1763 .query_filtered::<Entity, (With<EguiContext>, With<PrimaryEguiContext>)>()
1764 .iter(world)
1765 .next()
1766 .is_none()
1767 {
1768 return;
1771 }
1772 if !used_schedules.contains(&ScheduleLabel::intern(&EguiPrimaryContextPass)) {
1773 let _ = world.try_run_schedule(EguiPrimaryContextPass);
1774 }
1775}
1776
1777#[cfg(feature = "picking")]
1779pub trait BevyEguiEntityCommandsExt {
1780 fn add_picking_observers_for_context(&mut self, context: Entity) -> &mut Self;
1782}
1783
1784#[cfg(feature = "picking")]
1785impl<'a> BevyEguiEntityCommandsExt for EntityCommands<'a> {
1786 fn add_picking_observers_for_context(&mut self, context: Entity) -> &mut Self {
1787 self.insert(picking::PickableEguiContext(context))
1788 .observe(picking::handle_over_system)
1789 .observe(picking::handle_out_system)
1790 .observe(picking::handle_move_system)
1791 }
1792}
1793
1794#[cfg(test)]
1795mod tests {
1796 #[test]
1797 fn test_readme_deps() {
1798 version_sync::assert_markdown_deps_updated!("README.md");
1799 }
1800}