1#![warn(missing_docs)]
2#![allow(clippy::type_complexity)]
3
4#[cfg(feature = "render")]
144pub mod egui_node;
145pub mod helpers;
147pub mod input;
149pub mod output;
151#[cfg(feature = "render")]
153pub mod render_systems;
154#[cfg(target_arch = "wasm32")]
156pub mod text_agent;
157#[cfg(all(feature = "manage_clipboard", target_arch = "wasm32",))]
159pub mod web_clipboard;
160
161pub use egui;
162
163use crate::input::*;
164#[cfg(target_arch = "wasm32")]
165use crate::text_agent::{
166 install_text_agent_system, is_mobile_safari, process_safari_virtual_keyboard_system,
167 write_text_agent_channel_events_system, SafariVirtualKeyboardTouchState, TextAgentChannel,
168 VirtualTouchInfo,
169};
170#[cfg(feature = "render")]
171use crate::{
172 egui_node::{EguiPipeline, EGUI_SHADER_HANDLE},
173 render_systems::{EguiRenderData, EguiTransforms, ExtractedEguiManagedTextures},
174};
175#[cfg(all(
176 feature = "manage_clipboard",
177 not(any(target_arch = "wasm32", target_os = "android"))
178))]
179use arboard::Clipboard;
180use bevy_app::prelude::*;
181#[cfg(feature = "render")]
182use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle};
183use bevy_derive::{Deref, DerefMut};
184use bevy_ecs::{
185 prelude::*,
186 query::{QueryData, QueryEntityError},
187 schedule::{InternedScheduleLabel, ScheduleLabel},
188 system::SystemParam,
189};
190#[cfg(feature = "render")]
191use bevy_image::{Image, ImageSampler};
192use bevy_input::InputSystem;
193use bevy_log as log;
194#[cfg(feature = "picking")]
195use bevy_picking::{
196 backend::{HitData, PointerHits},
197 pointer::{PointerId, PointerLocation},
198};
199#[cfg(feature = "render")]
200use bevy_platform::collections::HashMap;
201use bevy_platform::collections::HashSet;
202use bevy_reflect::Reflect;
203#[cfg(feature = "picking")]
204use bevy_render::camera::NormalizedRenderTarget;
205#[cfg(feature = "render")]
206use bevy_render::{
207 extract_component::{ExtractComponent, ExtractComponentPlugin},
208 extract_resource::{ExtractResource, ExtractResourcePlugin},
209 render_resource::{LoadOp, SpecializedRenderPipelines},
210 ExtractSchedule, Render, RenderApp, RenderSet,
211};
212use bevy_window::{PrimaryWindow, Window};
213use bevy_winit::cursor::CursorIcon;
214use output::process_output_system;
215#[cfg(all(
216 feature = "manage_clipboard",
217 not(any(target_arch = "wasm32", target_os = "android"))
218))]
219use std::cell::{RefCell, RefMut};
220use std::sync::Arc;
221#[cfg(target_arch = "wasm32")]
222use wasm_bindgen::prelude::*;
223
224pub struct EguiPlugin {
226 pub enable_multipass_for_primary_context: bool,
326}
327
328#[derive(Clone, Debug, Resource, Reflect)]
330pub struct EguiGlobalSettings {
331 pub enable_focused_non_window_context_updates: bool,
336 pub input_system_settings: EguiInputSystemSettings,
338 pub enable_absorb_bevy_input_system: bool,
352}
353
354impl Default for EguiGlobalSettings {
355 fn default() -> Self {
356 Self {
357 enable_focused_non_window_context_updates: true,
358 input_system_settings: EguiInputSystemSettings::default(),
359 enable_absorb_bevy_input_system: false,
360 }
361 }
362}
363
364#[derive(Resource)]
366pub struct EnableMultipassForPrimaryContext;
367
368#[derive(Clone, Debug, Component, Reflect)]
370#[cfg_attr(feature = "render", derive(ExtractComponent))]
371pub struct EguiContextSettings {
372 pub run_manually: bool,
374 pub scale_factor: f32,
388 #[cfg(feature = "open_url")]
391 pub default_open_url_target: Option<String>,
392 #[cfg(feature = "picking")]
394 pub capture_pointer_input: bool,
395 pub input_system_settings: EguiInputSystemSettings,
397}
398
399impl PartialEq for EguiContextSettings {
401 #[allow(clippy::let_and_return)]
402 fn eq(&self, other: &Self) -> bool {
403 let eq = self.scale_factor == other.scale_factor;
404 #[cfg(feature = "open_url")]
405 let eq = eq && self.default_open_url_target == other.default_open_url_target;
406 eq
407 }
408}
409
410impl Default for EguiContextSettings {
411 fn default() -> Self {
412 Self {
413 run_manually: false,
414 scale_factor: 1.0,
415 #[cfg(feature = "open_url")]
416 default_open_url_target: None,
417 #[cfg(feature = "picking")]
418 capture_pointer_input: true,
419 input_system_settings: EguiInputSystemSettings::default(),
420 }
421 }
422}
423
424#[derive(Clone, Debug, Reflect, PartialEq, Eq)]
425pub struct EguiInputSystemSettings {
427 pub run_write_modifiers_keys_state_system: bool,
429 pub run_write_window_pointer_moved_events_system: bool,
431 pub run_write_pointer_button_events_system: bool,
433 pub run_write_window_touch_events_system: bool,
435 pub run_write_non_window_pointer_moved_events_system: bool,
437 pub run_write_mouse_wheel_events_system: bool,
439 pub run_write_non_window_touch_events_system: bool,
441 pub run_write_keyboard_input_events_system: bool,
443 pub run_write_ime_events_system: bool,
445 pub run_write_file_dnd_events_system: bool,
447 #[cfg(target_arch = "wasm32")]
449 pub run_write_text_agent_channel_events_system: bool,
450 #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
452 pub run_write_web_clipboard_events_system: bool,
453}
454
455impl Default for EguiInputSystemSettings {
456 fn default() -> Self {
457 Self {
458 run_write_modifiers_keys_state_system: true,
459 run_write_window_pointer_moved_events_system: true,
460 run_write_pointer_button_events_system: true,
461 run_write_window_touch_events_system: true,
462 run_write_non_window_pointer_moved_events_system: true,
463 run_write_mouse_wheel_events_system: true,
464 run_write_non_window_touch_events_system: true,
465 run_write_keyboard_input_events_system: true,
466 run_write_ime_events_system: true,
467 run_write_file_dnd_events_system: true,
468 #[cfg(target_arch = "wasm32")]
469 run_write_text_agent_channel_events_system: true,
470 #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
471 run_write_web_clipboard_events_system: true,
472 }
473 }
474}
475
476#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
479pub struct EguiContextPass;
480
481#[derive(Component, Clone)]
484pub struct EguiMultipassSchedule(pub InternedScheduleLabel);
485
486impl EguiMultipassSchedule {
487 pub fn new(schedule: impl ScheduleLabel) -> Self {
489 Self(schedule.intern())
490 }
491}
492
493#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
497pub struct EguiInput(pub egui::RawInput);
498
499#[derive(Component, Clone, Default, Deref, DerefMut)]
501pub struct EguiFullOutput(pub Option<egui::FullOutput>);
502
503#[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
507#[derive(Default, Resource)]
508pub struct EguiClipboard {
509 #[cfg(not(target_arch = "wasm32"))]
510 clipboard: thread_local::ThreadLocal<Option<RefCell<Clipboard>>>,
511 #[cfg(target_arch = "wasm32")]
512 clipboard: web_clipboard::WebClipboard,
513}
514
515#[derive(Component, Clone, Default, Debug)]
517#[cfg_attr(feature = "render", derive(ExtractComponent))]
518pub struct EguiRenderOutput {
519 pub paint_jobs: Arc<Vec<egui::ClippedPrimitive>>,
526
527 pub textures_delta: Arc<egui::TexturesDelta>,
531}
532
533impl EguiRenderOutput {
534 pub fn is_empty(&self) -> bool {
536 self.paint_jobs.is_empty() && self.textures_delta.is_empty()
537 }
538}
539
540#[derive(Component, Clone, Default)]
542pub struct EguiOutput {
543 pub platform_output: egui::PlatformOutput,
545}
546
547#[derive(Clone, Component, Default)]
549#[cfg_attr(feature = "render", derive(ExtractComponent))]
550#[require(
551 EguiContextSettings,
552 EguiInput,
553 EguiContextPointerPosition,
554 EguiContextPointerTouchId,
555 EguiContextImeState,
556 EguiFullOutput,
557 EguiRenderOutput,
558 EguiOutput,
559 RenderTargetSize,
560 CursorIcon
561)]
562pub struct EguiContext {
563 ctx: egui::Context,
564}
565
566impl EguiContext {
567 #[cfg(feature = "immutable_ctx")]
577 #[must_use]
578 pub fn get(&self) -> &egui::Context {
579 &self.ctx
580 }
581
582 #[must_use]
592 pub fn get_mut(&mut self) -> &mut egui::Context {
593 &mut self.ctx
594 }
595}
596
597#[cfg(not(feature = "render"))]
598type EguiContextsFilter = With<Window>;
599
600#[cfg(feature = "render")]
601type EguiContextsFilter = Or<(With<Window>, With<EguiRenderToImage>)>;
602
603#[derive(SystemParam)]
604pub struct EguiContexts<'w, 's> {
607 q: Query<
608 'w,
609 's,
610 (
611 Entity,
612 &'static mut EguiContext,
613 Option<&'static PrimaryWindow>,
614 ),
615 EguiContextsFilter,
616 >,
617 #[cfg(feature = "render")]
618 user_textures: ResMut<'w, EguiUserTextures>,
619}
620
621impl EguiContexts<'_, '_> {
622 #[must_use]
624 pub fn ctx_mut(&mut self) -> &mut egui::Context {
625 self.try_ctx_mut()
626 .expect("`EguiContexts::ctx_mut` was called for an uninitialized context (primary window), make sure your system is run after [`EguiPreUpdateSet::InitContexts`] (or [`EguiStartupSet::InitContexts`] for startup systems)")
627 }
628
629 #[must_use]
631 pub fn try_ctx_mut(&mut self) -> Option<&mut egui::Context> {
632 self.q
633 .iter_mut()
634 .find_map(|(_window_entity, ctx, primary_window)| {
635 if primary_window.is_some() {
636 Some(ctx.into_inner().get_mut())
637 } else {
638 None
639 }
640 })
641 }
642
643 #[must_use]
645 pub fn ctx_for_entity_mut(&mut self, entity: Entity) -> &mut egui::Context {
646 self.try_ctx_for_entity_mut(entity)
647 .unwrap_or_else(|| panic!("`EguiContexts::ctx_for_window_mut` was called for an uninitialized context (entity {entity:?}), make sure your system is run after [`EguiPreUpdateSet::InitContexts`] (or [`EguiStartupSet::InitContexts`] for startup systems)"))
648 }
649
650 #[must_use]
652 #[track_caller]
653 pub fn try_ctx_for_entity_mut(&mut self, entity: Entity) -> Option<&mut egui::Context> {
654 self.q
655 .iter_mut()
656 .find_map(|(window_entity, ctx, _primary_window)| {
657 if window_entity == entity {
658 Some(ctx.into_inner().get_mut())
659 } else {
660 None
661 }
662 })
663 }
664
665 #[track_caller]
668 pub fn ctx_for_entities_mut<const N: usize>(
669 &mut self,
670 ids: [Entity; N],
671 ) -> Result<[&mut egui::Context; N], QueryEntityError> {
672 self.q
673 .get_many_mut(ids)
674 .map(|arr| arr.map(|(_window_entity, ctx, _primary_window)| ctx.into_inner().get_mut()))
675 }
676
677 #[cfg(feature = "immutable_ctx")]
687 #[must_use]
688 pub fn ctx(&self) -> &egui::Context {
689 self.try_ctx()
690 .expect("`EguiContexts::ctx` was called for an uninitialized context (primary window), make sure your system is run after [`EguiPreUpdateSet::InitContexts`] (or [`EguiStartupSet::InitContexts`] for startup systems)")
691 }
692
693 #[cfg(feature = "immutable_ctx")]
703 #[must_use]
704 pub fn try_ctx(&self) -> Option<&egui::Context> {
705 self.q
706 .iter()
707 .find_map(|(_window_entity, ctx, primary_window)| {
708 if primary_window.is_some() {
709 Some(ctx.get())
710 } else {
711 None
712 }
713 })
714 }
715
716 #[must_use]
726 #[cfg(feature = "immutable_ctx")]
727 pub fn ctx_for_entity(&self, entity: Entity) -> &egui::Context {
728 self.try_ctx_for_entity(entity)
729 .unwrap_or_else(|| panic!("`EguiContexts::ctx_for_entity` was called for an uninitialized context (entity {entity:?}), make sure your system is run after [`EguiPreUpdateSet::InitContexts`] (or [`EguiStartupSet::InitContexts`] for startup systems)"))
730 }
731
732 #[must_use]
742 #[track_caller]
743 #[cfg(feature = "immutable_ctx")]
744 pub fn try_ctx_for_entity(&self, entity: Entity) -> Option<&egui::Context> {
745 self.q
746 .iter()
747 .find_map(|(window_entity, ctx, _primary_window)| {
748 if window_entity == entity {
749 Some(ctx.get())
750 } else {
751 None
752 }
753 })
754 }
755
756 #[cfg(feature = "render")]
765 pub fn add_image(&mut self, image: Handle<Image>) -> egui::TextureId {
766 self.user_textures.add_image(image)
767 }
768
769 #[cfg(feature = "render")]
771 #[track_caller]
772 pub fn remove_image(&mut self, image: &Handle<Image>) -> Option<egui::TextureId> {
773 self.user_textures.remove_image(image)
774 }
775
776 #[cfg(feature = "render")]
778 #[must_use]
779 #[track_caller]
780 pub fn image_id(&self, image: &Handle<Image>) -> Option<egui::TextureId> {
781 self.user_textures.image_id(image)
782 }
783}
784
785#[cfg(feature = "render")]
790#[derive(Component, Clone, Debug, ExtractComponent)]
791#[require(EguiContext)]
792pub struct EguiRenderToImage {
793 pub handle: Handle<Image>,
795 pub load_op: LoadOp<wgpu_types::Color>,
800}
801
802#[cfg(feature = "render")]
803impl EguiRenderToImage {
804 pub fn new(handle: Handle<Image>) -> Self {
806 Self {
807 handle,
808 load_op: LoadOp::Clear(wgpu_types::Color::TRANSPARENT),
809 }
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)]
877#[cfg_attr(feature = "render", derive(ExtractComponent))]
878pub struct RenderTargetSize {
879 pub physical_width: f32,
881 pub physical_height: f32,
883 pub scale_factor: f32,
885}
886
887impl RenderTargetSize {
888 fn new(physical_width: f32, physical_height: f32, scale_factor: f32) -> Self {
889 Self {
890 physical_width,
891 physical_height,
892 scale_factor,
893 }
894 }
895
896 #[inline]
898 pub fn width(&self) -> f32 {
899 self.physical_width / self.scale_factor
900 }
901
902 #[inline]
904 pub fn height(&self) -> f32 {
905 self.physical_height / self.scale_factor
906 }
907}
908
909pub mod node {
911 pub const EGUI_PASS: &str = "egui_pass";
913}
914
915#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
916pub enum EguiStartupSet {
918 InitContexts,
920}
921
922#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
924pub enum EguiPreUpdateSet {
925 InitContexts,
927 ProcessInput,
933 BeginPass,
935}
936
937#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
939pub enum EguiInputSet {
940 InitReading,
944 FocusContext,
946 ReadBevyEvents,
948 WriteEguiEvents,
950}
951
952#[derive(SystemSet, Clone, Hash, Debug, Eq, PartialEq)]
954pub enum EguiPostUpdateSet {
955 EndPass,
957 ProcessOutput,
959 PostProcessOutput,
961}
962
963impl Plugin for EguiPlugin {
964 fn build(&self, app: &mut App) {
965 app.register_type::<EguiGlobalSettings>();
966 app.register_type::<EguiContextSettings>();
967 app.init_resource::<EguiGlobalSettings>();
968 app.init_resource::<ModifierKeysState>();
969 app.init_resource::<EguiWantsInput>();
970 app.add_event::<EguiInputEvent>();
971 app.add_event::<EguiFileDragAndDropEvent>();
972
973 if self.enable_multipass_for_primary_context {
974 app.insert_resource(EnableMultipassForPrimaryContext);
975 }
976
977 #[cfg(feature = "render")]
978 {
979 app.init_resource::<EguiManagedTextures>();
980 app.init_resource::<EguiUserTextures>();
981 app.add_plugins(ExtractResourcePlugin::<EguiUserTextures>::default());
982 app.add_plugins(ExtractResourcePlugin::<ExtractedEguiManagedTextures>::default());
983 app.add_plugins(ExtractComponentPlugin::<EguiContext>::default());
984 app.add_plugins(ExtractComponentPlugin::<EguiContextSettings>::default());
985 app.add_plugins(ExtractComponentPlugin::<RenderTargetSize>::default());
986 app.add_plugins(ExtractComponentPlugin::<EguiRenderOutput>::default());
987 app.add_plugins(ExtractComponentPlugin::<EguiRenderToImage>::default());
988 }
989
990 #[cfg(target_arch = "wasm32")]
991 app.init_non_send_resource::<SubscribedEvents>();
992
993 #[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
994 app.init_resource::<EguiClipboard>();
995
996 app.configure_sets(
997 PreUpdate,
998 (
999 EguiPreUpdateSet::InitContexts,
1000 EguiPreUpdateSet::ProcessInput.after(InputSystem),
1001 EguiPreUpdateSet::BeginPass,
1002 )
1003 .chain(),
1004 );
1005 app.configure_sets(
1006 PreUpdate,
1007 (
1008 EguiInputSet::InitReading,
1009 EguiInputSet::FocusContext,
1010 EguiInputSet::ReadBevyEvents,
1011 EguiInputSet::WriteEguiEvents,
1012 )
1013 .chain(),
1014 );
1015 #[cfg(not(feature = "accesskit_placeholder"))]
1016 app.configure_sets(
1017 PostUpdate,
1018 (
1019 EguiPostUpdateSet::EndPass,
1020 EguiPostUpdateSet::ProcessOutput,
1021 EguiPostUpdateSet::PostProcessOutput,
1022 )
1023 .chain(),
1024 );
1025 #[cfg(feature = "accesskit_placeholder")]
1026 app.configure_sets(
1027 PostUpdate,
1028 (
1029 EguiPostUpdateSet::EndPass,
1030 EguiPostUpdateSet::ProcessOutput,
1031 EguiPostUpdateSet::PostProcessOutput.before(bevy_a11y::AccessibilitySystem::Update),
1032 )
1033 .chain(),
1034 );
1035
1036 #[cfg(all(feature = "manage_clipboard", target_arch = "wasm32"))]
1038 {
1039 app.add_systems(PreStartup, web_clipboard::startup_setup_web_events_system);
1040 }
1041 app.add_systems(
1042 PreStartup,
1043 (
1044 setup_new_windows_system,
1045 ApplyDeferred,
1046 update_ui_size_and_scale_system,
1047 )
1048 .chain()
1049 .in_set(EguiStartupSet::InitContexts),
1050 );
1051
1052 app.add_systems(
1054 PreUpdate,
1055 (
1056 setup_new_windows_system,
1057 ApplyDeferred,
1058 update_ui_size_and_scale_system,
1059 )
1060 .chain()
1061 .in_set(EguiPreUpdateSet::InitContexts),
1062 );
1063 app.add_systems(
1064 PreUpdate,
1065 (
1066 (
1067 write_modifiers_keys_state_system.run_if(input_system_is_enabled(|s| {
1068 s.run_write_modifiers_keys_state_system
1069 })),
1070 write_window_pointer_moved_events_system.run_if(input_system_is_enabled(|s| {
1071 s.run_write_window_pointer_moved_events_system
1072 })),
1073 )
1074 .in_set(EguiInputSet::InitReading),
1075 (
1076 write_pointer_button_events_system.run_if(input_system_is_enabled(|s| {
1077 s.run_write_pointer_button_events_system
1078 })),
1079 write_window_touch_events_system.run_if(input_system_is_enabled(|s| {
1080 s.run_write_window_touch_events_system
1081 })),
1082 )
1083 .in_set(EguiInputSet::FocusContext),
1084 (
1085 write_non_window_pointer_moved_events_system.run_if(input_system_is_enabled(
1086 |s| s.run_write_non_window_pointer_moved_events_system,
1087 )),
1088 write_non_window_touch_events_system.run_if(input_system_is_enabled(|s| {
1089 s.run_write_non_window_touch_events_system
1090 })),
1091 write_mouse_wheel_events_system.run_if(input_system_is_enabled(|s| {
1092 s.run_write_mouse_wheel_events_system
1093 })),
1094 write_keyboard_input_events_system.run_if(input_system_is_enabled(|s| {
1095 s.run_write_keyboard_input_events_system
1096 })),
1097 write_ime_events_system
1098 .run_if(input_system_is_enabled(|s| s.run_write_ime_events_system)),
1099 write_file_dnd_events_system.run_if(input_system_is_enabled(|s| {
1100 s.run_write_file_dnd_events_system
1101 })),
1102 )
1103 .in_set(EguiInputSet::ReadBevyEvents),
1104 (
1105 write_egui_input_system,
1106 absorb_bevy_input_system.run_if(|settings: Res<EguiGlobalSettings>| {
1107 settings.enable_absorb_bevy_input_system
1108 }),
1109 )
1110 .in_set(EguiInputSet::WriteEguiEvents),
1111 )
1112 .chain()
1113 .in_set(EguiPreUpdateSet::ProcessInput),
1114 );
1115 app.add_systems(
1116 PreUpdate,
1117 begin_pass_system.in_set(EguiPreUpdateSet::BeginPass),
1118 );
1119
1120 #[cfg(target_arch = "wasm32")]
1122 {
1123 use std::sync::{LazyLock, Mutex};
1124
1125 let maybe_window_plugin = app.get_added_plugins::<bevy_window::WindowPlugin>();
1126
1127 if !maybe_window_plugin.is_empty()
1128 && maybe_window_plugin[0].primary_window.is_some()
1129 && maybe_window_plugin[0]
1130 .primary_window
1131 .as_ref()
1132 .unwrap()
1133 .prevent_default_event_handling
1134 {
1135 app.init_resource::<TextAgentChannel>();
1136
1137 let (sender, receiver) = crossbeam_channel::unbounded();
1138 static TOUCH_INFO: LazyLock<Mutex<VirtualTouchInfo>> =
1139 LazyLock::new(|| Mutex::new(VirtualTouchInfo::default()));
1140
1141 app.insert_resource(SafariVirtualKeyboardTouchState {
1142 sender,
1143 receiver,
1144 touch_info: &TOUCH_INFO,
1145 });
1146
1147 app.add_systems(
1148 PreStartup,
1149 install_text_agent_system.in_set(EguiStartupSet::InitContexts),
1150 );
1151
1152 app.add_systems(
1153 PreUpdate,
1154 write_text_agent_channel_events_system
1155 .run_if(input_system_is_enabled(|s| {
1156 s.run_write_text_agent_channel_events_system
1157 }))
1158 .in_set(EguiPreUpdateSet::ProcessInput)
1159 .in_set(EguiInputSet::ReadBevyEvents),
1160 );
1161
1162 if is_mobile_safari() {
1163 app.add_systems(
1164 PostUpdate,
1165 process_safari_virtual_keyboard_system
1166 .in_set(EguiPostUpdateSet::PostProcessOutput),
1167 );
1168 }
1169 }
1170
1171 #[cfg(feature = "manage_clipboard")]
1172 app.add_systems(
1173 PreUpdate,
1174 web_clipboard::write_web_clipboard_events_system
1175 .run_if(input_system_is_enabled(|s| {
1176 s.run_write_web_clipboard_events_system
1177 }))
1178 .in_set(EguiPreUpdateSet::ProcessInput)
1179 .in_set(EguiInputSet::ReadBevyEvents),
1180 );
1181 }
1182
1183 app.add_systems(
1185 PostUpdate,
1186 (run_egui_context_pass_loop_system, end_pass_system)
1187 .chain()
1188 .in_set(EguiPostUpdateSet::EndPass),
1189 );
1190 app.add_systems(
1191 PostUpdate,
1192 (process_output_system, write_egui_wants_input_system)
1193 .in_set(EguiPostUpdateSet::ProcessOutput),
1194 );
1195 #[cfg(feature = "picking")]
1196 if app.is_plugin_added::<bevy_picking::PickingPlugin>() {
1197 app.add_systems(PostUpdate, capture_pointer_input_system);
1198 } else {
1199 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)");
1200 }
1201
1202 #[cfg(feature = "render")]
1203 app.add_systems(
1204 PostUpdate,
1205 update_egui_textures_system.in_set(EguiPostUpdateSet::PostProcessOutput),
1206 )
1207 .add_systems(
1208 Render,
1209 render_systems::prepare_egui_transforms_system.in_set(RenderSet::Prepare),
1210 )
1211 .add_systems(
1212 Render,
1213 render_systems::queue_bind_groups_system.in_set(RenderSet::Queue),
1214 )
1215 .add_systems(
1216 Render,
1217 render_systems::queue_pipelines_system.in_set(RenderSet::Queue),
1218 )
1219 .add_systems(Last, free_egui_textures_system);
1220
1221 #[cfg(feature = "render")]
1222 load_internal_asset!(
1223 app,
1224 EGUI_SHADER_HANDLE,
1225 "egui.wgsl",
1226 bevy_render::render_resource::Shader::from_wgsl
1227 );
1228
1229 #[cfg(feature = "accesskit_placeholder")]
1230 app.add_systems(
1231 PostUpdate,
1232 update_accessibility_system.in_set(EguiPostUpdateSet::PostProcessOutput),
1233 );
1234 }
1235
1236 #[cfg(feature = "render")]
1237 fn finish(&self, app: &mut App) {
1238 if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
1239 render_app
1240 .init_resource::<egui_node::EguiPipeline>()
1241 .init_resource::<SpecializedRenderPipelines<EguiPipeline>>()
1242 .init_resource::<EguiTransforms>()
1243 .init_resource::<EguiRenderData>()
1244 .add_systems(
1245 ExtractSchedule,
1248 (
1249 render_systems::setup_new_egui_nodes_system,
1250 render_systems::teardown_window_nodes_system,
1251 render_systems::teardown_render_to_image_nodes_system,
1252 ),
1253 )
1254 .add_systems(
1255 Render,
1256 render_systems::prepare_egui_transforms_system.in_set(RenderSet::Prepare),
1257 )
1258 .add_systems(
1259 Render,
1260 render_systems::prepare_egui_render_target_data.in_set(RenderSet::Prepare),
1261 )
1262 .add_systems(
1263 Render,
1264 render_systems::queue_bind_groups_system.in_set(RenderSet::Queue),
1265 )
1266 .add_systems(
1267 Render,
1268 render_systems::queue_pipelines_system.in_set(RenderSet::Queue),
1269 );
1270 }
1271 }
1272}
1273
1274fn input_system_is_enabled(
1275 test: impl Fn(&EguiInputSystemSettings) -> bool,
1276) -> impl Fn(Res<EguiGlobalSettings>) -> bool {
1277 move |settings| test(&settings.input_system_settings)
1278}
1279
1280#[cfg(feature = "render")]
1282#[derive(Resource, Deref, DerefMut, Default)]
1283pub struct EguiManagedTextures(pub HashMap<(Entity, u64), EguiManagedTexture>);
1284
1285#[cfg(feature = "render")]
1287pub struct EguiManagedTexture {
1288 pub handle: Handle<Image>,
1290 pub color_image: egui::ColorImage,
1292}
1293
1294pub fn setup_new_windows_system(
1296 mut commands: Commands,
1297 new_windows: Query<(Entity, Option<&PrimaryWindow>), (Added<Window>, Without<EguiContext>)>,
1298 #[cfg(feature = "accesskit_placeholder")] adapters: Option<
1299 NonSend<bevy_winit::accessibility::AccessKitAdapters>,
1300 >,
1301 #[cfg(feature = "accesskit_placeholder")] mut manage_accessibility_updates: ResMut<
1302 bevy_a11y::ManageAccessibilityUpdates,
1303 >,
1304 enable_multipass_for_primary_context: Option<Res<EnableMultipassForPrimaryContext>>,
1305) {
1306 for (window, primary) in new_windows.iter() {
1307 let context = EguiContext::default();
1308 #[cfg(feature = "accesskit_placeholder")]
1309 if let Some(adapters) = &adapters {
1310 if adapters.get(&window).is_some() {
1311 context.ctx.enable_accesskit();
1312 **manage_accessibility_updates = false;
1313 }
1314 }
1315 let mut window_commands = commands.entity(window);
1317 window_commands.insert(context);
1318 if enable_multipass_for_primary_context.is_some() && primary.is_some() {
1319 window_commands.insert(EguiMultipassSchedule::new(EguiContextPass));
1320 }
1321 }
1322}
1323
1324#[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
1325impl EguiClipboard {
1326 pub fn set_text(&mut self, contents: &str) {
1328 self.set_text_impl(contents);
1329 }
1330
1331 #[cfg(target_arch = "wasm32")]
1334 pub fn set_text_internal(&mut self, text: &str) {
1335 self.clipboard.set_text_internal(text);
1336 }
1337
1338 #[must_use]
1340 pub fn get_text(&mut self) -> Option<String> {
1341 self.get_text_impl()
1342 }
1343
1344 pub fn set_image(&mut self, image: &egui::ColorImage) {
1346 self.set_image_impl(image);
1347 }
1348
1349 #[cfg(target_arch = "wasm32")]
1351 pub fn try_receive_clipboard_event(&self) -> Option<web_clipboard::WebClipboardEvent> {
1352 self.clipboard.try_receive_clipboard_event()
1353 }
1354
1355 #[cfg(not(target_arch = "wasm32"))]
1356 fn set_text_impl(&mut self, contents: &str) {
1357 if let Some(mut clipboard) = self.get() {
1358 if let Err(err) = clipboard.set_text(contents.to_owned()) {
1359 log::error!("Failed to set clipboard contents: {:?}", err);
1360 }
1361 }
1362 }
1363
1364 #[cfg(target_arch = "wasm32")]
1365 fn set_text_impl(&mut self, contents: &str) {
1366 self.clipboard.set_text(contents);
1367 }
1368
1369 #[cfg(not(target_arch = "wasm32"))]
1370 fn get_text_impl(&mut self) -> Option<String> {
1371 if let Some(mut clipboard) = self.get() {
1372 match clipboard.get_text() {
1373 Ok(contents) => return Some(contents),
1374 Err(arboard::Error::ContentNotAvailable) => return Some("".to_string()),
1376 Err(err) => log::error!("Failed to get clipboard contents: {:?}", err),
1377 }
1378 };
1379 None
1380 }
1381
1382 #[cfg(target_arch = "wasm32")]
1383 #[allow(clippy::unnecessary_wraps)]
1384 fn get_text_impl(&mut self) -> Option<String> {
1385 self.clipboard.get_text()
1386 }
1387
1388 #[cfg(not(target_arch = "wasm32"))]
1389 fn set_image_impl(&mut self, image: &egui::ColorImage) {
1390 if let Some(mut clipboard) = self.get() {
1391 if let Err(err) = clipboard.set_image(arboard::ImageData {
1392 width: image.width(),
1393 height: image.height(),
1394 bytes: std::borrow::Cow::Borrowed(bytemuck::cast_slice(&image.pixels)),
1395 }) {
1396 log::error!("Failed to set clipboard contents: {:?}", err);
1397 }
1398 }
1399 }
1400
1401 #[cfg(target_arch = "wasm32")]
1402 fn set_image_impl(&mut self, image: &egui::ColorImage) {
1403 self.clipboard.set_image(image);
1404 }
1405
1406 #[cfg(not(target_arch = "wasm32"))]
1407 fn get(&self) -> Option<RefMut<Clipboard>> {
1408 self.clipboard
1409 .get_or(|| {
1410 Clipboard::new()
1411 .map(RefCell::new)
1412 .map_err(|err| {
1413 log::error!("Failed to initialize clipboard: {:?}", err);
1414 })
1415 .ok()
1416 })
1417 .as_ref()
1418 .map(|cell| cell.borrow_mut())
1419 }
1420}
1421
1422#[cfg(feature = "picking")]
1424pub const PICKING_ORDER: f32 = 1_000_000.0;
1425
1426#[cfg(feature = "picking")]
1428pub fn capture_pointer_input_system(
1429 pointers: Query<(&PointerId, &PointerLocation)>,
1430 mut egui_context: Query<(Entity, &mut EguiContext, &EguiContextSettings), With<Window>>,
1431 mut output: EventWriter<PointerHits>,
1432) {
1433 use helpers::QueryHelper;
1434
1435 for (pointer, location) in pointers
1436 .iter()
1437 .filter_map(|(i, p)| p.location.as_ref().map(|l| (i, l)))
1438 {
1439 if let NormalizedRenderTarget::Window(id) = location.target {
1440 if let Some((entity, mut ctx, settings)) = egui_context.get_some_mut(id.entity()) {
1441 if settings.capture_pointer_input && ctx.get_mut().wants_pointer_input() {
1442 let entry = (entity, HitData::new(entity, 0.0, None, None));
1443 output.write(PointerHits::new(
1444 *pointer,
1445 Vec::from([entry]),
1446 PICKING_ORDER,
1447 ));
1448 }
1449 }
1450 }
1451 }
1452}
1453
1454#[cfg(feature = "render")]
1456pub fn update_egui_textures_system(
1457 mut egui_render_output: Query<
1458 (Entity, &EguiRenderOutput),
1459 Or<(With<Window>, With<EguiRenderToImage>)>,
1460 >,
1461 mut egui_managed_textures: ResMut<EguiManagedTextures>,
1462 mut image_assets: ResMut<Assets<Image>>,
1463) {
1464 for (entity, egui_render_output) in egui_render_output.iter_mut() {
1465 for (texture_id, image_delta) in &egui_render_output.textures_delta.set {
1466 let color_image = egui_node::as_color_image(&image_delta.image);
1467
1468 let texture_id = match texture_id {
1469 egui::TextureId::Managed(texture_id) => *texture_id,
1470 egui::TextureId::User(_) => continue,
1471 };
1472
1473 let sampler = ImageSampler::Descriptor(
1474 egui_node::texture_options_as_sampler_descriptor(&image_delta.options),
1475 );
1476 if let Some(pos) = image_delta.pos {
1477 if let Some(managed_texture) = egui_managed_textures.get_mut(&(entity, texture_id))
1479 {
1480 update_image_rect(&mut managed_texture.color_image, pos, &color_image);
1482 let image =
1483 egui_node::color_image_as_bevy_image(&managed_texture.color_image, sampler);
1484 managed_texture.handle = image_assets.add(image);
1485 } else {
1486 log::warn!("Partial update of a missing texture (id: {:?})", texture_id);
1487 }
1488 } else {
1489 let image = egui_node::color_image_as_bevy_image(&color_image, sampler);
1491 let handle = image_assets.add(image);
1492 egui_managed_textures.insert(
1493 (entity, texture_id),
1494 EguiManagedTexture {
1495 handle,
1496 color_image,
1497 },
1498 );
1499 }
1500 }
1501 }
1502
1503 fn update_image_rect(dest: &mut egui::ColorImage, [x, y]: [usize; 2], src: &egui::ColorImage) {
1504 for sy in 0..src.height() {
1505 for sx in 0..src.width() {
1506 dest[(x + sx, y + sy)] = src[(sx, sy)];
1507 }
1508 }
1509 }
1510}
1511
1512#[cfg(feature = "render")]
1517pub fn free_egui_textures_system(
1518 mut egui_user_textures: ResMut<EguiUserTextures>,
1519 egui_render_output: Query<
1520 (Entity, &EguiRenderOutput),
1521 Or<(With<Window>, With<EguiRenderToImage>)>,
1522 >,
1523 mut egui_managed_textures: ResMut<EguiManagedTextures>,
1524 mut image_assets: ResMut<Assets<Image>>,
1525 mut image_events: EventReader<AssetEvent<Image>>,
1526) {
1527 for (entity, egui_render_output) in egui_render_output.iter() {
1528 for &texture_id in &egui_render_output.textures_delta.free {
1529 if let egui::TextureId::Managed(texture_id) = texture_id {
1530 let managed_texture = egui_managed_textures.remove(&(entity, texture_id));
1531 if let Some(managed_texture) = managed_texture {
1532 image_assets.remove(&managed_texture.handle);
1533 }
1534 }
1535 }
1536 }
1537
1538 for image_event in image_events.read() {
1539 if let AssetEvent::Removed { id } = image_event {
1540 egui_user_textures.remove_image(&Handle::<Image>::Weak(*id));
1541 }
1542 }
1543}
1544
1545#[cfg(target_arch = "wasm32")]
1547pub fn string_from_js_value(value: &JsValue) -> String {
1548 value.as_string().unwrap_or_else(|| format!("{value:#?}"))
1549}
1550
1551#[cfg(target_arch = "wasm32")]
1552struct EventClosure<T> {
1553 target: web_sys::EventTarget,
1554 event_name: String,
1555 closure: wasm_bindgen::closure::Closure<dyn FnMut(T)>,
1556}
1557
1558#[cfg(target_arch = "wasm32")]
1560#[derive(Default)]
1561pub struct SubscribedEvents {
1562 #[cfg(feature = "manage_clipboard")]
1563 clipboard_event_closures: Vec<EventClosure<web_sys::ClipboardEvent>>,
1564 composition_event_closures: Vec<EventClosure<web_sys::CompositionEvent>>,
1565 keyboard_event_closures: Vec<EventClosure<web_sys::KeyboardEvent>>,
1566 input_event_closures: Vec<EventClosure<web_sys::InputEvent>>,
1567 touch_event_closures: Vec<EventClosure<web_sys::TouchEvent>>,
1568}
1569
1570#[cfg(target_arch = "wasm32")]
1571impl SubscribedEvents {
1572 pub fn unsubscribe_from_all_events(&mut self) {
1575 #[cfg(feature = "manage_clipboard")]
1576 Self::unsubscribe_from_events(&mut self.clipboard_event_closures);
1577 Self::unsubscribe_from_events(&mut self.composition_event_closures);
1578 Self::unsubscribe_from_events(&mut self.keyboard_event_closures);
1579 Self::unsubscribe_from_events(&mut self.input_event_closures);
1580 Self::unsubscribe_from_events(&mut self.touch_event_closures);
1581 }
1582
1583 fn unsubscribe_from_events<T>(events: &mut Vec<EventClosure<T>>) {
1584 let events_to_unsubscribe = std::mem::take(events);
1585
1586 if !events_to_unsubscribe.is_empty() {
1587 for event in events_to_unsubscribe {
1588 if let Err(err) = event.target.remove_event_listener_with_callback(
1589 event.event_name.as_str(),
1590 event.closure.as_ref().unchecked_ref(),
1591 ) {
1592 log::error!(
1593 "Failed to unsubscribe from event: {}",
1594 string_from_js_value(&err)
1595 );
1596 }
1597 }
1598 }
1599 }
1600}
1601
1602#[derive(QueryData)]
1603#[query_data(mutable)]
1604#[allow(missing_docs)]
1605pub struct UpdateUiSizeAndScaleQuery {
1606 ctx: &'static mut EguiContext,
1607 egui_input: &'static mut EguiInput,
1608 render_target_size: &'static mut RenderTargetSize,
1609 egui_settings: &'static EguiContextSettings,
1610 window: Option<&'static Window>,
1611 #[cfg(feature = "render")]
1612 render_to_image: Option<&'static EguiRenderToImage>,
1613}
1614
1615pub fn update_ui_size_and_scale_system(
1617 mut contexts: Query<UpdateUiSizeAndScaleQuery>,
1618 #[cfg(feature = "render")] images: Res<Assets<Image>>,
1619) {
1620 for mut context in contexts.iter_mut() {
1621 let mut render_target_size = None;
1622 if let Some(window) = context.window {
1623 render_target_size = Some(RenderTargetSize::new(
1624 window.physical_width() as f32,
1625 window.physical_height() as f32,
1626 window.scale_factor(),
1627 ));
1628 }
1629 #[cfg(feature = "render")]
1630 if let Some(EguiRenderToImage { handle, .. }) = context.render_to_image {
1631 if let Some(image) = images.get(handle) {
1632 let size = image.size_f32();
1633 render_target_size = Some(RenderTargetSize {
1634 physical_width: size.x,
1635 physical_height: size.y,
1636 scale_factor: 1.0,
1637 })
1638 } else {
1639 log::warn!("Invalid EguiRenderToImage handle: {handle:?}");
1640 }
1641 }
1642
1643 let Some(new_render_target_size) = render_target_size else {
1644 log::error!("bevy_egui context without window or render to texture!");
1645 continue;
1646 };
1647 let width = new_render_target_size.physical_width
1648 / new_render_target_size.scale_factor
1649 / context.egui_settings.scale_factor;
1650 let height = new_render_target_size.physical_height
1651 / new_render_target_size.scale_factor
1652 / context.egui_settings.scale_factor;
1653
1654 if width < 1.0 || height < 1.0 {
1655 continue;
1656 }
1657
1658 context.egui_input.screen_rect = Some(egui::Rect::from_min_max(
1659 egui::pos2(0.0, 0.0),
1660 egui::pos2(width, height),
1661 ));
1662
1663 context.ctx.get_mut().set_pixels_per_point(
1664 new_render_target_size.scale_factor * context.egui_settings.scale_factor,
1665 );
1666
1667 *context.render_target_size = new_render_target_size;
1668 }
1669}
1670
1671pub fn begin_pass_system(
1673 mut contexts: Query<
1674 (&mut EguiContext, &EguiContextSettings, &mut EguiInput),
1675 Without<EguiMultipassSchedule>,
1676 >,
1677) {
1678 for (mut ctx, egui_settings, mut egui_input) in contexts.iter_mut() {
1679 if !egui_settings.run_manually {
1680 ctx.get_mut().begin_pass(egui_input.take());
1681 }
1682 }
1683}
1684
1685pub fn end_pass_system(
1687 mut contexts: Query<
1688 (&mut EguiContext, &EguiContextSettings, &mut EguiFullOutput),
1689 Without<EguiMultipassSchedule>,
1690 >,
1691) {
1692 for (mut ctx, egui_settings, mut full_output) in contexts.iter_mut() {
1693 if !egui_settings.run_manually {
1694 **full_output = Some(ctx.get_mut().end_pass());
1695 }
1696 }
1697}
1698
1699#[cfg(feature = "accesskit_placeholder")]
1701pub fn update_accessibility_system(
1702 requested: Res<bevy_a11y::AccessibilityRequested>,
1703 mut manage_accessibility_updates: ResMut<bevy_a11y::ManageAccessibilityUpdates>,
1704 outputs: Query<(Entity, &EguiOutput)>,
1705 mut adapters: NonSendMut<bevy_winit::accessibility::AccessKitAdapters>,
1706) {
1707 if requested.get() {
1708 for (entity, output) in &outputs {
1709 if let Some(adapter) = adapters.get_mut(&entity) {
1710 if let Some(update) = &output.platform_output.accesskit_update {
1711 **manage_accessibility_updates = false;
1712 adapter.update_if_active(|| update.clone());
1713 } else if !**manage_accessibility_updates {
1714 **manage_accessibility_updates = true;
1715 }
1716 }
1717 }
1718 }
1719}
1720
1721#[derive(QueryData)]
1722#[query_data(mutable)]
1723#[allow(missing_docs)]
1724pub struct MultiPassEguiQuery {
1725 entity: Entity,
1726 context: &'static mut EguiContext,
1727 input: &'static mut EguiInput,
1728 output: &'static mut EguiFullOutput,
1729 multipass_schedule: &'static EguiMultipassSchedule,
1730 settings: &'static EguiContextSettings,
1731}
1732
1733pub fn run_egui_context_pass_loop_system(world: &mut World) {
1736 let mut contexts_query = world.query::<MultiPassEguiQuery>();
1737 let mut used_schedules = HashSet::<InternedScheduleLabel>::default();
1738
1739 let mut multipass_contexts: Vec<_> = contexts_query
1740 .iter_mut(world)
1741 .filter_map(|mut egui_context| {
1742 if egui_context.settings.run_manually {
1743 return None;
1744 }
1745
1746 Some((
1747 egui_context.entity,
1748 egui_context.context.get_mut().clone(),
1749 egui_context.input.take(),
1750 egui_context.multipass_schedule.clone(),
1751 ))
1752 })
1753 .collect();
1754
1755 for (entity, ctx, ref mut input, EguiMultipassSchedule(multipass_schedule)) in
1756 &mut multipass_contexts
1757 {
1758 if !used_schedules.insert(*multipass_schedule) {
1759 panic!("Each Egui context running in the multi-pass mode must have a unique schedule (attempted to reuse schedule {multipass_schedule:?})");
1760 }
1761
1762 let output = ctx.run(input.take(), |_| {
1763 let _ = world.try_run_schedule(*multipass_schedule);
1764 });
1765
1766 **contexts_query
1767 .get_mut(world, *entity)
1768 .expect("previously queried context")
1769 .output = Some(output);
1770 }
1771
1772 if !used_schedules.contains(&ScheduleLabel::intern(&EguiContextPass)) {
1773 let _ = world.try_run_schedule(EguiContextPass);
1774 }
1775}
1776
1777#[cfg(test)]
1778mod tests {
1779 #[test]
1780 fn test_readme_deps() {
1781 version_sync::assert_markdown_deps_updated!("README.md");
1782 }
1783}