mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 05:10:03 -04:00
Merge branch 'main' into lucas/malmal/main
This commit is contained in:
@@ -72,7 +72,8 @@ pub struct CreationContext<'s> {
|
||||
|
||||
/// The `get_proc_address` wrapper of underlying GL context
|
||||
#[cfg(feature = "glow")]
|
||||
pub get_proc_address: Option<&'s dyn Fn(&std::ffi::CStr) -> *const std::ffi::c_void>,
|
||||
pub get_proc_address:
|
||||
Option<std::sync::Arc<dyn Fn(&std::ffi::CStr) -> *const std::ffi::c_void + Send + Sync>>,
|
||||
|
||||
/// The underlying WGPU render state.
|
||||
///
|
||||
|
||||
@@ -204,7 +204,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
|
||||
use crate::icon_data::IconDataExt as _;
|
||||
profiling::function_scope!();
|
||||
|
||||
use objc2::ClassType as _;
|
||||
use objc2::AnyThread as _;
|
||||
use objc2_app_kit::{NSApplication, NSImage};
|
||||
use objc2_foundation::NSString;
|
||||
|
||||
|
||||
@@ -265,6 +265,7 @@ impl EpiIntegration {
|
||||
app: &mut dyn epi::App,
|
||||
viewport_ui_cb: Option<&DeferredViewportUiCallback>,
|
||||
mut raw_input: egui::RawInput,
|
||||
is_visible: bool,
|
||||
) -> egui::FullOutput {
|
||||
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
|
||||
|
||||
@@ -275,23 +276,27 @@ impl EpiIntegration {
|
||||
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
||||
if let Some(viewport_ui_cb) = viewport_ui_cb {
|
||||
// Child viewport
|
||||
profiling::scope!("viewport_callback");
|
||||
viewport_ui_cb(ui);
|
||||
if is_visible {
|
||||
profiling::scope!("viewport_callback");
|
||||
viewport_ui_cb(ui);
|
||||
}
|
||||
} else {
|
||||
{
|
||||
profiling::scope!("App::logic");
|
||||
app.logic(ui.ctx(), &mut self.frame);
|
||||
}
|
||||
|
||||
{
|
||||
profiling::scope!("App::update");
|
||||
#[expect(deprecated)]
|
||||
app.update(ui.ctx(), &mut self.frame);
|
||||
}
|
||||
if is_visible {
|
||||
{
|
||||
profiling::scope!("App::update");
|
||||
#[expect(deprecated)]
|
||||
app.update(ui.ctx(), &mut self.frame);
|
||||
}
|
||||
|
||||
{
|
||||
profiling::scope!("App::ui");
|
||||
app.ui(ui, &mut self.frame);
|
||||
{
|
||||
profiling::scope!("App::ui");
|
||||
app.ui(ui, &mut self.frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ use egui_winit::accesskit_winit;
|
||||
|
||||
use crate::{
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::epi_integration::EpiIntegration,
|
||||
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -294,14 +294,15 @@ impl<'app> GlowWinitApp<'app> {
|
||||
// Use latest raw_window_handle for eframe compatibility
|
||||
use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _};
|
||||
|
||||
let get_proc_address = |addr: &_| glutin.get_proc_address(addr);
|
||||
let gl_config = glutin.gl_config.clone();
|
||||
let get_proc_address = move |addr: &_| gl_config.display().get_proc_address(addr);
|
||||
let window = glutin.window(ViewportId::ROOT);
|
||||
let cc = CreationContext {
|
||||
egui_ctx: integration.egui_ctx.clone(),
|
||||
integration_info: integration.frame.info().clone(),
|
||||
storage: integration.frame.storage(),
|
||||
gl: Some(gl),
|
||||
get_proc_address: Some(&get_proc_address),
|
||||
get_proc_address: Some(Arc::new(get_proc_address)),
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state: None,
|
||||
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
|
||||
@@ -466,12 +467,21 @@ impl WinitApp for GlowWinitApp<'_> {
|
||||
if let Some(viewport) = glutin
|
||||
.focused_viewport
|
||||
.and_then(|viewport| glutin.viewports.get_mut(&viewport))
|
||||
&& let Some(window) = viewport.window.as_ref()
|
||||
{
|
||||
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
|
||||
egui_winit.on_mouse_motion(delta);
|
||||
if !window.has_focus()
|
||||
&& !viewport
|
||||
.egui_winit
|
||||
.as_ref()
|
||||
.map(|state| state.is_any_pointer_button_down())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(EventResult::Wait);
|
||||
}
|
||||
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
if let Some(egui_winit) = viewport.egui_winit.as_mut()
|
||||
&& egui_winit.on_mouse_motion(delta)
|
||||
{
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
}
|
||||
@@ -554,7 +564,7 @@ impl GlowWinitRunning<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
let (raw_input, viewport_ui_cb) = {
|
||||
let (raw_input, viewport_ui_cb, is_visible) = {
|
||||
let mut glutin = self.glutin.borrow_mut();
|
||||
let egui_ctx = glutin.egui_ctx.clone();
|
||||
let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else {
|
||||
@@ -565,6 +575,8 @@ impl GlowWinitRunning<'_> {
|
||||
};
|
||||
egui_winit::update_viewport_info(&mut viewport.info, &egui_ctx, window, false);
|
||||
|
||||
let is_visible = viewport.info.visible().unwrap_or(true);
|
||||
|
||||
let Some(egui_winit) = viewport.egui_winit.as_mut() else {
|
||||
return Ok(EventResult::Wait);
|
||||
};
|
||||
@@ -580,7 +592,7 @@ impl GlowWinitRunning<'_> {
|
||||
.map(|(id, viewport)| (*id, viewport.info.clone()))
|
||||
.collect();
|
||||
|
||||
(raw_input, viewport_ui_cb)
|
||||
(raw_input, viewport_ui_cb, is_visible)
|
||||
};
|
||||
|
||||
// HACK: In order to get the right clear_color, the system theme needs to be set, which
|
||||
@@ -596,7 +608,7 @@ impl GlowWinitRunning<'_> {
|
||||
let has_many_viewports = self.glutin.borrow().viewports.len() > 1;
|
||||
let clear_before_update = !has_many_viewports; // HACK: for some reason, an early clear doesn't "take" on Mac with multiple viewports.
|
||||
|
||||
if clear_before_update {
|
||||
if is_visible && clear_before_update {
|
||||
// clear before we call update, so users can paint between clear-color and egui windows:
|
||||
|
||||
let mut glutin = self.glutin.borrow_mut();
|
||||
@@ -631,9 +643,12 @@ impl GlowWinitRunning<'_> {
|
||||
// The update function, which could call immediate viewports,
|
||||
// so make sure we don't hold any locks here required by the immediate viewports rendeer.
|
||||
|
||||
let full_output =
|
||||
self.integration
|
||||
.update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
|
||||
let full_output = self.integration.update(
|
||||
self.app.as_mut(),
|
||||
viewport_ui_cb.as_deref(),
|
||||
raw_input,
|
||||
is_visible,
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -676,85 +691,87 @@ impl GlowWinitRunning<'_> {
|
||||
|
||||
egui_winit.handle_platform_output(&window, platform_output);
|
||||
|
||||
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
|
||||
if is_visible {
|
||||
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
|
||||
|
||||
{
|
||||
// We may need to switch contexts again, because of immediate viewports:
|
||||
frame_timer.pause();
|
||||
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
|
||||
frame_timer.resume();
|
||||
}
|
||||
{
|
||||
// We may need to switch contexts again, because of immediate viewports:
|
||||
frame_timer.pause();
|
||||
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
|
||||
frame_timer.resume();
|
||||
}
|
||||
|
||||
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
|
||||
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
|
||||
|
||||
if !clear_before_update {
|
||||
painter.clear(screen_size_in_pixels, clear_color);
|
||||
}
|
||||
if !clear_before_update {
|
||||
painter.clear(screen_size_in_pixels, clear_color);
|
||||
}
|
||||
|
||||
painter.paint_and_update_textures(
|
||||
screen_size_in_pixels,
|
||||
pixels_per_point,
|
||||
&clipped_primitives,
|
||||
&textures_delta,
|
||||
);
|
||||
painter.paint_and_update_textures(
|
||||
screen_size_in_pixels,
|
||||
pixels_per_point,
|
||||
&clipped_primitives,
|
||||
&textures_delta,
|
||||
);
|
||||
|
||||
{
|
||||
for action in viewport.actions_requested.drain(..) {
|
||||
match action {
|
||||
ActionRequested::Screenshot(user_data) => {
|
||||
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
|
||||
egui_winit
|
||||
.egui_input_mut()
|
||||
.events
|
||||
.push(egui::Event::Screenshot {
|
||||
viewport_id,
|
||||
user_data,
|
||||
image: screenshot.into(),
|
||||
});
|
||||
}
|
||||
ActionRequested::Cut => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
|
||||
}
|
||||
ActionRequested::Copy => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
|
||||
}
|
||||
ActionRequested::Paste => {
|
||||
if let Some(contents) = egui_winit.clipboard_text() {
|
||||
let contents = contents.replace("\r\n", "\n");
|
||||
if !contents.is_empty() {
|
||||
egui_winit
|
||||
.egui_input_mut()
|
||||
.events
|
||||
.push(egui::Event::Paste(contents));
|
||||
{
|
||||
for action in viewport.actions_requested.drain(..) {
|
||||
match action {
|
||||
ActionRequested::Screenshot(user_data) => {
|
||||
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
|
||||
egui_winit
|
||||
.egui_input_mut()
|
||||
.events
|
||||
.push(egui::Event::Screenshot {
|
||||
viewport_id,
|
||||
user_data,
|
||||
image: screenshot.into(),
|
||||
});
|
||||
}
|
||||
ActionRequested::Cut => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
|
||||
}
|
||||
ActionRequested::Copy => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
|
||||
}
|
||||
ActionRequested::Paste => {
|
||||
if let Some(contents) = egui_winit.clipboard_text() {
|
||||
let contents = contents.replace("\r\n", "\n");
|
||||
if !contents.is_empty() {
|
||||
egui_winit
|
||||
.egui_input_mut()
|
||||
.events
|
||||
.push(egui::Event::Paste(contents));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
integration.post_rendering(&window);
|
||||
}
|
||||
|
||||
integration.post_rendering(&window);
|
||||
}
|
||||
{
|
||||
// vsync - don't count as frame-time:
|
||||
frame_timer.pause();
|
||||
profiling::scope!("swap_buffers");
|
||||
let context = current_gl_context.as_ref().ok_or_else(|| {
|
||||
egui_glow::PainterError::from(
|
||||
"failed to get current context to swap buffers".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
{
|
||||
// vsync - don't count as frame-time:
|
||||
frame_timer.pause();
|
||||
profiling::scope!("swap_buffers");
|
||||
let context = current_gl_context.as_ref().ok_or_else(|| {
|
||||
egui_glow::PainterError::from(
|
||||
"failed to get current context to swap buffers".to_owned(),
|
||||
)
|
||||
})?;
|
||||
gl_surface.swap_buffers(context)?;
|
||||
frame_timer.resume();
|
||||
}
|
||||
|
||||
gl_surface.swap_buffers(context)?;
|
||||
frame_timer.resume();
|
||||
}
|
||||
|
||||
// give it time to settle:
|
||||
#[cfg(feature = "__screenshot")]
|
||||
if integration.egui_ctx.cumulative_pass_nr() == 2
|
||||
&& let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
|
||||
{
|
||||
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
|
||||
// give it time to settle:
|
||||
#[cfg(feature = "__screenshot")]
|
||||
if integration.egui_ctx.cumulative_pass_nr() == 2
|
||||
&& let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
|
||||
{
|
||||
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
|
||||
}
|
||||
}
|
||||
|
||||
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
|
||||
@@ -763,9 +780,11 @@ impl GlowWinitRunning<'_> {
|
||||
|
||||
integration.maybe_autosave(app.as_mut(), Some(&window));
|
||||
|
||||
if window.is_minimized() == Some(true) {
|
||||
if is_invisible_or_minimized(&window) {
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
// On Windows, an invisible window also uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/7776
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
@@ -831,6 +850,14 @@ impl GlowWinitRunning<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
winit::event::WindowEvent::Occluded(is_occluded) => {
|
||||
if let Some(viewport_id) = viewport_id
|
||||
&& let Some(viewport) = glutin.viewports.get_mut(&viewport_id)
|
||||
{
|
||||
viewport.info.occluded = Some(*is_occluded);
|
||||
}
|
||||
}
|
||||
|
||||
winit::event::WindowEvent::CloseRequested => {
|
||||
if viewport_id == Some(ViewportId::ROOT) && self.integration.should_close() {
|
||||
log::debug!(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::time::Instant;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
@@ -11,9 +11,20 @@ use ahash::HashMap;
|
||||
use super::winit_integration::{UserEvent, WinitApp};
|
||||
use crate::{
|
||||
Result, epi,
|
||||
native::{event_loop_context, winit_integration::EventResult},
|
||||
native::{
|
||||
event_loop_context,
|
||||
winit_integration::{EventResult, is_invisible_or_minimized},
|
||||
},
|
||||
};
|
||||
|
||||
/// Minimum interval between repaints for invisible windows.
|
||||
///
|
||||
/// On Windows, invisible windows don't receive `RedrawRequested` events,
|
||||
/// so we throttle their repaints to avoid busy-looping while still
|
||||
/// processing viewport commands like `Visible(true)`.
|
||||
/// See <https://github.com/emilk/egui/issues/7776>.
|
||||
const INVISIBLE_WINDOW_REPAINT_INTERVAL: Duration = Duration::from_millis(100);
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result<EventLoop<UserEvent>> {
|
||||
#[cfg(target_os = "android")]
|
||||
@@ -177,23 +188,54 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
fn check_redraw_requests(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let now = Instant::now();
|
||||
|
||||
let mut invisible_window_ids = Vec::new();
|
||||
|
||||
self.windows_next_repaint_times
|
||||
.retain(|window_id, repaint_time| {
|
||||
if now < *repaint_time {
|
||||
return true; // not yet ready
|
||||
}
|
||||
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
if let Some(window) = self.winit_app.window(*window_id) {
|
||||
log::trace!("request_redraw for {window_id:?}");
|
||||
window.request_redraw();
|
||||
// On Windows, invisible windows don't receive RedrawRequested
|
||||
// events, so pending viewport commands (e.g. Visible(true)) would
|
||||
// never be processed. We collect these windows to paint them
|
||||
// directly below.
|
||||
// See: https://github.com/emilk/egui/issues/5229
|
||||
if is_invisible_or_minimized(&window) {
|
||||
invisible_window_ids.push(*window_id);
|
||||
} else {
|
||||
log::trace!("request_redraw for {window_id:?}");
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
window.request_redraw();
|
||||
}
|
||||
} else {
|
||||
log::trace!("No window found for {window_id:?}");
|
||||
}
|
||||
false
|
||||
});
|
||||
|
||||
// Paint invisible windows directly, since they won't receive
|
||||
// RedrawRequested events on Windows. This ensures that viewport
|
||||
// commands like Visible(true) are still processed.
|
||||
for window_id in &invisible_window_ids {
|
||||
let event_result = self.winit_app.run_ui_and_paint(event_loop, *window_id);
|
||||
self.handle_event_result(event_loop, event_result);
|
||||
}
|
||||
|
||||
// Throttle any already-scheduled repaints for invisible windows
|
||||
// to avoid busy-looping. If no repaint was requested by the app,
|
||||
// the window will simply sleep.
|
||||
// See: https://github.com/emilk/egui/issues/7776
|
||||
if !invisible_window_ids.is_empty() {
|
||||
let next_paint = Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL;
|
||||
for window_id in &invisible_window_ids {
|
||||
self.windows_next_repaint_times
|
||||
.entry(*window_id)
|
||||
.and_modify(|t| *t = (*t).min(next_paint));
|
||||
}
|
||||
}
|
||||
|
||||
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
|
||||
if let Some(next_repaint_time) = next_repaint_time {
|
||||
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
|
||||
@@ -270,6 +312,16 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
|
||||
if let Some(window_id) =
|
||||
self.winit_app.window_id_from_viewport_id(viewport_id)
|
||||
{
|
||||
// Throttle repaints for invisible windows to prevent
|
||||
// high CPU usage on Windows.
|
||||
// See: https://github.com/emilk/egui/issues/7776
|
||||
let when = if let Some(window) = self.winit_app.window(window_id)
|
||||
&& is_invisible_or_minimized(&window)
|
||||
{
|
||||
when.max(Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL)
|
||||
} else {
|
||||
when
|
||||
};
|
||||
Ok(EventResult::RepaintAt(window_id, when))
|
||||
} else {
|
||||
Ok(EventResult::Wait)
|
||||
|
||||
@@ -27,7 +27,10 @@ use winit_integration::UserEvent;
|
||||
|
||||
use crate::{
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
|
||||
native::{
|
||||
epi_integration::EpiIntegration,
|
||||
winit_integration::{EventResult, is_invisible_or_minimized},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp};
|
||||
@@ -184,9 +187,17 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
builder: ViewportBuilder,
|
||||
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
|
||||
profiling::function_scope!();
|
||||
// Inject the display handle into the wgpu setup so that wgpu can create
|
||||
// surfaces on platforms that require it (e.g. GLES on Wayland).
|
||||
let mut wgpu_options = self.native_options.wgpu_options.clone();
|
||||
if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup
|
||||
&& create_new.display_handle.is_none()
|
||||
{
|
||||
create_new.display_handle = Some(Box::new(event_loop.owned_display_handle()));
|
||||
}
|
||||
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
|
||||
egui_ctx.clone(),
|
||||
self.native_options.wgpu_options.clone(),
|
||||
wgpu_options,
|
||||
self.native_options.viewport.transparent.unwrap_or(false),
|
||||
egui_wgpu::RendererOptions {
|
||||
msaa_samples: self.native_options.multisampling as _,
|
||||
@@ -474,12 +485,21 @@ impl WinitApp for WgpuWinitApp<'_> {
|
||||
if let Some(viewport) = shared
|
||||
.focused_viewport
|
||||
.and_then(|viewport| shared.viewports.get_mut(&viewport))
|
||||
&& let Some(window) = viewport.window.as_ref()
|
||||
{
|
||||
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
|
||||
egui_winit.on_mouse_motion(delta);
|
||||
if !window.has_focus()
|
||||
&& !viewport
|
||||
.egui_winit
|
||||
.as_ref()
|
||||
.map(|state| state.is_any_pointer_button_down())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(EventResult::Wait);
|
||||
}
|
||||
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
if let Some(egui_winit) = viewport.egui_winit.as_mut()
|
||||
&& egui_winit.on_mouse_motion(delta)
|
||||
{
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
}
|
||||
@@ -584,7 +604,7 @@ impl WgpuWinitRunning<'_> {
|
||||
let mut frame_timer = crate::stopwatch::Stopwatch::new();
|
||||
frame_timer.start();
|
||||
|
||||
let (viewport_ui_cb, raw_input) = {
|
||||
let (viewport_ui_cb, raw_input, is_visible) = {
|
||||
profiling::scope!("Prepare");
|
||||
let mut shared_lock = shared.borrow_mut();
|
||||
|
||||
@@ -628,6 +648,8 @@ impl WgpuWinitRunning<'_> {
|
||||
};
|
||||
egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false);
|
||||
|
||||
let is_visible = viewport.info.visible().unwrap_or(true);
|
||||
|
||||
{
|
||||
profiling::scope!("set_window");
|
||||
pollster::block_on(painter.set_window(viewport_id, Some(Arc::clone(window))))?;
|
||||
@@ -648,14 +670,19 @@ impl WgpuWinitRunning<'_> {
|
||||
|
||||
painter.handle_screenshots(&mut raw_input.events);
|
||||
|
||||
(viewport_ui_cb, raw_input)
|
||||
(viewport_ui_cb, raw_input, is_visible)
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
// Runs the update, which could call immediate viewports,
|
||||
// so make sure we hold no locks here!
|
||||
let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
|
||||
let full_output = integration.update(
|
||||
app.as_mut(),
|
||||
viewport_ui_cb.as_deref(),
|
||||
raw_input,
|
||||
is_visible,
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -696,52 +723,58 @@ impl WgpuWinitRunning<'_> {
|
||||
|
||||
egui_winit.handle_platform_output(window, platform_output);
|
||||
|
||||
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
|
||||
let vsync_secs = if is_visible {
|
||||
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
|
||||
|
||||
let mut screenshot_commands = vec![];
|
||||
viewport.actions_requested.retain(|cmd| {
|
||||
if let ActionRequested::Screenshot(info) = cmd {
|
||||
screenshot_commands.push(info.clone());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
let vsync_secs = painter.paint_and_update_textures(
|
||||
viewport_id,
|
||||
pixels_per_point,
|
||||
app.clear_color(&egui_ctx.global_style().visuals),
|
||||
&clipped_primitives,
|
||||
&textures_delta,
|
||||
screenshot_commands,
|
||||
);
|
||||
let mut screenshot_commands = vec![];
|
||||
viewport.actions_requested.retain(|cmd| {
|
||||
if let ActionRequested::Screenshot(info) = cmd {
|
||||
screenshot_commands.push(info.clone());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
let vsync_secs = painter.paint_and_update_textures(
|
||||
viewport_id,
|
||||
pixels_per_point,
|
||||
app.clear_color(&egui_ctx.global_style().visuals),
|
||||
&clipped_primitives,
|
||||
&textures_delta,
|
||||
screenshot_commands,
|
||||
);
|
||||
|
||||
for action in viewport.actions_requested.drain(..) {
|
||||
match action {
|
||||
ActionRequested::Screenshot { .. } => {
|
||||
// already handled above
|
||||
}
|
||||
ActionRequested::Cut => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
|
||||
}
|
||||
ActionRequested::Copy => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
|
||||
}
|
||||
ActionRequested::Paste => {
|
||||
if let Some(contents) = egui_winit.clipboard_text() {
|
||||
let contents = contents.replace("\r\n", "\n");
|
||||
if !contents.is_empty() {
|
||||
egui_winit
|
||||
.egui_input_mut()
|
||||
.events
|
||||
.push(egui::Event::Paste(contents));
|
||||
for action in viewport.actions_requested.drain(..) {
|
||||
match action {
|
||||
ActionRequested::Screenshot { .. } => {
|
||||
// already handled above
|
||||
}
|
||||
ActionRequested::Cut => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
|
||||
}
|
||||
ActionRequested::Copy => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
|
||||
}
|
||||
ActionRequested::Paste => {
|
||||
if let Some(contents) = egui_winit.clipboard_text() {
|
||||
let contents = contents.replace("\r\n", "\n");
|
||||
if !contents.is_empty() {
|
||||
egui_winit
|
||||
.egui_input_mut()
|
||||
.events
|
||||
.push(egui::Event::Paste(contents));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
integration.post_rendering(window);
|
||||
integration.post_rendering(window);
|
||||
|
||||
vsync_secs
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect();
|
||||
|
||||
@@ -768,10 +801,12 @@ impl WgpuWinitRunning<'_> {
|
||||
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
|
||||
|
||||
if let Some(window) = window
|
||||
&& window.is_minimized() == Some(true)
|
||||
&& is_invisible_or_minimized(window)
|
||||
{
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
// On Windows, an invisible window also uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/7776
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
@@ -863,6 +898,14 @@ impl WgpuWinitRunning<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
winit::event::WindowEvent::Occluded(is_occluded) => {
|
||||
if let Some(viewport_id) = viewport_id
|
||||
&& let Some(viewport) = shared.viewports.get_mut(&viewport_id)
|
||||
{
|
||||
viewport.info.occluded = Some(*is_occluded);
|
||||
}
|
||||
}
|
||||
|
||||
winit::event::WindowEvent::CloseRequested => {
|
||||
if viewport_id == Some(ViewportId::ROOT) && integration.should_close() {
|
||||
log::debug!(
|
||||
|
||||
@@ -9,6 +9,14 @@ use egui::ViewportId;
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui_winit::accesskit_winit;
|
||||
|
||||
/// Returns `true` if the window is invisible or minimized.
|
||||
///
|
||||
/// These windows don't receive `RedrawRequested` events on Windows,
|
||||
/// so they need special handling to keep processing viewport commands.
|
||||
pub fn is_invisible_or_minimized(window: &Window) -> bool {
|
||||
window.is_visible() == Some(false) || window.is_minimized() == Some(true)
|
||||
}
|
||||
|
||||
/// Create an egui context, restoring it from storage if possible.
|
||||
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
|
||||
profiling::function_scope!();
|
||||
|
||||
@@ -274,13 +274,21 @@ impl AppRunner {
|
||||
|
||||
self.app.raw_input_hook(&self.egui_ctx, &mut raw_input);
|
||||
|
||||
let is_visible = raw_input
|
||||
.viewports
|
||||
.get(&egui::ViewportId::ROOT)
|
||||
.and_then(|v| v.visible())
|
||||
.unwrap_or(true);
|
||||
|
||||
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
||||
self.app.logic(ui.ctx(), &mut self.frame);
|
||||
|
||||
#[expect(deprecated)]
|
||||
self.app.update(ui.ctx(), &mut self.frame);
|
||||
if is_visible {
|
||||
#[expect(deprecated)]
|
||||
self.app.update(ui.ctx(), &mut self.frame);
|
||||
|
||||
self.app.ui(ui, &mut self.frame);
|
||||
self.app.ui(ui, &mut self.frame);
|
||||
}
|
||||
});
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
@@ -311,8 +319,10 @@ impl AppRunner {
|
||||
}
|
||||
|
||||
self.handle_platform_output(platform_output);
|
||||
self.textures_delta.append(textures_delta);
|
||||
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
|
||||
if is_visible {
|
||||
self.textures_delta.append(textures_delta);
|
||||
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint the results of the last call to [`Self::logic`].
|
||||
|
||||
@@ -31,11 +31,18 @@ impl WebInput {
|
||||
time: Some(super::now_sec()),
|
||||
..self.raw.take()
|
||||
};
|
||||
raw_input
|
||||
let viewport = raw_input
|
||||
.viewports
|
||||
.entry(egui::ViewportId::ROOT)
|
||||
.or_default()
|
||||
.native_pixels_per_point = Some(super::native_pixels_per_point());
|
||||
.or_default();
|
||||
viewport.native_pixels_per_point = Some(super::native_pixels_per_point());
|
||||
|
||||
// A hidden browser tab is effectively occluded.
|
||||
let hidden = web_sys::window()
|
||||
.and_then(|w| w.document())
|
||||
.is_some_and(|doc| doc.hidden());
|
||||
viewport.occluded = Some(hidden);
|
||||
|
||||
raw_input
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ mod web_painter_wgpu;
|
||||
pub use backend::*;
|
||||
|
||||
use egui::Theme;
|
||||
use js_sys::Object;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{Document, MediaQueryList, Node};
|
||||
|
||||
@@ -370,5 +371,5 @@ pub fn percent_decode(s: &str) -> String {
|
||||
|
||||
/// Are we running inside the Safari browser?
|
||||
pub fn is_safari_browser() -> bool {
|
||||
web_sys::window().is_some_and(|window| window.has_own_property(&JsValue::from("safari")))
|
||||
web_sys::window().is_some_and(|window| Object::has_own(&window, &JsValue::from("safari")))
|
||||
}
|
||||
|
||||
@@ -15,13 +15,32 @@ pub(crate) struct WebPainterWgpu {
|
||||
surface: wgpu::Surface<'static>,
|
||||
surface_configuration: wgpu::SurfaceConfiguration,
|
||||
render_state: Option<RenderState>,
|
||||
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
|
||||
on_surface_status: Arc<dyn Fn(&wgpu::CurrentSurfaceTexture) -> SurfaceErrorAction>,
|
||||
depth_stencil_format: Option<wgpu::TextureFormat>,
|
||||
depth_texture_view: Option<wgpu::TextureView>,
|
||||
screen_capture_state: Option<CaptureState>,
|
||||
capture_tx: CaptureSender,
|
||||
capture_rx: CaptureReceiver,
|
||||
ctx: egui::Context,
|
||||
needs_reconfigure: bool,
|
||||
}
|
||||
|
||||
/// Owned web display handle that is `Send + Sync`.
|
||||
///
|
||||
/// `DisplayHandle` from `raw-window-handle` is `!Send`/`!Sync` because the enum
|
||||
/// contains platform variants with raw pointers. On web the handle is always empty,
|
||||
/// so this wrapper is safe.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Clone, Debug)]
|
||||
struct WebDisplay;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl egui_wgpu::wgpu::rwh::HasDisplayHandle for WebDisplay {
|
||||
fn display_handle(
|
||||
&self,
|
||||
) -> Result<egui_wgpu::wgpu::rwh::DisplayHandle<'_>, egui_wgpu::wgpu::rwh::HandleError> {
|
||||
Ok(egui_wgpu::wgpu::rwh::DisplayHandle::web())
|
||||
}
|
||||
}
|
||||
|
||||
impl WebPainterWgpu {
|
||||
@@ -63,7 +82,17 @@ impl WebPainterWgpu {
|
||||
) -> Result<Self, String> {
|
||||
log::debug!("Creating wgpu painter");
|
||||
|
||||
let instance = options.wgpu_options.wgpu_setup.new_instance().await;
|
||||
// Inject the display handle into the wgpu setup so that wgpu can create surfaces on WebGL.
|
||||
let mut wgpu_options = options.wgpu_options.clone();
|
||||
if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup
|
||||
&& create_new.display_handle.is_none()
|
||||
{
|
||||
// Force WebGL, useful for quick & dirty testing:
|
||||
//create_new.instance_descriptor.backends = wgpu::Backends::GL;
|
||||
create_new.display_handle = Some(Box::new(WebDisplay));
|
||||
}
|
||||
|
||||
let instance = wgpu_options.wgpu_setup.new_instance().await;
|
||||
let surface = instance
|
||||
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
|
||||
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
|
||||
@@ -71,7 +100,7 @@ impl WebPainterWgpu {
|
||||
let depth_stencil_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);
|
||||
|
||||
let render_state = RenderState::create(
|
||||
&options.wgpu_options,
|
||||
&wgpu_options,
|
||||
&instance,
|
||||
Some(&surface),
|
||||
egui_wgpu::RendererOptions {
|
||||
@@ -89,7 +118,7 @@ impl WebPainterWgpu {
|
||||
|
||||
let surface_configuration = wgpu::SurfaceConfiguration {
|
||||
format: render_state.target_format,
|
||||
present_mode: options.wgpu_options.present_mode,
|
||||
present_mode: wgpu_options.present_mode,
|
||||
view_formats: vec![render_state.target_format],
|
||||
..default_configuration
|
||||
};
|
||||
@@ -105,11 +134,12 @@ impl WebPainterWgpu {
|
||||
surface_configuration,
|
||||
depth_stencil_format,
|
||||
depth_texture_view: None,
|
||||
on_surface_error: Arc::clone(&options.wgpu_options.on_surface_error) as _,
|
||||
on_surface_status: Arc::clone(&wgpu_options.on_surface_status) as _,
|
||||
screen_capture_state: None,
|
||||
capture_tx,
|
||||
capture_rx,
|
||||
ctx,
|
||||
needs_reconfigure: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -195,18 +225,28 @@ impl WebPainter for WebPainterWgpu {
|
||||
);
|
||||
}
|
||||
|
||||
if self.needs_reconfigure {
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
self.needs_reconfigure = false;
|
||||
}
|
||||
|
||||
let output_frame = match self.surface.get_current_texture() {
|
||||
Ok(frame) => frame,
|
||||
Err(err) => match (*self.on_surface_error)(err) {
|
||||
SurfaceErrorAction::RecreateSurface => {
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
return Ok(());
|
||||
wgpu::CurrentSurfaceTexture::Success(frame) => frame,
|
||||
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
|
||||
self.needs_reconfigure = true;
|
||||
frame
|
||||
}
|
||||
other => {
|
||||
match (*self.on_surface_status)(&other) {
|
||||
SurfaceErrorAction::RecreateSurface => {
|
||||
self.surface
|
||||
.configure(&render_state.device, &self.surface_configuration);
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame => {}
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame => {
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
@@ -268,6 +308,7 @@ impl WebPainter for WebPainterWgpu {
|
||||
label: Some("egui_render"),
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
|
||||
// Forgetting the pass' lifetime means that we are no longer compile-time protected from
|
||||
@@ -280,15 +321,13 @@ impl WebPainter for WebPainterWgpu {
|
||||
);
|
||||
}
|
||||
|
||||
let mut capture_buffer = None;
|
||||
|
||||
if capture && let Some(capture_state) = &mut self.screen_capture_state {
|
||||
capture_buffer = Some(capture_state.copy_textures(
|
||||
&render_state.device,
|
||||
&output_frame,
|
||||
&mut encoder,
|
||||
));
|
||||
}
|
||||
let capture_buffer = if capture
|
||||
&& let Some(capture_state) = &mut self.screen_capture_state
|
||||
{
|
||||
Some(capture_state.copy_textures(&render_state.device, &output_frame, &mut encoder))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Some((output_frame, capture_buffer))
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user