bevy_egui/
output.rs

1use crate::{
2    helpers, input::WindowToEguiContextMap, EguiContext, EguiContextSettings, EguiFullOutput,
3    EguiGlobalSettings, EguiOutput, EguiRenderOutput,
4};
5use bevy_ecs::{
6    entity::Entity,
7    event::EventWriter,
8    system::{Commands, Local, NonSend, Query, Res},
9};
10use bevy_platform::collections::HashMap;
11use bevy_window::RequestRedraw;
12use bevy_winit::{cursor::CursorIcon, EventLoopProxy, WakeUp};
13use std::time::Duration;
14
15/// Reads Egui output.
16#[allow(clippy::too_many_arguments)]
17pub fn process_output_system(
18    mut commands: Commands,
19    mut context_query: Query<(
20        Entity,
21        &mut EguiContext,
22        &mut EguiFullOutput,
23        &mut EguiRenderOutput,
24        &mut EguiOutput,
25        &EguiContextSettings,
26    )>,
27    #[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
28    mut egui_clipboard: bevy_ecs::system::ResMut<crate::EguiClipboard>,
29    mut event: EventWriter<RequestRedraw>,
30    mut last_cursor_icon: Local<HashMap<Entity, egui::CursorIcon>>,
31    event_loop_proxy: Option<NonSend<EventLoopProxy<WakeUp>>>,
32    egui_global_settings: Res<EguiGlobalSettings>,
33    window_to_egui_context_map: Res<WindowToEguiContextMap>,
34) {
35    let mut should_request_redraw = false;
36
37    for (entity, mut context, mut full_output, mut render_output, mut egui_output, settings) in
38        context_query.iter_mut()
39    {
40        let ctx = context.get_mut();
41        let Some(full_output) = full_output.0.take() else {
42            bevy_log::error!("bevy_egui pass output has not been prepared (if EguiSettings::run_manually is set to true, make sure to call egui::Context::run or egui::Context::begin_pass and egui::Context::end_pass)");
43            continue;
44        };
45        let egui::FullOutput {
46            platform_output,
47            shapes,
48            textures_delta,
49            pixels_per_point,
50            viewport_output: _,
51        } = full_output;
52        let paint_jobs = ctx.tessellate(shapes, pixels_per_point);
53
54        render_output.paint_jobs = paint_jobs;
55        render_output.textures_delta = textures_delta;
56        egui_output.platform_output = platform_output;
57
58        for command in &egui_output.platform_output.commands {
59            match command {
60                egui::OutputCommand::CopyText(_text) =>
61                {
62                    #[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
63                    if !_text.is_empty() {
64                        egui_clipboard.set_text(_text);
65                    }
66                }
67                egui::OutputCommand::CopyImage(_image) => {
68                    #[cfg(all(feature = "manage_clipboard", not(target_os = "android")))]
69                    egui_clipboard.set_image(_image);
70                }
71                egui::OutputCommand::OpenUrl(_url) => {
72                    #[cfg(feature = "open_url")]
73                    {
74                        let egui::output::OpenUrl { url, new_tab } = _url;
75                        let target = if *new_tab {
76                            "_blank"
77                        } else {
78                            settings
79                                .default_open_url_target
80                                .as_deref()
81                                .unwrap_or("_self")
82                        };
83                        if let Err(err) = webbrowser::open_browser_with_options(
84                            webbrowser::Browser::Default,
85                            url,
86                            webbrowser::BrowserOptions::new().with_target_hint(target),
87                        ) {
88                            bevy_log::error!("Failed to open '{}': {:?}", url, err);
89                        }
90                    }
91                }
92            }
93        }
94
95        if egui_global_settings.enable_cursor_icon_updates && settings.enable_cursor_icon_updates {
96            if let Some(window_entity) = window_to_egui_context_map.context_to_window.get(&entity) {
97                let last_cursor_icon = last_cursor_icon.entry(entity).or_default();
98                if *last_cursor_icon != egui_output.platform_output.cursor_icon {
99                    commands.entity(*window_entity).insert(CursorIcon::System(
100                        helpers::egui_to_winit_cursor_icon(egui_output.platform_output.cursor_icon)
101                            .unwrap_or(bevy_window::SystemCursorIcon::Default),
102                    ));
103                    *last_cursor_icon = egui_output.platform_output.cursor_icon;
104                }
105            }
106        }
107
108        let needs_repaint = !render_output.is_empty();
109        should_request_redraw |= ctx.has_requested_repaint() && needs_repaint;
110
111        // The resource doesn't exist in the headless mode.
112        if let Some(event_loop_proxy) = &event_loop_proxy {
113            // A zero duration indicates that it's an outstanding redraw request, which gives Egui an
114            // opportunity to settle the effects of interactions with widgets. Such repaint requests
115            // are processed not immediately but on a next frame. In this case, we need to indicate to
116            // winit, that it needs to wake up next frame as well even if there are no inputs.
117            //
118            // TLDR: this solves repaint corner cases of `WinitSettings::desktop_app()`.
119            if let Some(Duration::ZERO) =
120                ctx.viewport(|viewport| viewport.input.wants_repaint_after())
121            {
122                let _ = event_loop_proxy.send_event(WakeUp);
123            }
124        }
125    }
126
127    if should_request_redraw {
128        event.write(RequestRedraw);
129    }
130}