1
0
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:
lucasmerlin
2026-03-24 20:18:46 +01:00
257 changed files with 4285 additions and 2054 deletions

View File

@@ -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.
///

View File

@@ -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;

View File

@@ -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);
}
}
}
});

View File

@@ -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!(

View File

@@ -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)

View File

@@ -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!(

View File

@@ -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!();

View File

@@ -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`].

View File

@@ -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
}

View File

@@ -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")))
}

View File

@@ -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))
};