1#![warn(missing_docs)]
2#![allow(clippy::type_complexity)]
3
4pub mod helpers;
151pub mod input;
153pub mod output;
155#[cfg(feature = "picking")]
157pub mod picking;
158#[cfg(feature = "render")]
160pub mod render;
161#[cfg(target_arch = "wasm32")]
163pub mod text_agent;
164#[cfg(all(feature = "manage_clipboard", target_arch = "wasm32",))]
166pub mod web_clipboard;
167
168pub use egui;
169
170use crate::input::*;
171#[cfg(target_arch = "wasm32")]
172use crate::text_agent::{
173 install_text_agent_system, is_mobile_safari, process_safari_virtual_keyboard_system,
174 write_text_agent_channel_events_system, SafariVirtualKeyboardTouchState, TextAgentChannel,
175 VirtualTouchInfo,
176};
177#[cfg(all(
178 feature = "manage_clipboard",
179 not(any(target_arch = "wasm32", target_os = "android"))
180))]
181use arboard::Clipboard;
182use bevy_app::prelude::*;
183#[cfg(feature = "render")]
184use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle};
185use bevy_derive::{Deref, DerefMut};
186use bevy_ecs::{
187 prelude::*,
188 query::{QueryData, QueryEntityError, QuerySingleError},
189 schedule::{InternedScheduleLabel, ScheduleLabel},
190 system::SystemParam,
191};
192#[cfg(feature = "render")]
193use bevy_image::{Image, ImageSampler};
194use bevy_input::InputSystem;
195#[allow(unused_imports)]
196use bevy_log as log;
197#[cfg(feature = "picking")]
198use bevy_picking::{
199 backend::{HitData, PointerHits},
200 pointer::{PointerId, PointerLocation},
201};
202#[cfg(feature = "render")]
203use bevy_platform::collections::HashMap;
204use bevy_platform::collections::HashSet;
205use bevy_reflect::Reflect;
206#[cfg(feature = "picking")]
207use bevy_render::camera::NormalizedRenderTarget;
208#[cfg(feature = "render")]
209use bevy_render::{
210 extract_resource::{ExtractResource, ExtractResourcePlugin},
211 render_resource::SpecializedRenderPipelines,
212 ExtractSchedule, Render, RenderApp, RenderSet,
213};
214use bevy_winit::cursor::CursorIcon;
215use output::process_output_system;
216#[cfg(all(
217 feature = "manage_clipboard",
218 not(any(target_arch = "wasm32", target_os = "android"))
219))]
220use std::cell::{RefCell, RefMut};
221#[cfg(target_arch = "wasm32")]
222use wasm_bindgen::prelude::*;
223
224pub struct EguiPlugin {
226 #[deprecated(
353 note = "The option to disable the multi-pass mode is now deprecated, use `EguiPlugin::default` instead"
354 )]
355 pub enable_multipass_for_primary_context: bool,
356
357 #[cfg(feature = "bevy_ui")]
362 pub ui_render_order: UiRenderOrder,
363}
364
365impl Default for EguiPlugin {
366 fn default() -> Self {
367 Self {
368 #[allow(deprecated)]
369 enable_multipass_for_primary_context: true,
370 #[cfg(feature = "bevy_ui")]
371 ui_render_order: UiRenderOrder::EguiAboveBevyUi,
372 }
373 }
374}
375
376#[cfg(feature = "bevy_ui")]
380#[derive(Debug, Clone, Copy, PartialEq, Eq)]
381pub enum UiRenderOrder {
382 EguiAboveBevyUi,
384 BevyUiAboveEgui,
386}
387
388#[derive(Clone, Debug, Resource, Reflect)]
390pub struct EguiGlobalSettings {
391 pub auto_create_primary_context: bool,
395 pub enable_focused_non_window_context_updates: bool,
400 pub input_system_settings: EguiInputSystemSettings,
402 pub enable_absorb_bevy_input_system: bool,
416 pub enable_cursor_icon_updates: bool,
421}
422
423impl Default for EguiGlobalSettings {
424 fn default() -> Self {
425 Self {
426 auto_create_primary_context: true,
427 enable_focused_non_window_context_updates: true,
428 input_system_settings: EguiInputSystemSettings::default(),
429 enable_absorb_bevy_input_system: false,
430 enable_cursor_icon_updates: true,
431 }
432 }
433}
434
435#[derive(Resource)]
437pub struct EnableMultipassForPrimaryContext;
438
439#[derive(Clone, Debug, Component, Reflect)]
441pub struct EguiContextSettings {
442 pub run_manually: bool,
444 pub scale_factor: f32,
458 #[cfg(feature = "open_url")]
461 pub default_open_url_target: Option<String>,
462 #[cfg(feature = "picking")]
464 pub capture_pointer_input: bool,
465 pub input_system_settings: EguiInputSystemSettings,
467 pub enable_cursor_icon_updates: bool,
472}
473
474impl PartialEq for EguiContextSettings {
476 #[allow(clippy::let_and_return)]
477 fn eq(&self, other: &Self) -> bool {
478 let eq = self.scale_factor == other.scale_factor;
479 #[cfg(feature = "open_url")]
480 let eq = eq && self.default_open_url_target == other.default_open_url_target;
481 eq
482 }
483}
484
485impl Default for EguiContextSettings {
486 fn default() -> Self {
487 Self {
488 run_manually: false,
489 scale_factor: 1.0,
490 #[cfg(feature = "open_url")]
491 default_open_url_target: None,
492 #[cfg(feature = "picking")]
493 capture_pointer_input: true,
494 input_system_settings: EguiInputSystemSettings::default(),
495 enable_cursor_icon_updates: true,
496 }
497 }
498}
499
500#[derive(Clone, Debug, Reflect, PartialEq, Eq)]
501pub struct EguiInputSystemSettings {
503 pub run_write_modifiers_keys_state_system: bool,
505 pub run_write_window_pointer_moved_events_system: bool,
507 pub run_write_pointer_button_events_system: bool,
509 pub run_write_window_touch_events_system: bool,
511 pub run_write_non_window_pointer_moved_events_system: bool,
513 pub run_write_mouse_wheel_events_system: bool,
515 pub run_write_non_window_touch_events_system: bool,
517 pub run_write_keyboard_input_events_system: bool,
519 pub run_write_ime_events_system: bool,
521 pub run_write_file_dnd_events_system: bool,
523 #[cfg(target_arch = "wasm32")]
525 pub run_write_text_agent_channel_events_system: bool,
526 #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
528 pub run_write_web_clipboard_events_system: bool,
529}
530
531impl Default for EguiInputSystemSettings {
532 fn default() -> Self {
533 Self {
534 run_write_modifiers_keys_state_system: true,
535 run_write_window_pointer_moved_events_system: true,
536 run_write_pointer_button_events_system: true,
537 run_write_window_touch_events_system: true,
538 run_write_non_window_pointer_moved_events_system: true,
539 run_write_mouse_wheel_events_system: true,
540 run_write_non_window_touch_events_system: true,
541 run_write_keyboard_input_events_system: true,
542 run_write_ime_events_system: true,
543 run_write_file_dnd_events_system: true,
544 #[cfg(target_arch = "wasm32")]
545 run_write_text_agent_channel_events_system: true,
546 #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
547 run_write_web_clipboard_events_system: true,
548 }
549 }
550}
551
552#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
555pub struct EguiPrimaryContextPass;
556
557#[derive(Component, Clone)]
559#[require(EguiMultipassSchedule::new(EguiPrimaryContextPass))]
560pub struct PrimaryEguiContext;
561
562#[derive(Component, Clone)]
565#[require(EguiContext)]
566pub struct EguiMultipassSchedule(pub InternedScheduleLabel);
567
568impl EguiMultipassSchedule {
569 pub fn new(schedule: impl ScheduleLabel) -> Self {
571 Self(schedule.intern())
572 }
573}
574
575#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
579pub struct EguiInput(pub egui::RawInput);
580
581#[derive(Component, Clone, Default, Deref, DerefMut)]
583pub struct EguiFullOutput(pub Option<egui::FullOutput>);
584
585#[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
589#[derive(Default, Resource)]
590pub struct EguiClipboard {
591 #[cfg(not(target_arch = "wasm32"))]
592 clipboard: thread_local::ThreadLocal<Option<RefCell<Clipboard>>>,
593 #[cfg(target_arch = "wasm32")]
594 clipboard: web_clipboard::WebClipboard,
595}
596
597#[derive(Component, Clone, Default, Debug)]
599pub struct EguiRenderOutput {
600 pub paint_jobs: Vec<egui::ClippedPrimitive>,
605 pub textures_delta: egui::TexturesDelta,
607}
608
609impl EguiRenderOutput {
610 pub fn is_empty(&self) -> bool {
612 self.paint_jobs.is_empty() && self.textures_delta.is_empty()
613 }
614}
615
616#[derive(Component, Clone, Default)]
618pub struct EguiOutput {
619 pub platform_output: egui::PlatformOutput,
621}
622
623#[derive(Clone, Component, Default)]
625#[require(
626 EguiContextSettings,
627 EguiInput,
628 EguiContextPointerPosition,
629 EguiContextPointerTouchId,
630 EguiContextImeState,
631 EguiFullOutput,
632 EguiRenderOutput,
633 EguiOutput,
634 CursorIcon
635)]
636pub struct EguiContext {
637 ctx: egui::Context,
638}
639
640impl EguiContext {
641 #[cfg(feature = "immutable_ctx")]
651 #[must_use]
652 pub fn get(&self) -> &egui::Context {
653 &self.ctx
654 }
655
656 #[must_use]
666 pub fn get_mut(&mut self) -> &mut egui::Context {
667 &mut self.ctx
668 }
669}
670
671type EguiContextsPrimaryQuery<'w, 's> =
673 Query<'w, 's, &'static mut EguiContext, With<PrimaryEguiContext>>;
674
675type EguiContextsQuery<'w, 's> = Query<
676 'w,
677 's,
678 (
679 &'static mut EguiContext,
680 Option<&'static PrimaryEguiContext>,
681 ),
682>;
683
684#[derive(SystemParam)]
685pub struct EguiContexts<'w, 's> {
688 q: EguiContextsQuery<'w, 's>,
689 #[cfg(feature = "render")]
690 user_textures: ResMut<'w, EguiUserTextures>,
691}
692
693#[allow(clippy::manual_try_fold)]
694impl EguiContexts<'_, '_> {
695 #[inline]
697 pub fn ctx_mut(&mut self) -> Result<&mut egui::Context, QuerySingleError> {
698 self.q.iter_mut().fold(
699 Err(QuerySingleError::NoEntities(core::any::type_name::<
700 EguiContextsPrimaryQuery,
701 >())),
702 |result, (ctx, primary)| match (&result, primary) {
703 (Err(QuerySingleError::MultipleEntities(_)), _) => result,
704 (Err(QuerySingleError::NoEntities(_)), Some(_)) => Ok(ctx.into_inner().get_mut()),
705 (Err(QuerySingleError::NoEntities(_)), None) => result,
706 (Ok(_), Some(_)) => {
707 Err(QuerySingleError::MultipleEntities(core::any::type_name::<
708 EguiContextsPrimaryQuery,
709 >()))
710 }
711 (Ok(_), None) => result,
712 },
713 )
714 }
715
716 #[inline]
718 pub fn ctx_for_entity_mut(
719 &mut self,
720 entity: Entity,
721 ) -> Result<&mut egui::Context, QueryEntityError> {
722 self.q
723 .get_mut(entity)
724 .map(|(context, _primary)| context.into_inner().get_mut())
725 }
726
727 #[inline]
730 pub fn ctx_for_entities_mut<const N: usize>(
731 &mut self,
732 ids: [Entity; N],
733 ) -> Result<[&mut egui::Context; N], QueryEntityError> {
734 self.q
735 .get_many_mut(ids)
736 .map(|arr| arr.map(|(ctx, _primary_window)| ctx.into_inner().get_mut()))
737 }
738
739 #[cfg(feature = "immutable_ctx")]
749 #[inline]
750 pub fn ctx(&self) -> Result<&egui::Context, QuerySingleError> {
751 self.q.iter().fold(
752 Err(QuerySingleError::NoEntities(core::any::type_name::<
753 EguiContextsPrimaryQuery,
754 >())),
755 |result, (ctx, primary)| match (&result, primary) {
756 (Err(QuerySingleError::MultipleEntities(_)), _) => result,
757 (Err(QuerySingleError::NoEntities(_)), Some(_)) => Ok(ctx.get()),
758 (Err(QuerySingleError::NoEntities(_)), None) => result,
759 (Ok(_), Some(_)) => {
760 Err(QuerySingleError::MultipleEntities(core::any::type_name::<
761 EguiContextsPrimaryQuery,
762 >()))
763 }
764 (Ok(_), None) => result,
765 },
766 )
767 }
768
769 #[inline]
779 #[cfg(feature = "immutable_ctx")]
780 pub fn ctx_for_entity(&self, entity: Entity) -> Result<&egui::Context, QueryEntityError> {
781 self.q.get(entity).map(|(context, _primary)| context.get())
782 }
783
784 #[cfg(feature = "render")]
793 pub fn add_image(&mut self, image: Handle<Image>) -> egui::TextureId {
794 self.user_textures.add_image(image)
795 }
796
797 #[cfg(feature = "render")]
799 #[track_caller]
800 pub fn remove_image(&mut self, image: &Handle<Image>) -> Option<egui::TextureId> {
801 self.user_textures.remove_image(image)
802 }
803
804 #[cfg(feature = "render")]
806 #[must_use]
807 #[track_caller]
808 pub fn image_id(&self, image: &Handle<Image>) -> Option<egui::TextureId> {
809 self.user_textures.image_id(image)
810 }
811}
812
813#[derive(Clone, Resource, ExtractResource)]
815#[cfg(feature = "render")]
816pub struct EguiUserTextures {
817 textures: HashMap<Handle<Image>, u64>,
818 free_list: Vec<u64>,
819}
820
821#[cfg(feature = "render")]
822impl Default for EguiUserTextures {
823 fn default() -> Self {
824 Self {
825 textures: HashMap::default(),
826 free_list: vec![0],
827 }
828 }
829}
830
831#[cfg(feature = "render")]
832impl EguiUserTextures {
833 pub fn add_image(&mut self, image: Handle<Image>) -> egui::TextureId {
842 let id = *self.textures.entry(image.clone()).or_insert_with(|| {
843 let id = self
844 .free_list
845 .pop()
846 .expect("free list must contain at least 1 element");
847 log::debug!("Add a new image (id: {}, handle: {:?})", id, image);
848 if self.free_list.is_empty() {
849 self.free_list.push(id.checked_add(1).expect("out of ids"));
850 }
851 id
852 });
853 egui::TextureId::User(id)
854 }
855
856 pub fn remove_image(&mut self, image: &Handle<Image>) -> Option<egui::TextureId> {
858 let id = self.textures.remove(image);
859 log::debug!("Remove image (id: {:?}, handle: {:?})", id, image);
860 if let Some(id) = id {
861 self.free_list.push(id);
862 }
863 id.map(egui::TextureId::User)
864 }
865
866 #[must_use]
868 pub fn image_id(&self, image: &Handle<Image>) -> Option<egui::TextureId> {
869 self.textures
870 .get(image)
871 .map(|&id| egui::TextureId::User(id))
872 }
873}
874
875#[derive(Component, Debug, Default, Clone, Copy, PartialEq)]
878pub struct RenderComputedScaleFactor {
879 pub scale_factor: f32,
881}
882
883pub mod node {
885 pub const EGUI_PASS: &str = "egui_pass";
887}
888
889#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
890pub enum EguiStartupSet {
892 InitContexts,
894}
895
896#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
898pub enum EguiPreUpdateSet {
899 InitContexts,
901 ProcessInput,
907 BeginPass,
909}
910
911#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
913pub enum EguiInputSet {
914 InitReading,
918 FocusContext,
920 ReadBevyEvents,
922 WriteEguiEvents,
924}
925
926#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
928pub enum EguiPostUpdateSet {
929 EndPass,
931 ProcessOutput,
933 PostProcessOutput,
935}
936
937impl Plugin for EguiPlugin {
938 fn build(&self, app: &mut App) {
939 app.register_type::<EguiGlobalSettings>();
940 app.register_type::<EguiContextSettings>();
941 app.init_resource::<EguiGlobalSettings>();
942 app.init_resource::<ModifierKeysState>();
943 app.init_resource::<EguiWantsInput>();
944 app.init_resource::<WindowToEguiContextMap>();
945 app.add_event::<EguiInputEvent>();
946 app.add_event::<EguiFileDragAndDropEvent>();
947
948 #[allow(deprecated)]
949 if self.enable_multipass_for_primary_context {
950 app.insert_resource(EnableMultipassForPrimaryContext);
951 }
952
953 #[cfg(feature = "render")]
954 {
955 app.init_resource::<EguiManagedTextures>();
956 app.init_resource::<EguiUserTextures>();
957 app.add_plugins(ExtractResourcePlugin::<EguiUserTextures>::default());
958 app.add_plugins(ExtractResourcePlugin::<
959 render::systems::ExtractedEguiManagedTextures,
960 >::default());
961 }
962
963 #[cfg(target_arch = "wasm32")]
964 app.init_non_send_resource::<SubscribedEvents>();
965
966 #[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
967 app.init_resource::<EguiClipboard>();
968
969 app.configure_sets(
970 PreUpdate,
971 (
972 EguiPreUpdateSet::InitContexts,
973 EguiPreUpdateSet::ProcessInput.after(InputSystem),
974 EguiPreUpdateSet::BeginPass,
975 )
976 .chain(),
977 );
978 app.configure_sets(
979 PreUpdate,
980 (
981 EguiInputSet::InitReading,
982 EguiInputSet::FocusContext,
983 EguiInputSet::ReadBevyEvents,
984 EguiInputSet::WriteEguiEvents,
985 )
986 .chain(),
987 );
988 #[cfg(not(feature = "accesskit_placeholder"))]
989 app.configure_sets(
990 PostUpdate,
991 (
992 EguiPostUpdateSet::EndPass,
993 EguiPostUpdateSet::ProcessOutput,
994 EguiPostUpdateSet::PostProcessOutput,
995 )
996 .chain(),
997 );
998 #[cfg(feature = "accesskit_placeholder")]
999 app.configure_sets(
1000 PostUpdate,
1001 (
1002 EguiPostUpdateSet::EndPass,
1003 EguiPostUpdateSet::ProcessOutput,
1004 EguiPostUpdateSet::PostProcessOutput.before(bevy_a11y::AccessibilitySystem::Update),
1005 )
1006 .chain(),
1007 );
1008
1009 #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
1011 {
1012 app.add_systems(PreStartup, web_clipboard::startup_setup_web_events_system);
1013 }
1014 #[cfg(feature = "render")]
1015 app.add_systems(
1016 PreStartup,
1017 (
1018 (setup_primary_egui_context_system, ApplyDeferred)
1019 .run_if(|s: Res<EguiGlobalSettings>| s.auto_create_primary_context),
1020 update_ui_size_and_scale_system,
1021 )
1022 .chain()
1023 .in_set(EguiStartupSet::InitContexts),
1024 );
1025
1026 #[cfg(feature = "render")]
1028 app.add_systems(
1029 PreUpdate,
1030 (
1031 setup_primary_egui_context_system
1032 .run_if(|s: Res<EguiGlobalSettings>| s.auto_create_primary_context),
1033 WindowToEguiContextMap::on_egui_context_added_system,
1034 WindowToEguiContextMap::on_egui_context_removed_system,
1035 ApplyDeferred,
1036 update_ui_size_and_scale_system,
1037 )
1038 .chain()
1039 .in_set(EguiPreUpdateSet::InitContexts),
1040 );
1041 app.add_systems(
1042 PreUpdate,
1043 (
1044 (
1045 write_modifiers_keys_state_system.run_if(input_system_is_enabled(|s| {
1046 s.run_write_modifiers_keys_state_system
1047 })),
1048 write_window_pointer_moved_events_system.run_if(input_system_is_enabled(|s| {
1049 s.run_write_window_pointer_moved_events_system
1050 })),
1051 )
1052 .in_set(EguiInputSet::InitReading),
1053 (
1054 write_pointer_button_events_system.run_if(input_system_is_enabled(|s| {
1055 s.run_write_pointer_button_events_system
1056 })),
1057 write_window_touch_events_system.run_if(input_system_is_enabled(|s| {
1058 s.run_write_window_touch_events_system
1059 })),
1060 )
1061 .in_set(EguiInputSet::FocusContext),
1062 (
1063 write_non_window_pointer_moved_events_system.run_if(input_system_is_enabled(
1064 |s| s.run_write_non_window_pointer_moved_events_system,
1065 )),
1066 write_non_window_touch_events_system.run_if(input_system_is_enabled(|s| {
1067 s.run_write_non_window_touch_events_system
1068 })),
1069 write_mouse_wheel_events_system.run_if(input_system_is_enabled(|s| {
1070 s.run_write_mouse_wheel_events_system
1071 })),
1072 write_keyboard_input_events_system.run_if(input_system_is_enabled(|s| {
1073 s.run_write_keyboard_input_events_system
1074 })),
1075 write_ime_events_system
1076 .run_if(input_system_is_enabled(|s| s.run_write_ime_events_system)),
1077 write_file_dnd_events_system.run_if(input_system_is_enabled(|s| {
1078 s.run_write_file_dnd_events_system
1079 })),
1080 )
1081 .in_set(EguiInputSet::ReadBevyEvents),
1082 (
1083 write_egui_input_system,
1084 absorb_bevy_input_system.run_if(|settings: Res<EguiGlobalSettings>| {
1085 settings.enable_absorb_bevy_input_system
1086 }),
1087 )
1088 .in_set(EguiInputSet::WriteEguiEvents),
1089 )
1090 .chain()
1091 .in_set(EguiPreUpdateSet::ProcessInput),
1092 );
1093 app.add_systems(
1094 PreUpdate,
1095 begin_pass_system.in_set(EguiPreUpdateSet::BeginPass),
1096 );
1097
1098 #[cfg(target_arch = "wasm32")]
1100 {
1101 use std::sync::{LazyLock, Mutex};
1102
1103 let maybe_window_plugin = app.get_added_plugins::<bevy_window::WindowPlugin>();
1104
1105 if !maybe_window_plugin.is_empty()
1106 && maybe_window_plugin[0].primary_window.is_some()
1107 && maybe_window_plugin[0]
1108 .primary_window
1109 .as_ref()
1110 .unwrap()
1111 .prevent_default_event_handling
1112 {
1113 app.init_resource::<TextAgentChannel>();
1114
1115 let (sender, receiver) = crossbeam_channel::unbounded();
1116 static TOUCH_INFO: LazyLock<Mutex<VirtualTouchInfo>> =
1117 LazyLock::new(|| Mutex::new(VirtualTouchInfo::default()));
1118
1119 app.insert_resource(SafariVirtualKeyboardTouchState {
1120 sender,
1121 receiver,
1122 touch_info: &TOUCH_INFO,
1123 });
1124
1125 app.add_systems(
1126 PreStartup,
1127 install_text_agent_system.in_set(EguiStartupSet::InitContexts),
1128 );
1129
1130 app.add_systems(
1131 PreUpdate,
1132 write_text_agent_channel_events_system
1133 .run_if(input_system_is_enabled(|s| {
1134 s.run_write_text_agent_channel_events_system
1135 }))
1136 .in_set(EguiPreUpdateSet::ProcessInput)
1137 .in_set(EguiInputSet::ReadBevyEvents),
1138 );
1139
1140 if is_mobile_safari() {
1141 app.add_systems(
1142 PostUpdate,
1143 process_safari_virtual_keyboard_system
1144 .in_set(EguiPostUpdateSet::PostProcessOutput),
1145 );
1146 }
1147 }
1148
1149 #[cfg(feature = "manage_clipboard")]
1150 app.add_systems(
1151 PreUpdate,
1152 web_clipboard::write_web_clipboard_events_system
1153 .run_if(input_system_is_enabled(|s| {
1154 s.run_write_web_clipboard_events_system
1155 }))
1156 .in_set(EguiPreUpdateSet::ProcessInput)
1157 .in_set(EguiInputSet::ReadBevyEvents),
1158 );
1159 }
1160
1161 app.add_systems(
1163 PostUpdate,
1164 (run_egui_context_pass_loop_system, end_pass_system)
1165 .chain()
1166 .in_set(EguiPostUpdateSet::EndPass),
1167 );
1168 app.add_systems(
1169 PostUpdate,
1170 (
1171 process_output_system,
1172 write_egui_wants_input_system,
1173 #[cfg(any(target_os = "ios", target_os = "android"))]
1174 set_ime_allowed_system,
1176 )
1177 .in_set(EguiPostUpdateSet::ProcessOutput),
1178 );
1179 #[cfg(feature = "picking")]
1180 if app.is_plugin_added::<bevy_picking::PickingPlugin>() {
1181 app.add_systems(PostUpdate, capture_pointer_input_system);
1182 } else {
1183 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)");
1184 }
1185
1186 #[cfg(feature = "render")]
1187 app.add_systems(
1188 PostUpdate,
1189 update_egui_textures_system.in_set(EguiPostUpdateSet::PostProcessOutput),
1190 )
1191 .add_systems(
1192 Render,
1193 render::systems::prepare_egui_transforms_system.in_set(RenderSet::Prepare),
1194 )
1195 .add_systems(
1196 Render,
1197 render::systems::queue_bind_groups_system.in_set(RenderSet::Queue),
1198 )
1199 .add_systems(
1200 Render,
1201 render::systems::queue_pipelines_system.in_set(RenderSet::Queue),
1202 )
1203 .add_systems(Last, free_egui_textures_system);
1204
1205 #[cfg(feature = "render")]
1206 {
1207 load_internal_asset!(
1208 app,
1209 render::EGUI_SHADER_HANDLE,
1210 "render/egui.wgsl",
1211 bevy_render::render_resource::Shader::from_wgsl
1212 );
1213
1214 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
1215 return;
1216 };
1217
1218 let egui_graph_2d = render::get_egui_graph(render_app);
1219 let egui_graph_3d = render::get_egui_graph(render_app);
1220 let mut graph = render_app
1221 .world_mut()
1222 .resource_mut::<bevy_render::render_graph::RenderGraph>();
1223
1224 if let Some(graph_2d) =
1225 graph.get_sub_graph_mut(bevy_core_pipeline::core_2d::graph::Core2d)
1226 {
1227 graph_2d.add_sub_graph(render::graph::SubGraphEgui, egui_graph_2d);
1228 graph_2d.add_node(
1229 render::graph::NodeEgui::EguiPass,
1230 render::RunEguiSubgraphOnEguiViewNode,
1231 );
1232 graph_2d.add_node_edge(
1233 bevy_core_pipeline::core_2d::graph::Node2d::EndMainPass,
1234 render::graph::NodeEgui::EguiPass,
1235 );
1236 graph_2d.add_node_edge(
1237 bevy_core_pipeline::core_2d::graph::Node2d::EndMainPassPostProcessing,
1238 render::graph::NodeEgui::EguiPass,
1239 );
1240 graph_2d.add_node_edge(
1241 render::graph::NodeEgui::EguiPass,
1242 bevy_core_pipeline::core_2d::graph::Node2d::Upscaling,
1243 );
1244 }
1245
1246 if let Some(graph_3d) =
1247 graph.get_sub_graph_mut(bevy_core_pipeline::core_3d::graph::Core3d)
1248 {
1249 graph_3d.add_sub_graph(render::graph::SubGraphEgui, egui_graph_3d);
1250 graph_3d.add_node(
1251 render::graph::NodeEgui::EguiPass,
1252 render::RunEguiSubgraphOnEguiViewNode,
1253 );
1254 graph_3d.add_node_edge(
1255 bevy_core_pipeline::core_3d::graph::Node3d::EndMainPass,
1256 render::graph::NodeEgui::EguiPass,
1257 );
1258 graph_3d.add_node_edge(
1259 bevy_core_pipeline::core_3d::graph::Node3d::EndMainPassPostProcessing,
1260 render::graph::NodeEgui::EguiPass,
1261 );
1262 graph_3d.add_node_edge(
1263 render::graph::NodeEgui::EguiPass,
1264 bevy_core_pipeline::core_3d::graph::Node3d::Upscaling,
1265 );
1266 }
1267 }
1268
1269 #[cfg(feature = "accesskit_placeholder")]
1270 app.add_systems(
1271 PostUpdate,
1272 update_accessibility_system.in_set(EguiPostUpdateSet::PostProcessOutput),
1273 );
1274 }
1275
1276 #[cfg(feature = "render")]
1277 fn finish(&self, app: &mut App) {
1278 #[cfg(feature = "bevy_ui")]
1279 let bevy_ui_is_enabled = app.is_plugin_added::<bevy_ui::UiPlugin>();
1280
1281 if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
1282 render_app
1283 .init_resource::<render::EguiPipeline>()
1284 .init_resource::<SpecializedRenderPipelines<render::EguiPipeline>>()
1285 .init_resource::<render::systems::EguiTransforms>()
1286 .init_resource::<render::systems::EguiRenderData>()
1287 .add_systems(
1288 ExtractSchedule,
1291 render::extract_egui_camera_view_system,
1292 )
1293 .add_systems(
1294 Render,
1295 render::systems::prepare_egui_transforms_system.in_set(RenderSet::Prepare),
1296 )
1297 .add_systems(
1298 Render,
1299 render::systems::prepare_egui_render_target_data_system
1300 .in_set(RenderSet::Prepare),
1301 )
1302 .add_systems(
1303 Render,
1304 render::systems::queue_bind_groups_system.in_set(RenderSet::Queue),
1305 )
1306 .add_systems(
1307 Render,
1308 render::systems::queue_pipelines_system.in_set(RenderSet::Queue),
1309 );
1310
1311 #[cfg(feature = "bevy_ui")]
1314 if bevy_ui_is_enabled {
1315 use bevy_render::render_graph::RenderLabel;
1316 let mut graph = render_app
1317 .world_mut()
1318 .resource_mut::<bevy_render::render_graph::RenderGraph>();
1319 let (below, above) = match self.ui_render_order {
1320 UiRenderOrder::EguiAboveBevyUi => (
1321 bevy_ui::graph::NodeUi::UiPass.intern(),
1322 render::graph::NodeEgui::EguiPass.intern(),
1323 ),
1324 UiRenderOrder::BevyUiAboveEgui => (
1325 render::graph::NodeEgui::EguiPass.intern(),
1326 bevy_ui::graph::NodeUi::UiPass.intern(),
1327 ),
1328 };
1329 if let Some(graph_2d) =
1330 graph.get_sub_graph_mut(bevy_core_pipeline::core_2d::graph::Core2d)
1331 {
1332 match graph_2d.get_node_state(bevy_ui::graph::NodeUi::UiPass) {
1337 Ok(_) => {
1338 graph_2d.add_node_edge(below, above);
1339 }
1340 Err(err) => log::warn!(
1341 error = &err as &dyn std::error::Error,
1342 "bevy_ui::UiPlugin is enabled but could not be found in 2D render graph, rendering order will be inconsistent",
1343 ),
1344 }
1345 }
1346 if let Some(graph_3d) =
1347 graph.get_sub_graph_mut(bevy_core_pipeline::core_3d::graph::Core3d)
1348 {
1349 match graph_3d.get_node_state(bevy_ui::graph::NodeUi::UiPass) {
1350 Ok(_) => {
1351 graph_3d.add_node_edge(below, above);
1352 }
1353 Err(err) => log::warn!(
1354 error = &err as &dyn std::error::Error,
1355 "bevy_ui::UiPlugin is enabled but could not be found in 3D render graph, rendering order will be inconsistent",
1356 ),
1357 }
1358 }
1359 } else {
1360 log::debug!("bevy_ui feature is enabled, but bevy_ui::UiPlugin is disabled, not applying configured rendering order")
1361 }
1362 }
1363 }
1364}
1365
1366fn input_system_is_enabled(
1367 test: impl Fn(&EguiInputSystemSettings) -> bool,
1368) -> impl Fn(Res<EguiGlobalSettings>) -> bool {
1369 move |settings| test(&settings.input_system_settings)
1370}
1371
1372#[cfg(feature = "render")]
1374#[derive(Resource, Deref, DerefMut, Default)]
1375pub struct EguiManagedTextures(pub HashMap<(Entity, u64), EguiManagedTexture>);
1376
1377#[cfg(feature = "render")]
1379pub struct EguiManagedTexture {
1380 pub handle: Handle<Image>,
1382 pub color_image: egui::ColorImage,
1384}
1385
1386#[cfg(feature = "render")]
1391pub fn setup_primary_egui_context_system(
1392 mut commands: Commands,
1393 new_cameras: Query<(Entity, Option<&EguiContext>), Added<bevy_render::camera::Camera>>,
1394 #[cfg(feature = "accesskit_placeholder")] adapters: Option<
1395 NonSend<bevy_winit::accessibility::AccessKitAdapters>,
1396 >,
1397 #[cfg(feature = "accesskit_placeholder")] mut manage_accessibility_updates: ResMut<
1398 bevy_a11y::ManageAccessibilityUpdates,
1399 >,
1400 enable_multipass_for_primary_context: Option<Res<EnableMultipassForPrimaryContext>>,
1401 mut egui_context_exists: Local<bool>,
1402) -> Result {
1403 for (camera_entity, context) in new_cameras {
1404 if context.is_some() || *egui_context_exists {
1405 *egui_context_exists = true;
1406 return Ok(());
1407 }
1408
1409 let context = EguiContext::default();
1410 #[cfg(feature = "accesskit_placeholder")]
1411 if let Some(adapters) = &adapters {
1412 if adapters.get(&camera_entity).is_some() {
1414 context.ctx.enable_accesskit();
1415 **manage_accessibility_updates = false;
1416 }
1417 }
1418
1419 log::debug!("Creating a primary Egui context");
1420 let mut camera_commands = commands.get_entity(camera_entity)?;
1422 camera_commands.insert(context).insert(PrimaryEguiContext);
1423 if enable_multipass_for_primary_context.is_some() {
1424 camera_commands.insert(EguiMultipassSchedule::new(EguiPrimaryContextPass));
1425 }
1426 *egui_context_exists = true;
1427 }
1428
1429 Ok(())
1430}
1431
1432#[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
1433impl EguiClipboard {
1434 pub fn set_text(&mut self, contents: &str) {
1436 self.set_text_impl(contents);
1437 }
1438
1439 #[cfg(target_arch = "wasm32")]
1442 pub fn set_text_internal(&mut self, text: &str) {
1443 self.clipboard.set_text_internal(text);
1444 }
1445
1446 #[must_use]
1448 pub fn get_text(&mut self) -> Option<String> {
1449 self.get_text_impl()
1450 }
1451
1452 pub fn set_image(&mut self, image: &egui::ColorImage) {
1454 self.set_image_impl(image);
1455 }
1456
1457 #[cfg(target_arch = "wasm32")]
1459 pub fn try_receive_clipboard_event(&self) -> Option<web_clipboard::WebClipboardEvent> {
1460 self.clipboard.try_receive_clipboard_event()
1461 }
1462
1463 #[cfg(not(target_arch = "wasm32"))]
1464 fn set_text_impl(&mut self, contents: &str) {
1465 if let Some(mut clipboard) = self.get() {
1466 if let Err(err) = clipboard.set_text(contents.to_owned()) {
1467 log::error!("Failed to set clipboard contents: {:?}", err);
1468 }
1469 }
1470 }
1471
1472 #[cfg(target_arch = "wasm32")]
1473 fn set_text_impl(&mut self, contents: &str) {
1474 self.clipboard.set_text(contents);
1475 }
1476
1477 #[cfg(not(target_arch = "wasm32"))]
1478 fn get_text_impl(&mut self) -> Option<String> {
1479 if let Some(mut clipboard) = self.get() {
1480 match clipboard.get_text() {
1481 Ok(contents) => return Some(contents),
1482 Err(arboard::Error::ContentNotAvailable) => return Some("".to_string()),
1484 Err(err) => log::error!("Failed to get clipboard contents: {:?}", err),
1485 }
1486 };
1487 None
1488 }
1489
1490 #[cfg(target_arch = "wasm32")]
1491 #[allow(clippy::unnecessary_wraps)]
1492 fn get_text_impl(&mut self) -> Option<String> {
1493 self.clipboard.get_text()
1494 }
1495
1496 #[cfg(not(target_arch = "wasm32"))]
1497 fn set_image_impl(&mut self, image: &egui::ColorImage) {
1498 if let Some(mut clipboard) = self.get() {
1499 if let Err(err) = clipboard.set_image(arboard::ImageData {
1500 width: image.width(),
1501 height: image.height(),
1502 bytes: std::borrow::Cow::Borrowed(bytemuck::cast_slice(&image.pixels)),
1503 }) {
1504 log::error!("Failed to set clipboard contents: {:?}", err);
1505 }
1506 }
1507 }
1508
1509 #[cfg(target_arch = "wasm32")]
1510 fn set_image_impl(&mut self, image: &egui::ColorImage) {
1511 self.clipboard.set_image(image);
1512 }
1513
1514 #[cfg(not(target_arch = "wasm32"))]
1515 fn get(&self) -> Option<RefMut<'_, Clipboard>> {
1516 self.clipboard
1517 .get_or(|| {
1518 Clipboard::new()
1519 .map(RefCell::new)
1520 .map_err(|err| {
1521 log::error!("Failed to initialize clipboard: {:?}", err);
1522 })
1523 .ok()
1524 })
1525 .as_ref()
1526 .map(|cell| cell.borrow_mut())
1527 }
1528}
1529
1530#[cfg(feature = "picking")]
1532pub const PICKING_ORDER: f32 = 1_000_000.0;
1533
1534#[cfg(feature = "picking")]
1536pub fn capture_pointer_input_system(
1537 pointers: Query<(&PointerId, &PointerLocation)>,
1538 mut egui_context: Query<(
1539 Entity,
1540 &mut EguiContext,
1541 &EguiContextSettings,
1542 &bevy_render::camera::Camera,
1543 )>,
1544 mut output: EventWriter<PointerHits>,
1545 window_to_egui_context_map: Res<WindowToEguiContextMap>,
1546) {
1547 use helpers::QueryHelper;
1548
1549 for (pointer, location) in pointers
1550 .iter()
1551 .filter_map(|(i, p)| p.location.as_ref().map(|l| (i, l)))
1552 {
1553 if let NormalizedRenderTarget::Window(window) = location.target {
1554 for window_context_entity in window_to_egui_context_map
1555 .window_to_contexts
1556 .get(&window.entity())
1557 .cloned()
1558 .unwrap_or_default()
1559 {
1560 let Some((entity, mut ctx, settings, camera)) =
1561 egui_context.get_some_mut(window_context_entity)
1562 else {
1563 continue;
1564 };
1565 if !camera
1566 .physical_viewport_rect()
1567 .is_some_and(|rect| rect.as_rect().contains(location.position))
1568 {
1569 continue;
1570 }
1571
1572 if settings.capture_pointer_input && ctx.get_mut().wants_pointer_input() {
1573 let entry = (entity, HitData::new(entity, 0.0, None, None));
1574 output.write(PointerHits::new(
1575 *pointer,
1576 Vec::from([entry]),
1577 PICKING_ORDER,
1578 ));
1579 }
1580 }
1581 }
1582 }
1583}
1584
1585#[cfg(feature = "render")]
1587pub fn update_egui_textures_system(
1588 mut egui_render_output: Query<(Entity, &EguiRenderOutput)>,
1589 mut egui_managed_textures: ResMut<EguiManagedTextures>,
1590 mut image_assets: ResMut<Assets<Image>>,
1591) {
1592 for (entity, egui_render_output) in egui_render_output.iter_mut() {
1593 for (texture_id, image_delta) in &egui_render_output.textures_delta.set {
1594 let color_image = render::as_color_image(&image_delta.image);
1595
1596 let texture_id = match texture_id {
1597 egui::TextureId::Managed(texture_id) => *texture_id,
1598 egui::TextureId::User(_) => continue,
1599 };
1600
1601 let sampler = ImageSampler::Descriptor(render::texture_options_as_sampler_descriptor(
1602 &image_delta.options,
1603 ));
1604 if let Some(pos) = image_delta.pos {
1605 if let Some(managed_texture) = egui_managed_textures.get_mut(&(entity, texture_id))
1607 {
1608 update_image_rect(&mut managed_texture.color_image, pos, &color_image);
1610 let image =
1611 render::color_image_as_bevy_image(&managed_texture.color_image, sampler);
1612 managed_texture.handle = image_assets.add(image);
1613 } else {
1614 log::warn!("Partial update of a missing texture (id: {:?})", texture_id);
1615 }
1616 } else {
1617 let image = render::color_image_as_bevy_image(&color_image, sampler);
1619 let handle = image_assets.add(image);
1620 egui_managed_textures.insert(
1621 (entity, texture_id),
1622 EguiManagedTexture {
1623 handle,
1624 color_image,
1625 },
1626 );
1627 }
1628 }
1629 }
1630
1631 fn update_image_rect(dest: &mut egui::ColorImage, [x, y]: [usize; 2], src: &egui::ColorImage) {
1632 for sy in 0..src.height() {
1633 for sx in 0..src.width() {
1634 dest[(x + sx, y + sy)] = src[(sx, sy)];
1635 }
1636 }
1637 }
1638}
1639
1640#[cfg(feature = "render")]
1645pub fn free_egui_textures_system(
1646 mut egui_user_textures: ResMut<EguiUserTextures>,
1647 egui_render_output: Query<(Entity, &EguiRenderOutput)>,
1648 mut egui_managed_textures: ResMut<EguiManagedTextures>,
1649 mut image_assets: ResMut<Assets<Image>>,
1650 mut image_events: EventReader<AssetEvent<Image>>,
1651) {
1652 for (entity, egui_render_output) in egui_render_output.iter() {
1653 for &texture_id in &egui_render_output.textures_delta.free {
1654 if let egui::TextureId::Managed(texture_id) = texture_id {
1655 let managed_texture = egui_managed_textures.remove(&(entity, texture_id));
1656 if let Some(managed_texture) = managed_texture {
1657 image_assets.remove(&managed_texture.handle);
1658 }
1659 }
1660 }
1661 }
1662
1663 for image_event in image_events.read() {
1664 if let AssetEvent::Removed { id } = image_event {
1665 egui_user_textures.remove_image(&Handle::<Image>::Weak(*id));
1666 }
1667 }
1668}
1669
1670#[cfg(target_arch = "wasm32")]
1672pub fn string_from_js_value(value: &JsValue) -> String {
1673 value.as_string().unwrap_or_else(|| format!("{value:#?}"))
1674}
1675
1676#[cfg(target_arch = "wasm32")]
1677struct EventClosure<T> {
1678 target: web_sys::EventTarget,
1679 event_name: String,
1680 closure: wasm_bindgen::closure::Closure<dyn FnMut(T)>,
1681}
1682
1683#[cfg(target_arch = "wasm32")]
1685#[derive(Default)]
1686pub struct SubscribedEvents {
1687 #[cfg(feature = "manage_clipboard")]
1688 clipboard_event_closures: Vec<EventClosure<web_sys::ClipboardEvent>>,
1689 composition_event_closures: Vec<EventClosure<web_sys::CompositionEvent>>,
1690 keyboard_event_closures: Vec<EventClosure<web_sys::KeyboardEvent>>,
1691 input_event_closures: Vec<EventClosure<web_sys::InputEvent>>,
1692 touch_event_closures: Vec<EventClosure<web_sys::TouchEvent>>,
1693}
1694
1695#[cfg(target_arch = "wasm32")]
1696impl SubscribedEvents {
1697 pub fn unsubscribe_from_all_events(&mut self) {
1700 #[cfg(feature = "manage_clipboard")]
1701 Self::unsubscribe_from_events(&mut self.clipboard_event_closures);
1702 Self::unsubscribe_from_events(&mut self.composition_event_closures);
1703 Self::unsubscribe_from_events(&mut self.keyboard_event_closures);
1704 Self::unsubscribe_from_events(&mut self.input_event_closures);
1705 Self::unsubscribe_from_events(&mut self.touch_event_closures);
1706 }
1707
1708 fn unsubscribe_from_events<T>(events: &mut Vec<EventClosure<T>>) {
1709 let events_to_unsubscribe = std::mem::take(events);
1710
1711 if !events_to_unsubscribe.is_empty() {
1712 for event in events_to_unsubscribe {
1713 if let Err(err) = event.target.remove_event_listener_with_callback(
1714 event.event_name.as_str(),
1715 event.closure.as_ref().unchecked_ref(),
1716 ) {
1717 log::error!(
1718 "Failed to unsubscribe from event: {}",
1719 string_from_js_value(&err)
1720 );
1721 }
1722 }
1723 }
1724 }
1725}
1726
1727#[derive(QueryData)]
1728#[query_data(mutable)]
1729#[allow(missing_docs)]
1730#[cfg(feature = "render")]
1731pub struct UpdateUiSizeAndScaleQuery {
1732 ctx: &'static mut EguiContext,
1733 egui_input: &'static mut EguiInput,
1734 egui_settings: &'static EguiContextSettings,
1735 camera: &'static bevy_render::camera::Camera,
1736}
1737
1738#[cfg(feature = "render")]
1739pub fn update_ui_size_and_scale_system(mut contexts: Query<UpdateUiSizeAndScaleQuery>) {
1741 for mut context in contexts.iter_mut() {
1742 let Some((scale_factor, viewport_rect)) = context
1743 .camera
1744 .target_scaling_factor()
1745 .map(|scale_factor| scale_factor * context.egui_settings.scale_factor)
1746 .zip(context.camera.physical_viewport_rect())
1747 else {
1748 continue;
1749 };
1750
1751 let viewport_rect = egui::Rect {
1752 min: helpers::vec2_into_egui_pos2(viewport_rect.min.as_vec2() / scale_factor),
1753 max: helpers::vec2_into_egui_pos2(viewport_rect.max.as_vec2() / scale_factor),
1754 };
1755 if viewport_rect.width() < 1.0 || viewport_rect.height() < 1.0 {
1756 continue;
1757 }
1758 context.egui_input.screen_rect = Some(viewport_rect);
1759 context.ctx.get_mut().set_pixels_per_point(scale_factor);
1760 }
1761}
1762
1763pub fn begin_pass_system(
1765 mut contexts: Query<
1766 (&mut EguiContext, &EguiContextSettings, &mut EguiInput),
1767 Without<EguiMultipassSchedule>,
1768 >,
1769) {
1770 for (mut ctx, egui_settings, mut egui_input) in contexts.iter_mut() {
1771 if !egui_settings.run_manually {
1772 ctx.get_mut().begin_pass(egui_input.take());
1773 }
1774 }
1775}
1776
1777pub fn end_pass_system(
1779 mut contexts: Query<
1780 (&mut EguiContext, &EguiContextSettings, &mut EguiFullOutput),
1781 Without<EguiMultipassSchedule>,
1782 >,
1783) {
1784 for (mut ctx, egui_settings, mut full_output) in contexts.iter_mut() {
1785 if !egui_settings.run_manually {
1786 **full_output = Some(ctx.get_mut().end_pass());
1787 }
1788 }
1789}
1790
1791#[cfg(feature = "accesskit_placeholder")]
1793pub fn update_accessibility_system(
1794 requested: Res<bevy_a11y::AccessibilityRequested>,
1795 mut manage_accessibility_updates: ResMut<bevy_a11y::ManageAccessibilityUpdates>,
1796 outputs: Query<(Entity, &EguiOutput)>,
1797 mut adapters: NonSendMut<bevy_winit::accessibility::AccessKitAdapters>,
1798) {
1799 if requested.get() {
1800 for (entity, output) in &outputs {
1801 if let Some(adapter) = adapters.get_mut(&entity) {
1802 if let Some(update) = &output.platform_output.accesskit_update {
1803 **manage_accessibility_updates = false;
1804 adapter.update_if_active(|| update.clone());
1805 } else if !**manage_accessibility_updates {
1806 **manage_accessibility_updates = true;
1807 }
1808 }
1809 }
1810 }
1811}
1812
1813#[derive(QueryData)]
1814#[query_data(mutable)]
1815#[allow(missing_docs)]
1816pub struct MultiPassEguiQuery {
1817 entity: Entity,
1818 context: &'static mut EguiContext,
1819 input: &'static mut EguiInput,
1820 output: &'static mut EguiFullOutput,
1821 multipass_schedule: &'static EguiMultipassSchedule,
1822 settings: &'static EguiContextSettings,
1823}
1824
1825pub fn run_egui_context_pass_loop_system(world: &mut World) {
1828 let mut contexts_query = world.query::<MultiPassEguiQuery>();
1829 let mut used_schedules = HashSet::<InternedScheduleLabel>::default();
1830
1831 let mut multipass_contexts: Vec<_> = contexts_query
1832 .iter_mut(world)
1833 .filter_map(|mut egui_context| {
1834 if egui_context.settings.run_manually {
1835 return None;
1836 }
1837
1838 Some((
1839 egui_context.entity,
1840 egui_context.context.get_mut().clone(),
1841 egui_context.input.take(),
1842 egui_context.multipass_schedule.clone(),
1843 ))
1844 })
1845 .collect();
1846
1847 for (entity, ctx, ref mut input, EguiMultipassSchedule(multipass_schedule)) in
1848 &mut multipass_contexts
1849 {
1850 if !used_schedules.insert(*multipass_schedule) {
1851 panic!("Each Egui context running in the multi-pass mode must have a unique schedule (attempted to reuse schedule {multipass_schedule:?})");
1852 }
1853
1854 let output = ctx.run(input.take(), |_| {
1855 let _ = world.try_run_schedule(*multipass_schedule);
1856 });
1857
1858 **contexts_query
1859 .get_mut(world, *entity)
1860 .expect("previously queried context")
1861 .output = Some(output);
1862 }
1863
1864 if world
1868 .query_filtered::<Entity, (With<EguiContext>, With<PrimaryEguiContext>)>()
1869 .iter(world)
1870 .next()
1871 .is_none()
1872 {
1873 return;
1876 }
1877 if !used_schedules.contains(&ScheduleLabel::intern(&EguiPrimaryContextPass)) {
1878 let _ = world.try_run_schedule(EguiPrimaryContextPass);
1879 }
1880}
1881
1882#[cfg(feature = "picking")]
1884pub trait BevyEguiEntityCommandsExt {
1885 fn add_picking_observers_for_context(&mut self, context: Entity) -> &mut Self;
1887}
1888
1889#[cfg(feature = "picking")]
1890impl<'a> BevyEguiEntityCommandsExt for EntityCommands<'a> {
1891 fn add_picking_observers_for_context(&mut self, context: Entity) -> &mut Self {
1892 self.insert(picking::PickableEguiContext(context))
1893 .observe(picking::handle_over_system)
1894 .observe(picking::handle_out_system)
1895 .observe(picking::handle_move_system)
1896 }
1897}
1898
1899#[cfg(test)]
1900mod tests {
1901 #[test]
1902 fn test_readme_deps() {
1903 version_sync::assert_markdown_deps_updated!("README.md");
1904 }
1905}