bevy_egui/
output.rs

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