mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 05:10:03 -04:00
Merge branch 'main' into common-panels
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
use std::any::Any;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub use crate::native::winit_integration::UserEvent;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -22,7 +22,7 @@ use raw_window_handle::{
|
||||
use static_assertions::assert_not_impl_any;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub use winit::{event_loop::EventLoopBuilder, window::WindowAttributes};
|
||||
|
||||
/// Hook into the building of an event loop before it is run
|
||||
@@ -30,7 +30,7 @@ pub use winit::{event_loop::EventLoopBuilder, window::WindowAttributes};
|
||||
/// You can configure any platform specific details required on top of the default configuration
|
||||
/// done by `EFrame`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)>;
|
||||
|
||||
/// Hook into the building of a the native window.
|
||||
@@ -38,7 +38,7 @@ pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)
|
||||
/// You can configure any platform specific details required on top of the default configuration
|
||||
/// done by `eframe`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub type WindowBuilderHook = Box<dyn FnOnce(egui::ViewportBuilder) -> egui::ViewportBuilder>;
|
||||
|
||||
type DynError = Box<dyn std::error::Error + Send + Sync>;
|
||||
@@ -79,7 +79,7 @@ pub struct CreationContext<'s> {
|
||||
/// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`].
|
||||
///
|
||||
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
|
||||
/// Raw platform window handle
|
||||
@@ -91,7 +91,7 @@ pub struct CreationContext<'s> {
|
||||
pub(crate) raw_display_handle: Result<RawDisplayHandle, HandleError>,
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl HasWindowHandle for CreationContext<'_> {
|
||||
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
||||
@@ -100,7 +100,7 @@ impl HasWindowHandle for CreationContext<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl HasDisplayHandle for CreationContext<'_> {
|
||||
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
||||
@@ -121,7 +121,7 @@ impl CreationContext<'_> {
|
||||
gl: None,
|
||||
#[cfg(feature = "glow")]
|
||||
get_proc_address: None,
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state: None,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
raw_window_handle: Err(HandleError::NotSupported),
|
||||
@@ -133,7 +133,7 @@ impl CreationContext<'_> {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
|
||||
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe).
|
||||
pub trait App {
|
||||
/// Called each time the UI needs repainting, which may be many times per second.
|
||||
///
|
||||
@@ -317,7 +317,7 @@ pub struct NativeOptions {
|
||||
pub hardware_acceleration: HardwareAcceleration,
|
||||
|
||||
/// What rendering backend to use.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub renderer: Renderer,
|
||||
|
||||
/// This controls what happens when you close the main eframe window.
|
||||
@@ -340,7 +340,7 @@ pub struct NativeOptions {
|
||||
/// event loop before it is run.
|
||||
///
|
||||
/// Note: A [`NativeOptions`] clone will not include any `event_loop_builder` hook.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub event_loop_builder: Option<EventLoopBuilderHook>,
|
||||
|
||||
/// Hook into the building of a window.
|
||||
@@ -349,7 +349,7 @@ pub struct NativeOptions {
|
||||
/// window appearance.
|
||||
///
|
||||
/// Note: A [`NativeOptions`] clone will not include any `window_builder` hook.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub window_builder: Option<WindowBuilderHook>,
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
@@ -367,7 +367,7 @@ pub struct NativeOptions {
|
||||
pub centered: bool,
|
||||
|
||||
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub wgpu_options: egui_wgpu::WgpuConfiguration,
|
||||
|
||||
/// Controls whether or not the native window position and size will be
|
||||
@@ -404,13 +404,13 @@ impl Clone for NativeOptions {
|
||||
Self {
|
||||
viewport: self.viewport.clone(),
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
event_loop_builder: None, // Skip any builder callbacks if cloning
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
window_builder: None, // Skip any builder callbacks if cloning
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_options: self.wgpu_options.clone(),
|
||||
|
||||
persistence_path: self.persistence_path.clone(),
|
||||
@@ -435,15 +435,15 @@ impl Default for NativeOptions {
|
||||
stencil_buffer: 0,
|
||||
hardware_acceleration: HardwareAcceleration::Preferred,
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
renderer: Renderer::default(),
|
||||
|
||||
run_and_return: true,
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
event_loop_builder: None,
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
window_builder: None,
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
@@ -451,7 +451,7 @@ impl Default for NativeOptions {
|
||||
|
||||
centered: false,
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
|
||||
|
||||
persist_window: true,
|
||||
@@ -471,6 +471,10 @@ impl Default for NativeOptions {
|
||||
/// Options when using `eframe` in a web page.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct WebOptions {
|
||||
/// What rendering backend to use.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub renderer: Renderer,
|
||||
|
||||
/// Sets the number of bits in the depth buffer.
|
||||
///
|
||||
/// `egui` doesn't need the depth buffer, so the default value is 0.
|
||||
@@ -484,7 +488,7 @@ pub struct WebOptions {
|
||||
pub webgl_context_option: WebGlContextOption,
|
||||
|
||||
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub wgpu_options: egui_wgpu::WgpuConfiguration,
|
||||
|
||||
/// Controls whether to apply dithering to minimize banding artifacts.
|
||||
@@ -499,27 +503,43 @@ pub struct WebOptions {
|
||||
/// If the web event corresponding to an egui event should be propagated
|
||||
/// to the rest of the web page.
|
||||
///
|
||||
/// The default is `false`, meaning
|
||||
/// The default is `true`, meaning
|
||||
/// [`stopPropagation`](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation)
|
||||
/// is called on every event.
|
||||
pub should_propagate_event: Box<dyn Fn(&egui::Event) -> bool>,
|
||||
/// is called on every event, and the event is not propagated to the rest of the web page.
|
||||
pub should_stop_propagation: Box<dyn Fn(&egui::Event) -> bool>,
|
||||
|
||||
/// Whether the web event corresponding to an egui event should have `prevent_default` called
|
||||
/// on it or not.
|
||||
///
|
||||
/// Defaults to true.
|
||||
pub should_prevent_default: Box<dyn Fn(&egui::Event) -> bool>,
|
||||
|
||||
/// Maximum rate at which to repaint. This can be used to artificially reduce the repaint rate below
|
||||
/// vsync in order to save resources.
|
||||
pub max_fps: Option<u32>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl Default for WebOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
renderer: Renderer::default(),
|
||||
|
||||
depth_buffer: 0,
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
webgl_context_option: WebGlContextOption::BestFirst,
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
|
||||
|
||||
dithering: true,
|
||||
|
||||
should_propagate_event: Box::new(|_| false),
|
||||
should_stop_propagation: Box::new(|_| true),
|
||||
should_prevent_default: Box::new(|_| true),
|
||||
|
||||
max_fps: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -548,7 +568,7 @@ pub enum WebGlContextOption {
|
||||
/// What rendering backend to use.
|
||||
///
|
||||
/// You need to enable the "glow" and "wgpu" features to have a choice.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
||||
@@ -558,47 +578,49 @@ pub enum Renderer {
|
||||
Glow,
|
||||
|
||||
/// Use [`egui_wgpu`] renderer for [`wgpu`](https://github.com/gfx-rs/wgpu).
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Wgpu,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
impl Default for Renderer {
|
||||
fn default() -> Self {
|
||||
#[cfg(not(feature = "glow"))]
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
compile_error!("eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'");
|
||||
#[cfg(not(feature = "wgpu_no_default_features"))]
|
||||
compile_error!(
|
||||
"eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'"
|
||||
);
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
#[cfg(not(feature = "wgpu_no_default_features"))]
|
||||
return Self::Glow;
|
||||
|
||||
#[cfg(not(feature = "glow"))]
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
return Self::Wgpu;
|
||||
|
||||
// By default, only the `glow` feature is enabled, so if the user added `wgpu` to the feature list
|
||||
// they probably wanted to use wgpu:
|
||||
// It's weird that the user has enabled both glow and wgpu,
|
||||
// but let's pick the better of the two (wgpu):
|
||||
#[cfg(feature = "glow")]
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
return Self::Wgpu;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
impl std::fmt::Display for Renderer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
#[cfg(feature = "glow")]
|
||||
Self::Glow => "glow".fmt(f),
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Self::Wgpu => "wgpu".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
impl std::str::FromStr for Renderer {
|
||||
type Err = String;
|
||||
|
||||
@@ -607,10 +629,12 @@ impl std::str::FromStr for Renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
"glow" => Ok(Self::Glow),
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
"wgpu" => Ok(Self::Wgpu),
|
||||
|
||||
_ => Err(format!("eframe renderer {name:?} is not available. Make sure that the corresponding eframe feature is enabled."))
|
||||
_ => Err(format!(
|
||||
"eframe renderer {name:?} is not available. Make sure that the corresponding eframe feature is enabled."
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -638,7 +662,7 @@ pub struct Frame {
|
||||
Option<Box<dyn FnMut(glow::Texture) -> egui::TextureId>>,
|
||||
|
||||
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
#[doc(hidden)]
|
||||
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
|
||||
@@ -655,7 +679,7 @@ pub struct Frame {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
assert_not_impl_any!(Frame: Clone);
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl HasWindowHandle for Frame {
|
||||
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
||||
@@ -664,7 +688,7 @@ impl HasWindowHandle for Frame {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl HasDisplayHandle for Frame {
|
||||
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
||||
@@ -688,7 +712,7 @@ impl Frame {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
raw_window_handle: Err(HandleError::NotSupported),
|
||||
storage: None,
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state: None,
|
||||
}
|
||||
}
|
||||
@@ -696,7 +720,7 @@ impl Frame {
|
||||
/// True if you are in a web environment.
|
||||
///
|
||||
/// Equivalent to `cfg!(target_arch = "wasm32")`
|
||||
#[allow(clippy::unused_self)]
|
||||
#[expect(clippy::unused_self)]
|
||||
pub fn is_web(&self) -> bool {
|
||||
cfg!(target_arch = "wasm32")
|
||||
}
|
||||
@@ -747,7 +771,7 @@ impl Frame {
|
||||
/// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`].
|
||||
///
|
||||
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub fn wgpu_render_state(&self) -> Option<&egui_wgpu::RenderState> {
|
||||
self.wgpu_render_state.as_ref()
|
||||
}
|
||||
@@ -882,16 +906,15 @@ pub trait Storage {
|
||||
#[cfg(feature = "ron")]
|
||||
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
|
||||
profiling::function_scope!(key);
|
||||
storage
|
||||
.get_string(key)
|
||||
.and_then(|value| match ron::from_str(&value) {
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => {
|
||||
// This happens on when we break the format, e.g. when updating egui.
|
||||
log::debug!("Failed to decode RON: {err}");
|
||||
None
|
||||
}
|
||||
})
|
||||
let value = storage.get_string(key)?;
|
||||
match ron::from_str(&value) {
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => {
|
||||
// This happens on when we break the format, e.g. when updating egui.
|
||||
log::debug!("Failed to decode RON: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key.
|
||||
@@ -900,7 +923,7 @@ pub fn set_value<T: serde::Serialize>(storage: &mut dyn Storage, key: &str, valu
|
||||
profiling::function_scope!(key);
|
||||
match ron::ser::to_string(value) {
|
||||
Ok(string) => storage.set_string(key, string),
|
||||
Err(err) => log::error!("eframe failed to encode data using ron: {}", err),
|
||||
Err(err) => log::error!("eframe failed to encode data using ron: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! If you are planning to write an app for web or native,
|
||||
//! and want to use [`egui`] for everything, then `eframe` is for you!
|
||||
//!
|
||||
//! To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples).
|
||||
//! To get started, see the [examples](https://github.com/emilk/egui/tree/main/examples).
|
||||
//! To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
|
||||
//!
|
||||
//! In short, you implement [`App`] (especially [`App::update`]) and then
|
||||
@@ -69,7 +69,7 @@
|
||||
//! #[wasm_bindgen]
|
||||
//! impl WebHandle {
|
||||
//! /// Installs a panic hook, then returns.
|
||||
//! #[allow(clippy::new_without_default)]
|
||||
//! #[expect(clippy::new_without_default)]
|
||||
//! #[wasm_bindgen(constructor)]
|
||||
//! pub fn new() -> Self {
|
||||
//! // Redirect [`log`] message to `console.log` and friends:
|
||||
@@ -144,13 +144,22 @@
|
||||
#![warn(missing_docs)] // let's keep eframe well-documented
|
||||
#![allow(clippy::needless_doctest_main)]
|
||||
|
||||
// Limitation imposed by `accesskit_winit`:
|
||||
// https://github.com/AccessKit/accesskit/tree/accesskit-v0.18.0/platforms/winit#android-activity-compatibility`
|
||||
#[cfg(all(
|
||||
target_os = "android",
|
||||
feature = "accesskit",
|
||||
feature = "android-native-activity"
|
||||
))]
|
||||
compile_error!("`accesskit` feature is only available with `android-game-activity`");
|
||||
|
||||
// Re-export all useful libraries:
|
||||
pub use {egui, egui::emath, egui::epaint};
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
pub use {egui_glow, glow};
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub use {egui_wgpu, wgpu};
|
||||
|
||||
mod epi;
|
||||
@@ -179,11 +188,19 @@ pub use web::{WebLogger, WebRunner};
|
||||
// When compiling natively
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
mod native;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub use native::run::EframeWinitApplication;
|
||||
|
||||
#[cfg(not(any(target_arch = "wasm32", target_os = "ios")))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub use native::run::EframePumpStatus;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
#[cfg(feature = "persistence")]
|
||||
pub use native::file_storage::storage_dir;
|
||||
|
||||
@@ -192,7 +209,7 @@ pub mod icon_data;
|
||||
|
||||
/// This is how you start a native (desktop) app.
|
||||
///
|
||||
/// The first argument is name of your app, which is a an identifier
|
||||
/// The first argument is name of your app, which is an identifier
|
||||
/// used for the save location of persistence (see [`App::save`]).
|
||||
/// It is also used as the application id on wayland.
|
||||
/// If you set no title on the viewport, the app id will be used
|
||||
@@ -235,13 +252,113 @@ pub mod icon_data;
|
||||
/// # Errors
|
||||
/// This function can fail if we fail to set up a graphics context.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)]
|
||||
pub fn run_native(
|
||||
app_name: &str,
|
||||
mut native_options: NativeOptions,
|
||||
app_creator: AppCreator<'_>,
|
||||
) -> Result {
|
||||
let renderer = init_native(app_name, &mut native_options);
|
||||
|
||||
match renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
Renderer::Glow => {
|
||||
log::debug!("Using the glow renderer");
|
||||
native::run::run_glow(app_name, native_options, app_creator)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Renderer::Wgpu => {
|
||||
log::debug!("Using the wgpu renderer");
|
||||
native::run::run_wgpu(app_name, native_options, app_creator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a proxy for your native eframe application to run on your own event loop.
|
||||
///
|
||||
/// See `run_native` for details about `app_name`.
|
||||
///
|
||||
/// Call from `fn main` like this:
|
||||
/// ``` no_run
|
||||
/// use eframe::{egui, UserEvent};
|
||||
/// use winit::event_loop::{ControlFlow, EventLoop};
|
||||
///
|
||||
/// fn main() -> eframe::Result {
|
||||
/// let native_options = eframe::NativeOptions::default();
|
||||
/// let eventloop = EventLoop::<UserEvent>::with_user_event().build()?;
|
||||
/// eventloop.set_control_flow(ControlFlow::Poll);
|
||||
///
|
||||
/// let mut winit_app = eframe::create_native(
|
||||
/// "MyExtApp",
|
||||
/// native_options,
|
||||
/// Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc)))),
|
||||
/// &eventloop,
|
||||
/// );
|
||||
///
|
||||
/// eventloop.run_app(&mut winit_app)?;
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Default)]
|
||||
/// struct MyEguiApp {}
|
||||
///
|
||||
/// impl MyEguiApp {
|
||||
/// fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
/// Self::default()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl eframe::App for MyEguiApp {
|
||||
/// fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
/// egui::CentralPanel::default().show(ctx, |ui| {
|
||||
/// ui.heading("Hello World!");
|
||||
/// });
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// See the `external_eventloop` example for a more complete example.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub fn create_native<'a>(
|
||||
app_name: &str,
|
||||
mut native_options: NativeOptions,
|
||||
app_creator: AppCreator<'a>,
|
||||
event_loop: &winit::event_loop::EventLoop<UserEvent>,
|
||||
) -> EframeWinitApplication<'a> {
|
||||
let renderer = init_native(app_name, &mut native_options);
|
||||
|
||||
match renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
Renderer::Glow => {
|
||||
log::debug!("Using the glow renderer");
|
||||
EframeWinitApplication::new(native::run::create_glow(
|
||||
app_name,
|
||||
native_options,
|
||||
app_creator,
|
||||
event_loop,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Renderer::Wgpu => {
|
||||
log::debug!("Using the wgpu renderer");
|
||||
EframeWinitApplication::new(native::run::create_wgpu(
|
||||
app_name,
|
||||
native_options,
|
||||
app_creator,
|
||||
event_loop,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer {
|
||||
#[cfg(not(feature = "__screenshot"))]
|
||||
assert!(
|
||||
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
||||
@@ -254,28 +371,16 @@ pub fn run_native(
|
||||
|
||||
let renderer = native_options.renderer;
|
||||
|
||||
#[cfg(all(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(all(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
{
|
||||
match renderer {
|
||||
match native_options.renderer {
|
||||
Renderer::Glow => "glow",
|
||||
Renderer::Wgpu => "wgpu",
|
||||
};
|
||||
log::info!("Both the glow and wgpu renderers are available. Using {renderer}.");
|
||||
}
|
||||
|
||||
match renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
Renderer::Glow => {
|
||||
log::debug!("Using the glow renderer");
|
||||
native::run::run_glow(app_name, native_options, app_creator)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
Renderer::Wgpu => {
|
||||
log::debug!("Using the wgpu renderer");
|
||||
native::run::run_wgpu(app_name, native_options, app_creator)
|
||||
}
|
||||
}
|
||||
renderer
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -315,7 +420,7 @@ pub fn run_native(
|
||||
/// # Errors
|
||||
/// This function can fail if we fail to set up a graphics context.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub fn run_simple_native(
|
||||
app_name: &str,
|
||||
native_options: NativeOptions,
|
||||
@@ -367,7 +472,7 @@ pub enum Error {
|
||||
OpenGL(egui_glow::PainterError),
|
||||
|
||||
/// An error from [`wgpu`].
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Wgpu(egui_wgpu::WgpuError),
|
||||
}
|
||||
|
||||
@@ -405,7 +510,7 @@ impl From<egui_glow::PainterError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
impl From<egui_wgpu::WgpuError> for Error {
|
||||
#[inline]
|
||||
fn from(err: egui_wgpu::WgpuError) -> Self {
|
||||
@@ -446,7 +551,7 @@ impl std::fmt::Display for Error {
|
||||
write!(f, "egui_glow: {err}")
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Self::Wgpu(err) => {
|
||||
write!(f, "WGPU error: {err}")
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ pub struct AppTitleIconSetter {
|
||||
|
||||
impl AppTitleIconSetter {
|
||||
pub fn new(title: String, mut icon_data: Option<Arc<IconData>>) -> Self {
|
||||
if let Some(icon) = &icon_data {
|
||||
if **icon == IconData::default() {
|
||||
icon_data = None;
|
||||
}
|
||||
if let Some(icon) = &icon_data
|
||||
&& **icon == IconData::default()
|
||||
{
|
||||
icon_data = None;
|
||||
}
|
||||
|
||||
Self {
|
||||
@@ -47,7 +47,7 @@ enum AppIconStatus {
|
||||
NotSetTryAgain,
|
||||
|
||||
/// We successfully set the icon and it should be visible now.
|
||||
#[allow(dead_code)] // Not used on Linux
|
||||
#[allow(dead_code, clippy::allow_attributes)] // Not used on Linux
|
||||
Set,
|
||||
}
|
||||
|
||||
@@ -71,16 +71,20 @@ fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconSta
|
||||
#[cfg(target_os = "macos")]
|
||||
return set_title_and_icon_mac(_title, _icon_data);
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
#[allow(unreachable_code, clippy::allow_attributes)]
|
||||
AppIconStatus::NotSetIgnored
|
||||
}
|
||||
|
||||
/// Set icon for Windows applications.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
use crate::icon_data::IconDataExt as _;
|
||||
use winapi::um::winuser;
|
||||
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow;
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
CreateIconFromResourceEx, GetSystemMetrics, HICON, ICON_BIG, ICON_SMALL, LR_DEFAULTCOLOR,
|
||||
SM_CXICON, SM_CXSMICON, SendMessageW, WM_SETICON,
|
||||
};
|
||||
|
||||
// We would get fairly far already with winit's `set_window_icon` (which is exposed to eframe) actually!
|
||||
// However, it only sets ICON_SMALL, i.e. doesn't allow us to set a higher resolution icon for the task bar.
|
||||
@@ -92,16 +96,13 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
// * using undocumented SetConsoleIcon method (successfully queried via GetProcAddress)
|
||||
|
||||
// SAFETY: WinApi function without side-effects.
|
||||
let window_handle = unsafe { winuser::GetActiveWindow() };
|
||||
let window_handle = unsafe { GetActiveWindow() };
|
||||
if window_handle.is_null() {
|
||||
// The Window isn't available yet. Try again later!
|
||||
return AppIconStatus::NotSetTryAgain;
|
||||
}
|
||||
|
||||
fn create_hicon_with_scale(
|
||||
unscaled_image: &image::RgbaImage,
|
||||
target_size: i32,
|
||||
) -> winapi::shared::windef::HICON {
|
||||
fn create_hicon_with_scale(unscaled_image: &image::RgbaImage, target_size: i32) -> HICON {
|
||||
let image_scaled = image::imageops::resize(
|
||||
unscaled_image,
|
||||
target_size as _,
|
||||
@@ -127,14 +128,14 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
|
||||
// SAFETY: Creating an HICON which should be readonly on our data.
|
||||
unsafe {
|
||||
winuser::CreateIconFromResourceEx(
|
||||
CreateIconFromResourceEx(
|
||||
image_scaled_bytes.as_mut_ptr(),
|
||||
image_scaled_bytes.len() as u32,
|
||||
1, // Means this is an icon, not a cursor.
|
||||
0x00030000, // Version number of the HICON
|
||||
target_size, // Note that this method can scale, but it does so *very* poorly. So let's avoid that!
|
||||
target_size,
|
||||
winuser::LR_DEFAULTCOLOR,
|
||||
LR_DEFAULTCOLOR,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -155,7 +156,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
// Note that ICON_SMALL may be used even if we don't render a title bar as it may be used in alt+tab!
|
||||
{
|
||||
// SAFETY: WinAPI getter function with no known side effects.
|
||||
let icon_size_big = unsafe { winuser::GetSystemMetrics(winuser::SM_CXICON) };
|
||||
let icon_size_big = unsafe { GetSystemMetrics(SM_CXICON) };
|
||||
let icon_big = create_hicon_with_scale(&unscaled_image, icon_size_big);
|
||||
if icon_big.is_null() {
|
||||
log::warn!("Failed to create HICON (for big icon) from embedded png data.");
|
||||
@@ -163,10 +164,10 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
} else {
|
||||
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
||||
unsafe {
|
||||
winuser::SendMessageW(
|
||||
SendMessageW(
|
||||
window_handle,
|
||||
winuser::WM_SETICON,
|
||||
winuser::ICON_BIG as usize,
|
||||
WM_SETICON,
|
||||
ICON_BIG as usize,
|
||||
icon_big as isize,
|
||||
);
|
||||
}
|
||||
@@ -174,7 +175,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
}
|
||||
{
|
||||
// SAFETY: WinAPI getter function with no known side effects.
|
||||
let icon_size_small = unsafe { winuser::GetSystemMetrics(winuser::SM_CXSMICON) };
|
||||
let icon_size_small = unsafe { GetSystemMetrics(SM_CXSMICON) };
|
||||
let icon_small = create_hicon_with_scale(&unscaled_image, icon_size_small);
|
||||
if icon_small.is_null() {
|
||||
log::warn!("Failed to create HICON (for small icon) from embedded png data.");
|
||||
@@ -182,10 +183,10 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
} else {
|
||||
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
||||
unsafe {
|
||||
winuser::SendMessageW(
|
||||
SendMessageW(
|
||||
window_handle,
|
||||
winuser::WM_SETICON,
|
||||
winuser::ICON_SMALL as usize,
|
||||
WM_SETICON,
|
||||
ICON_SMALL as usize,
|
||||
icon_small as isize,
|
||||
);
|
||||
}
|
||||
@@ -198,20 +199,25 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
|
||||
/// Set icon & app title for `MacOS` applications.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
|
||||
use crate::icon_data::IconDataExt as _;
|
||||
profiling::function_scope!();
|
||||
|
||||
use objc2::ClassType;
|
||||
use objc2::ClassType as _;
|
||||
use objc2_app_kit::{NSApplication, NSImage};
|
||||
use objc2_foundation::{NSData, NSString};
|
||||
use objc2_foundation::NSString;
|
||||
|
||||
let png_bytes = if let Some(icon_data) = icon_data {
|
||||
match icon_data.to_png_bytes() {
|
||||
Ok(png_bytes) => Some(png_bytes),
|
||||
// Do NOT use png even though creating `NSImage` from it is much easier than from raw images data!
|
||||
//
|
||||
// Some MacOS versions have a bug where creating an `NSImage` from a png will cause it to load an arbitrary `libpng.dylib`.
|
||||
// If this dylib isn't the right version, the application will crash with SIGBUS.
|
||||
// For details see https://github.com/emilk/egui/issues/7155
|
||||
let image = if let Some(icon_data) = icon_data {
|
||||
match icon_data.to_image() {
|
||||
Ok(image) => Some(image),
|
||||
Err(err) => {
|
||||
log::warn!("Failed to convert IconData to png: {err}");
|
||||
log::warn!("Failed to read icon data: {err}");
|
||||
return AppIconStatus::NotSetIgnored;
|
||||
}
|
||||
}
|
||||
@@ -220,7 +226,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
|
||||
};
|
||||
|
||||
// TODO(madsmtm): Move this into `objc2-app-kit`
|
||||
extern "C" {
|
||||
unsafe extern "C" {
|
||||
static NSApp: Option<&'static NSApplication>;
|
||||
}
|
||||
|
||||
@@ -231,25 +237,50 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
|
||||
return AppIconStatus::NotSetIgnored;
|
||||
};
|
||||
|
||||
if let Some(png_bytes) = png_bytes {
|
||||
let data = NSData::from_vec(png_bytes);
|
||||
if let Some(image) = image {
|
||||
use objc2_app_kit::{NSBitmapImageRep, NSDeviceRGBColorSpace};
|
||||
use objc2_foundation::NSSize;
|
||||
|
||||
log::trace!("NSImage::initWithData…");
|
||||
let app_icon = NSImage::initWithData(NSImage::alloc(), &data);
|
||||
log::trace!(
|
||||
"NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel"
|
||||
);
|
||||
let Some(image_rep) = NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(
|
||||
NSBitmapImageRep::alloc(),
|
||||
[image.as_raw().as_ptr().cast_mut()].as_mut_ptr(),
|
||||
image.width() as isize,
|
||||
image.height() as isize,
|
||||
8, // bits per sample
|
||||
4, // samples per pixel
|
||||
true, // has alpha
|
||||
false, // is not planar
|
||||
NSDeviceRGBColorSpace,
|
||||
(image.width() * 4) as isize, // bytes per row
|
||||
32 // bits per pixel
|
||||
) else {
|
||||
log::warn!("Failed to create NSBitmapImageRep from app icon data.");
|
||||
return AppIconStatus::NotSetIgnored;
|
||||
};
|
||||
|
||||
log::trace!("NSImage::initWithSize");
|
||||
let app_icon = NSImage::initWithSize(
|
||||
NSImage::alloc(),
|
||||
NSSize::new(image.width() as f64, image.height() as f64),
|
||||
);
|
||||
log::trace!("NSImage::addRepresentation");
|
||||
app_icon.addRepresentation(&image_rep);
|
||||
|
||||
profiling::scope!("setApplicationIconImage_");
|
||||
log::trace!("setApplicationIconImage…");
|
||||
app.setApplicationIconImage(app_icon.as_deref());
|
||||
app.setApplicationIconImage(Some(&app_icon));
|
||||
}
|
||||
|
||||
// Change the title in the top bar - for python processes this would be again "python" otherwise.
|
||||
if let Some(main_menu) = app.mainMenu() {
|
||||
if let Some(item) = main_menu.itemAtIndex(0) {
|
||||
if let Some(app_menu) = item.submenu() {
|
||||
profiling::scope!("setTitle_");
|
||||
app_menu.setTitle(&NSString::from_str(title));
|
||||
}
|
||||
}
|
||||
if let Some(main_menu) = app.mainMenu()
|
||||
&& let Some(item) = main_menu.itemAtIndex(0)
|
||||
&& let Some(app_menu) = item.submenu()
|
||||
{
|
||||
profiling::scope!("setTitle_");
|
||||
app_menu.setTitle(&NSString::from_str(title));
|
||||
}
|
||||
|
||||
// The title in the Dock apparently can't be changed.
|
||||
|
||||
@@ -52,14 +52,13 @@ pub fn viewport_builder(
|
||||
viewport_builder = viewport_builder.with_position(pos);
|
||||
}
|
||||
|
||||
if clamp_size_to_monitor_size {
|
||||
if let Some(initial_window_size) = viewport_builder.inner_size {
|
||||
let initial_window_size = egui::NumExt::at_most(
|
||||
initial_window_size,
|
||||
largest_monitor_point_size(egui_zoom_factor, event_loop),
|
||||
);
|
||||
viewport_builder = viewport_builder.with_inner_size(initial_window_size);
|
||||
}
|
||||
if clamp_size_to_monitor_size && let Some(initial_window_size) = viewport_builder.inner_size
|
||||
{
|
||||
let initial_window_size = egui::NumExt::at_most(
|
||||
initial_window_size,
|
||||
largest_monitor_point_size(egui_zoom_factor, event_loop),
|
||||
);
|
||||
viewport_builder = viewport_builder.with_inner_size(initial_window_size);
|
||||
}
|
||||
|
||||
viewport_builder.inner_size
|
||||
@@ -136,7 +135,7 @@ pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
#[allow(clippy::allow_attributes, clippy::unnecessary_wraps)]
|
||||
pub fn create_storage_with_file(_file: impl Into<PathBuf>) -> Option<Box<dyn epi::Storage>> {
|
||||
#[cfg(feature = "persistence")]
|
||||
return Some(Box::new(
|
||||
@@ -169,7 +168,7 @@ pub struct EpiIntegration {
|
||||
}
|
||||
|
||||
impl EpiIntegration {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::allow_attributes, clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
egui_ctx: egui::Context,
|
||||
window: &winit::window::Window,
|
||||
@@ -180,7 +179,9 @@ impl EpiIntegration {
|
||||
#[cfg(feature = "glow")] glow_register_native_texture: Option<
|
||||
Box<dyn FnMut(glow::Texture) -> egui::TextureId>,
|
||||
>,
|
||||
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
#[cfg(feature = "wgpu_no_default_features")] wgpu_render_state: Option<
|
||||
egui_wgpu::RenderState,
|
||||
>,
|
||||
) -> Self {
|
||||
let frame = epi::Frame {
|
||||
info: epi::IntegrationInfo { cpu_usage: None },
|
||||
@@ -189,7 +190,7 @@ impl EpiIntegration {
|
||||
gl,
|
||||
#[cfg(feature = "glow")]
|
||||
glow_register_native_texture,
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state,
|
||||
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
|
||||
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
|
||||
@@ -326,30 +327,32 @@ impl EpiIntegration {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) {
|
||||
pub fn save(&mut self, app: &mut dyn epi::App, window: Option<&winit::window::Window>) {
|
||||
#[cfg(not(feature = "persistence"))]
|
||||
let _ = (self, app, window);
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
if let Some(storage) = self.frame.storage_mut() {
|
||||
profiling::function_scope!();
|
||||
|
||||
if let Some(window) = _window {
|
||||
if self.persist_window {
|
||||
profiling::scope!("native_window");
|
||||
epi::set_value(
|
||||
storage,
|
||||
STORAGE_WINDOW_KEY,
|
||||
&WindowSettings::from_window(self.egui_ctx.zoom_factor(), window),
|
||||
);
|
||||
}
|
||||
if let Some(window) = window
|
||||
&& self.persist_window
|
||||
{
|
||||
profiling::scope!("native_window");
|
||||
epi::set_value(
|
||||
storage,
|
||||
STORAGE_WINDOW_KEY,
|
||||
&WindowSettings::from_window(self.egui_ctx.zoom_factor(), window),
|
||||
);
|
||||
}
|
||||
if _app.persist_egui_memory() {
|
||||
if app.persist_egui_memory() {
|
||||
profiling::scope!("egui_memory");
|
||||
self.egui_ctx
|
||||
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
|
||||
}
|
||||
{
|
||||
profiling::scope!("App::save");
|
||||
_app.save(storage);
|
||||
app.save(storage);
|
||||
}
|
||||
|
||||
profiling::scope!("Storage::flush");
|
||||
|
||||
@@ -27,7 +27,7 @@ impl Drop for EventLoopGuard {
|
||||
}
|
||||
|
||||
// Helper function to safely use the current event loop
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
pub fn with_current_event_loop<F, R>(f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&ActiveEventLoop) -> R,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::Write,
|
||||
io::Write as _,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
@@ -42,20 +42,20 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
|
||||
// Adapted from
|
||||
// https://github.com/rust-lang/cargo/blob/6e11c77384989726bb4f412a0e23b59c27222c34/crates/home/src/windows.rs#L19-L37
|
||||
#[cfg(all(windows, not(target_vendor = "uwp")))]
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
fn roaming_appdata() -> Option<PathBuf> {
|
||||
use std::ffi::OsString;
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
use std::os::windows::ffi::OsStringExt as _;
|
||||
use std::ptr;
|
||||
use std::slice;
|
||||
|
||||
use windows_sys::Win32::Foundation::S_OK;
|
||||
use windows_sys::Win32::System::Com::CoTaskMemFree;
|
||||
use windows_sys::Win32::UI::Shell::{
|
||||
FOLDERID_RoamingAppData, SHGetKnownFolderPath, KF_FLAG_DONT_VERIFY,
|
||||
FOLDERID_RoamingAppData, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
unsafe extern "C" {
|
||||
fn wcslen(buf: *const u16) -> usize;
|
||||
}
|
||||
let mut path_raw = ptr::null_mut();
|
||||
@@ -72,7 +72,7 @@ fn roaming_appdata() -> Option<PathBuf> {
|
||||
};
|
||||
|
||||
let path = if result == S_OK {
|
||||
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a nullterminated string for us.
|
||||
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a null-terminated string for us.
|
||||
let path_slice = unsafe { slice::from_raw_parts(path_raw, wcslen(path_raw)) };
|
||||
Some(PathBuf::from(OsString::from_wide(path_slice)))
|
||||
} else {
|
||||
@@ -118,7 +118,7 @@ impl FileStorage {
|
||||
pub(crate) fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
|
||||
profiling::function_scope!();
|
||||
let ron_filepath: PathBuf = ron_filepath.into();
|
||||
log::debug!("Loading app state from {:?}…", ron_filepath);
|
||||
log::debug!("Loading app state from {}…", ron_filepath.display());
|
||||
Self {
|
||||
kv: read_ron(&ron_filepath).unwrap_or_default(),
|
||||
ron_filepath,
|
||||
@@ -133,9 +133,8 @@ impl FileStorage {
|
||||
if let Some(data_dir) = storage_dir(app_id) {
|
||||
if let Err(err) = std::fs::create_dir_all(&data_dir) {
|
||||
log::warn!(
|
||||
"Saving disabled: Failed to create app path at {:?}: {}",
|
||||
data_dir,
|
||||
err
|
||||
"Saving disabled: Failed to create app path at {}: {err}",
|
||||
data_dir.display()
|
||||
);
|
||||
None
|
||||
} else {
|
||||
@@ -193,12 +192,11 @@ impl crate::Storage for FileStorage {
|
||||
fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
|
||||
profiling::function_scope!();
|
||||
|
||||
if let Some(parent_dir) = file_path.parent() {
|
||||
if !parent_dir.exists() {
|
||||
if let Err(err) = std::fs::create_dir_all(parent_dir) {
|
||||
log::warn!("Failed to create directory {parent_dir:?}: {err}");
|
||||
}
|
||||
}
|
||||
if let Some(parent_dir) = file_path.parent()
|
||||
&& !parent_dir.exists()
|
||||
&& let Err(err) = std::fs::create_dir_all(parent_dir)
|
||||
{
|
||||
log::warn!("Failed to create directory {}: {err}", parent_dir.display());
|
||||
}
|
||||
|
||||
match std::fs::File::create(file_path) {
|
||||
@@ -207,16 +205,17 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
|
||||
let config = Default::default();
|
||||
|
||||
profiling::scope!("ron::serialize");
|
||||
if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config)
|
||||
if let Err(err) = ron::Options::default()
|
||||
.to_io_writer_pretty(&mut writer, &kv, config)
|
||||
.and_then(|_| writer.flush().map_err(|err| err.into()))
|
||||
{
|
||||
log::warn!("Failed to serialize app state: {}", err);
|
||||
log::warn!("Failed to serialize app state: {err}");
|
||||
} else {
|
||||
log::trace!("Persisted to {:?}", file_path);
|
||||
log::trace!("Persisted to {}", file_path.display());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Failed to create file {file_path:?}: {err}");
|
||||
log::warn!("Failed to create file {}: {err}", file_path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,7 +233,7 @@ where
|
||||
match ron::de::from_reader(reader) {
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => {
|
||||
log::warn!("Failed to parse RON: {}", err);
|
||||
log::warn!("Failed to parse RON: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,34 +11,34 @@ use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant};
|
||||
|
||||
use egui_winit::ActionRequested;
|
||||
use glutin::{
|
||||
config::GlConfig,
|
||||
context::NotCurrentGlContext,
|
||||
display::GetGlDisplay,
|
||||
prelude::{GlDisplay, PossiblyCurrentGlContext},
|
||||
surface::GlSurface,
|
||||
config::GlConfig as _,
|
||||
context::NotCurrentGlContext as _,
|
||||
display::GetGlDisplay as _,
|
||||
prelude::{GlDisplay as _, PossiblyCurrentGlContext as _},
|
||||
surface::GlSurface as _,
|
||||
};
|
||||
use raw_window_handle::HasWindowHandle;
|
||||
use raw_window_handle::HasWindowHandle as _;
|
||||
use winit::{
|
||||
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
|
||||
window::{Window, WindowId},
|
||||
};
|
||||
|
||||
use ahash::{HashMap, HashSet};
|
||||
use ahash::HashMap;
|
||||
use egui::{
|
||||
DeferredViewportUiCallback, ImmediateViewport, ViewportBuilder, ViewportClass, ViewportId,
|
||||
ViewportIdMap, ViewportIdPair, ViewportInfo, ViewportOutput,
|
||||
DeferredViewportUiCallback, ImmediateViewport, OrderedViewportIdMap, ViewportBuilder,
|
||||
ViewportClass, ViewportId, ViewportIdPair, ViewportInfo, ViewportOutput,
|
||||
};
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui_winit::accesskit_winit;
|
||||
|
||||
use crate::{
|
||||
native::epi_integration::EpiIntegration, App, AppCreator, CreationContext, NativeOptions,
|
||||
Result, Storage,
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::epi_integration::EpiIntegration,
|
||||
};
|
||||
|
||||
use super::{
|
||||
epi_integration, event_loop_context,
|
||||
winit_integration::{create_egui_context, EventResult, UserEvent, WinitApp},
|
||||
winit_integration::{EventResult, UserEvent, WinitApp, create_egui_context},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -94,9 +94,9 @@ struct GlutinWindowContext {
|
||||
current_gl_context: Option<glutin::context::PossiblyCurrentContext>,
|
||||
not_current_gl_context: Option<glutin::context::NotCurrentContext>,
|
||||
|
||||
viewports: ViewportIdMap<Viewport>,
|
||||
viewports: OrderedViewportIdMap<Viewport>,
|
||||
viewport_from_window: HashMap<WindowId, ViewportId>,
|
||||
window_from_viewport: ViewportIdMap<WindowId>,
|
||||
window_from_viewport: OrderedViewportIdMap<WindowId>,
|
||||
|
||||
focused_viewport: Option<ViewportId>,
|
||||
}
|
||||
@@ -107,7 +107,7 @@ struct Viewport {
|
||||
builder: ViewportBuilder,
|
||||
deferred_commands: Vec<egui::viewport::ViewportCommand>,
|
||||
info: ViewportInfo,
|
||||
actions_requested: HashSet<egui_winit::ActionRequested>,
|
||||
actions_requested: Vec<egui_winit::ActionRequested>,
|
||||
|
||||
/// The user-callback that shows the ui.
|
||||
/// None for immediate viewports.
|
||||
@@ -139,7 +139,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
fn create_glutin_windowed_context(
|
||||
egui_ctx: &egui::Context,
|
||||
event_loop: &ActiveEventLoop,
|
||||
@@ -239,7 +239,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
let painter = painter.clone();
|
||||
move |native| painter.borrow_mut().register_native_texture(native)
|
||||
})),
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -272,7 +272,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
..
|
||||
} = viewport
|
||||
{
|
||||
egui_winit.init_accesskit(window, event_loop_proxy);
|
||||
egui_winit.init_accesskit(event_loop, window, event_loop_proxy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,10 +281,9 @@ impl<'app> GlowWinitApp<'app> {
|
||||
.viewport
|
||||
.mouse_passthrough
|
||||
.unwrap_or(false)
|
||||
&& let Err(err) = glutin.window(ViewportId::ROOT).set_cursor_hittest(false)
|
||||
{
|
||||
if let Err(err) = glutin.window(ViewportId::ROOT).set_cursor_hittest(false) {
|
||||
log::warn!("set_cursor_hittest(false) failed: {err}");
|
||||
}
|
||||
log::warn!("set_cursor_hittest(false) failed: {err}");
|
||||
}
|
||||
|
||||
let app_creator = std::mem::take(&mut self.app_creator)
|
||||
@@ -302,7 +301,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
storage: integration.frame.storage(),
|
||||
gl: Some(gl),
|
||||
get_proc_address: Some(&get_proc_address),
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state: None,
|
||||
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
|
||||
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
|
||||
@@ -336,10 +335,10 @@ impl<'app> GlowWinitApp<'app> {
|
||||
}
|
||||
|
||||
Ok(self.running.insert(GlowWinitRunning {
|
||||
glutin,
|
||||
painter,
|
||||
integration,
|
||||
app,
|
||||
glutin,
|
||||
painter,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -362,8 +361,12 @@ impl WinitApp for GlowWinitApp<'_> {
|
||||
|
||||
fn window_id_from_viewport_id(&self, id: ViewportId) -> Option<WindowId> {
|
||||
self.running
|
||||
.as_ref()
|
||||
.and_then(|r| r.glutin.borrow().window_from_viewport.get(&id).copied())
|
||||
.as_ref()?
|
||||
.glutin
|
||||
.borrow()
|
||||
.window_from_viewport
|
||||
.get(&id)
|
||||
.copied()
|
||||
}
|
||||
|
||||
fn save(&mut self) {
|
||||
@@ -436,20 +439,20 @@ impl WinitApp for GlowWinitApp<'_> {
|
||||
_: winit::event::DeviceId,
|
||||
event: winit::event::DeviceEvent,
|
||||
) -> crate::Result<EventResult> {
|
||||
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
|
||||
if let Some(running) = &mut self.running {
|
||||
let mut glutin = running.glutin.borrow_mut();
|
||||
if let Some(viewport) = glutin
|
||||
.focused_viewport
|
||||
.and_then(|viewport| glutin.viewports.get_mut(&viewport))
|
||||
{
|
||||
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
|
||||
egui_winit.on_mouse_motion(delta);
|
||||
}
|
||||
if let winit::event::DeviceEvent::MouseMotion { delta } = event
|
||||
&& let Some(running) = &mut self.running
|
||||
{
|
||||
let mut glutin = running.glutin.borrow_mut();
|
||||
if let Some(viewport) = glutin
|
||||
.focused_viewport
|
||||
.and_then(|viewport| glutin.viewports.get_mut(&viewport))
|
||||
{
|
||||
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
|
||||
egui_winit.on_mouse_motion(delta);
|
||||
}
|
||||
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,7 +469,7 @@ impl WinitApp for GlowWinitApp<'_> {
|
||||
if let Some(running) = &mut self.running {
|
||||
Ok(running.on_window_event(window_id, &event))
|
||||
} else {
|
||||
Ok(EventResult::Wait)
|
||||
Ok(EventResult::Exit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,16 +479,15 @@ impl WinitApp for GlowWinitApp<'_> {
|
||||
|
||||
if let Some(running) = &self.running {
|
||||
let mut glutin = running.glutin.borrow_mut();
|
||||
if let Some(viewport_id) = glutin.viewport_from_window.get(&event.window_id).copied() {
|
||||
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
|
||||
if let Some(egui_winit) = &mut viewport.egui_winit {
|
||||
return Ok(winit_integration::on_accesskit_window_event(
|
||||
egui_winit,
|
||||
event.window_id,
|
||||
&event.window_event,
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(viewport_id) = glutin.viewport_from_window.get(&event.window_id).copied()
|
||||
&& let Some(viewport) = glutin.viewports.get_mut(&viewport_id)
|
||||
&& let Some(egui_winit) = &mut viewport.egui_winit
|
||||
{
|
||||
return Ok(winit_integration::on_accesskit_window_event(
|
||||
egui_winit,
|
||||
event.window_id,
|
||||
&event.window_event,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,10 +525,10 @@ impl GlowWinitRunning<'_> {
|
||||
if is_immediate && viewport_id != ViewportId::ROOT {
|
||||
// This will only happen if this is an immediate viewport.
|
||||
// That means that the viewport cannot be rendered by itself and needs his parent to be rendered.
|
||||
if let Some(parent_viewport) = glutin.viewports.get(&viewport.ids.parent) {
|
||||
if let Some(window) = parent_viewport.window.as_ref() {
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
if let Some(parent_viewport) = glutin.viewports.get(&viewport.ids.parent)
|
||||
&& let Some(window) = parent_viewport.window.as_ref()
|
||||
{
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
return Ok(EventResult::Wait);
|
||||
}
|
||||
@@ -561,6 +563,12 @@ impl GlowWinitRunning<'_> {
|
||||
(raw_input, viewport_ui_cb)
|
||||
};
|
||||
|
||||
// HACK: In order to get the right clear_color, the system theme needs to be set, which
|
||||
// usually only happens in the `update` call. So we call Options::begin_pass early
|
||||
// to set the right theme. Without this there would be a black flash on the first frame.
|
||||
self.integration
|
||||
.egui_ctx
|
||||
.options_mut(|opt| opt.begin_pass(&raw_input));
|
||||
let clear_color = self
|
||||
.app
|
||||
.clear_color(&self.integration.egui_ctx.style().visuals);
|
||||
@@ -671,7 +679,7 @@ impl GlowWinitRunning<'_> {
|
||||
);
|
||||
|
||||
{
|
||||
for action in viewport.actions_requested.drain() {
|
||||
for action in viewport.actions_requested.drain(..) {
|
||||
match action {
|
||||
ActionRequested::Screenshot(user_data) => {
|
||||
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
|
||||
@@ -723,10 +731,10 @@ impl GlowWinitRunning<'_> {
|
||||
|
||||
// give it time to settle:
|
||||
#[cfg(feature = "__screenshot")]
|
||||
if integration.egui_ctx.cumulative_pass_nr() == 2 {
|
||||
if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") {
|
||||
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
|
||||
}
|
||||
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);
|
||||
@@ -743,7 +751,7 @@ impl GlowWinitRunning<'_> {
|
||||
}
|
||||
|
||||
if integration.should_close() {
|
||||
Ok(EventResult::Exit)
|
||||
Ok(EventResult::CloseRequested)
|
||||
} else {
|
||||
Ok(EventResult::Wait)
|
||||
}
|
||||
@@ -773,19 +781,33 @@ impl GlowWinitRunning<'_> {
|
||||
let mut repaint_asap = false;
|
||||
|
||||
match event {
|
||||
winit::event::WindowEvent::Focused(new_focused) => {
|
||||
glutin.focused_viewport = new_focused.then(|| viewport_id).flatten();
|
||||
winit::event::WindowEvent::Focused(focused) => {
|
||||
let focused = if cfg!(target_os = "macos")
|
||||
&& let Some(viewport_id) = viewport_id
|
||||
&& let Some(viewport) = glutin.viewports.get(&viewport_id)
|
||||
&& let Some(window) = &viewport.window
|
||||
{
|
||||
// TODO(emilk): remove this work-around once we update winit
|
||||
// https://github.com/rust-windowing/winit/issues/4371
|
||||
// https://github.com/emilk/egui/issues/7588
|
||||
window.has_focus()
|
||||
} else {
|
||||
*focused
|
||||
};
|
||||
|
||||
glutin.focused_viewport = focused.then_some(viewport_id).flatten();
|
||||
}
|
||||
|
||||
winit::event::WindowEvent::Resized(physical_size) => {
|
||||
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
||||
// See: https://github.com/rust-windowing/winit/issues/208
|
||||
// This solves an issue where the app would panic when minimizing on Windows.
|
||||
if 0 < physical_size.width && 0 < physical_size.height {
|
||||
if let Some(viewport_id) = viewport_id {
|
||||
repaint_asap = true;
|
||||
glutin.resize(viewport_id, *physical_size);
|
||||
}
|
||||
if 0 < physical_size.width
|
||||
&& 0 < physical_size.height
|
||||
&& let Some(viewport_id) = viewport_id
|
||||
{
|
||||
repaint_asap = true;
|
||||
glutin.resize(viewport_id, *physical_size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -794,44 +816,31 @@ impl GlowWinitRunning<'_> {
|
||||
log::debug!(
|
||||
"Received WindowEvent::CloseRequested for main viewport - shutting down."
|
||||
);
|
||||
return EventResult::Exit;
|
||||
return EventResult::CloseRequested;
|
||||
}
|
||||
|
||||
log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}");
|
||||
|
||||
if let Some(viewport_id) = viewport_id {
|
||||
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
|
||||
// Tell viewport it should close:
|
||||
viewport.info.events.push(egui::ViewportEvent::Close);
|
||||
if let Some(viewport_id) = viewport_id
|
||||
&& let Some(viewport) = glutin.viewports.get_mut(&viewport_id)
|
||||
{
|
||||
// Tell viewport it should close:
|
||||
viewport.info.events.push(egui::ViewportEvent::Close);
|
||||
|
||||
// We may need to repaint both us and our parent to close the window,
|
||||
// and perhaps twice (once to notice the close-event, once again to enforce it).
|
||||
// `request_repaint_of` does a double-repaint though:
|
||||
self.integration.egui_ctx.request_repaint_of(viewport_id);
|
||||
self.integration
|
||||
.egui_ctx
|
||||
.request_repaint_of(viewport.ids.parent);
|
||||
}
|
||||
// We may need to repaint both us and our parent to close the window,
|
||||
// and perhaps twice (once to notice the close-event, once again to enforce it).
|
||||
// `request_repaint_of` does a double-repaint though:
|
||||
self.integration.egui_ctx.request_repaint_of(viewport_id);
|
||||
self.integration
|
||||
.egui_ctx
|
||||
.request_repaint_of(viewport.ids.parent);
|
||||
}
|
||||
}
|
||||
|
||||
winit::event::WindowEvent::Destroyed => {
|
||||
log::debug!(
|
||||
"Received WindowEvent::Destroyed for viewport {:?}",
|
||||
viewport_id
|
||||
);
|
||||
if viewport_id == Some(ViewportId::ROOT) {
|
||||
return EventResult::Exit;
|
||||
} else {
|
||||
return EventResult::Wait;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if self.integration.should_close() {
|
||||
return EventResult::Exit;
|
||||
return EventResult::CloseRequested;
|
||||
}
|
||||
|
||||
let mut event_response = egui_winit::EventResponse {
|
||||
@@ -901,7 +910,7 @@ fn change_gl_context(
|
||||
}
|
||||
|
||||
impl GlutinWindowContext {
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
unsafe fn new(
|
||||
egui_ctx: &egui::Context,
|
||||
viewport_builder: ViewportBuilder,
|
||||
@@ -959,7 +968,6 @@ impl GlutinWindowContext {
|
||||
.with_preference(glutin_winit::ApiPreference::FallbackEgl)
|
||||
.with_window_attributes(Some(egui_winit::create_winit_window_attributes(
|
||||
egui_ctx,
|
||||
event_loop,
|
||||
viewport_builder.clone(),
|
||||
)));
|
||||
|
||||
@@ -1016,7 +1024,9 @@ impl GlutinWindowContext {
|
||||
let gl_context = match gl_context_result {
|
||||
Ok(it) => it,
|
||||
Err(err) => {
|
||||
log::warn!("Failed to create context using default context attributes {context_attributes:?} due to error: {err}");
|
||||
log::warn!(
|
||||
"Failed to create context using default context attributes {context_attributes:?} due to error: {err}"
|
||||
);
|
||||
log::debug!(
|
||||
"Retrying with fallback context attributes: {fallback_context_attributes:?}"
|
||||
);
|
||||
@@ -1030,15 +1040,27 @@ impl GlutinWindowContext {
|
||||
let not_current_gl_context = Some(gl_context);
|
||||
|
||||
let mut viewport_from_window = HashMap::default();
|
||||
let mut window_from_viewport = ViewportIdMap::default();
|
||||
let mut info = ViewportInfo::default();
|
||||
let mut window_from_viewport = OrderedViewportIdMap::default();
|
||||
let mut viewport_info = ViewportInfo::default();
|
||||
if let Some(window) = &window {
|
||||
viewport_from_window.insert(window.id(), ViewportId::ROOT);
|
||||
window_from_viewport.insert(ViewportId::ROOT, window.id());
|
||||
egui_winit::update_viewport_info(&mut info, egui_ctx, window, true);
|
||||
egui_winit::update_viewport_info(&mut viewport_info, egui_ctx, window, true);
|
||||
|
||||
// Tell egui right away about native_pixels_per_point etc,
|
||||
// so that the app knows about it during app creation:
|
||||
let pixels_per_point = egui_winit::pixels_per_point(egui_ctx, window);
|
||||
|
||||
egui_ctx.input_mut(|i| {
|
||||
i.raw
|
||||
.viewports
|
||||
.insert(ViewportId::ROOT, viewport_info.clone());
|
||||
|
||||
i.pixels_per_point = pixels_per_point;
|
||||
});
|
||||
}
|
||||
|
||||
let mut viewports = ViewportIdMap::default();
|
||||
let mut viewports = OrderedViewportIdMap::default();
|
||||
viewports.insert(
|
||||
ViewportId::ROOT,
|
||||
Viewport {
|
||||
@@ -1046,7 +1068,7 @@ impl GlutinWindowContext {
|
||||
class: ViewportClass::Root,
|
||||
builder: viewport_builder,
|
||||
deferred_commands: vec![],
|
||||
info,
|
||||
info: viewport_info,
|
||||
actions_requested: Default::default(),
|
||||
viewport_ui_cb: None,
|
||||
gl_surface: None,
|
||||
@@ -1094,7 +1116,7 @@ impl GlutinWindowContext {
|
||||
}
|
||||
|
||||
/// Create a surface, window, and winit integration for the viewport, if missing.
|
||||
#[allow(unsafe_code)]
|
||||
#[expect(unsafe_code)]
|
||||
pub(crate) fn initialize_window(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
@@ -1113,7 +1135,6 @@ impl GlutinWindowContext {
|
||||
log::debug!("Creating a window for viewport {viewport_id:?}");
|
||||
let window_attributes = egui_winit::create_winit_window_attributes(
|
||||
&self.egui_ctx,
|
||||
event_loop,
|
||||
viewport.builder.clone(),
|
||||
);
|
||||
if window_attributes.transparent()
|
||||
@@ -1241,21 +1262,21 @@ impl GlutinWindowContext {
|
||||
let width_px = NonZeroU32::new(physical_size.width).unwrap_or(NonZeroU32::MIN);
|
||||
let height_px = NonZeroU32::new(physical_size.height).unwrap_or(NonZeroU32::MIN);
|
||||
|
||||
if let Some(viewport) = self.viewports.get(&viewport_id) {
|
||||
if let Some(gl_surface) = &viewport.gl_surface {
|
||||
change_gl_context(
|
||||
&mut self.current_gl_context,
|
||||
&mut self.not_current_gl_context,
|
||||
gl_surface,
|
||||
);
|
||||
gl_surface.resize(
|
||||
self.current_gl_context
|
||||
.as_ref()
|
||||
.expect("failed to get current context to resize surface"),
|
||||
width_px,
|
||||
height_px,
|
||||
);
|
||||
}
|
||||
if let Some(viewport) = self.viewports.get(&viewport_id)
|
||||
&& let Some(gl_surface) = &viewport.gl_surface
|
||||
{
|
||||
change_gl_context(
|
||||
&mut self.current_gl_context,
|
||||
&mut self.not_current_gl_context,
|
||||
gl_surface,
|
||||
);
|
||||
gl_surface.resize(
|
||||
self.current_gl_context
|
||||
.as_ref()
|
||||
.expect("failed to get current context to resize surface"),
|
||||
width_px,
|
||||
height_px,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1265,7 +1286,7 @@ impl GlutinWindowContext {
|
||||
|
||||
pub(crate) fn remove_viewports_not_in(
|
||||
&mut self,
|
||||
viewport_output: &ViewportIdMap<ViewportOutput>,
|
||||
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||
) {
|
||||
// GC old viewports
|
||||
self.viewports
|
||||
@@ -1280,7 +1301,7 @@ impl GlutinWindowContext {
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
egui_ctx: &egui::Context,
|
||||
viewport_output: &ViewportIdMap<ViewportOutput>,
|
||||
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
@@ -1337,7 +1358,7 @@ impl GlutinWindowContext {
|
||||
}
|
||||
|
||||
fn initialize_or_update_viewport(
|
||||
viewports: &mut ViewportIdMap<Viewport>,
|
||||
viewports: &mut OrderedViewportIdMap<Viewport>,
|
||||
ids: ViewportIdPair,
|
||||
class: ViewportClass,
|
||||
mut builder: ViewportBuilder,
|
||||
@@ -1345,6 +1366,8 @@ fn initialize_or_update_viewport(
|
||||
) -> &mut Viewport {
|
||||
profiling::function_scope!();
|
||||
|
||||
use std::collections::btree_map::Entry;
|
||||
|
||||
if builder.icon.is_none() {
|
||||
// Inherit icon from parent
|
||||
builder.icon = viewports
|
||||
@@ -1353,7 +1376,7 @@ fn initialize_or_update_viewport(
|
||||
}
|
||||
|
||||
match viewports.entry(ids.this) {
|
||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||
Entry::Vacant(entry) => {
|
||||
// New viewport:
|
||||
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
|
||||
entry.insert(Viewport {
|
||||
@@ -1370,7 +1393,7 @@ fn initialize_or_update_viewport(
|
||||
})
|
||||
}
|
||||
|
||||
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
||||
Entry::Occupied(mut entry) => {
|
||||
// Patch an existing viewport:
|
||||
let viewport = entry.get_mut();
|
||||
|
||||
@@ -1566,6 +1589,6 @@ fn save_screenshot_and_exit(
|
||||
});
|
||||
log::info!("Screenshot saved to {path:?}.");
|
||||
|
||||
#[allow(clippy::exit)]
|
||||
#[expect(clippy::exit)]
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@ pub(crate) mod winit_integration;
|
||||
#[cfg(feature = "glow")]
|
||||
mod glow_integration;
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
mod wgpu_integration;
|
||||
|
||||
@@ -10,9 +10,8 @@ use ahash::HashMap;
|
||||
|
||||
use super::winit_integration::{UserEvent, WinitApp};
|
||||
use crate::{
|
||||
epi,
|
||||
Result, epi,
|
||||
native::{event_loop_context, winit_integration::EventResult},
|
||||
Result,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -93,48 +92,57 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
|
||||
log::trace!("event_result: {event_result:?}");
|
||||
|
||||
let combined_result = event_result.and_then(|event_result| {
|
||||
match event_result {
|
||||
EventResult::Wait => {
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
Ok(event_result)
|
||||
}
|
||||
EventResult::RepaintNow(window_id) => {
|
||||
log::trace!("RepaintNow of {window_id:?}",);
|
||||
let mut event_result = event_result;
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
|
||||
self.winit_app.run_ui_and_paint(event_loop, window_id)
|
||||
} else {
|
||||
// Fix for https://github.com/emilk/egui/issues/2425
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
Ok(event_result)
|
||||
}
|
||||
}
|
||||
EventResult::RepaintNext(window_id) => {
|
||||
log::trace!("RepaintNext of {window_id:?}",);
|
||||
if cfg!(target_os = "windows")
|
||||
&& let Ok(EventResult::RepaintNow(window_id)) = event_result
|
||||
{
|
||||
log::trace!("RepaintNow of {window_id:?}");
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
|
||||
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
|
||||
event_result = self.winit_app.run_ui_and_paint(event_loop, window_id);
|
||||
}
|
||||
|
||||
let combined_result = event_result.map(|event_result| match event_result {
|
||||
EventResult::Wait => {
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
event_result
|
||||
}
|
||||
EventResult::RepaintNow(window_id) => {
|
||||
log::trace!("RepaintNow of {window_id:?}",);
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
event_result
|
||||
}
|
||||
EventResult::RepaintNext(window_id) => {
|
||||
log::trace!("RepaintNext of {window_id:?}",);
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
event_result
|
||||
}
|
||||
EventResult::RepaintAt(window_id, repaint_time) => {
|
||||
self.windows_next_repaint_times.insert(
|
||||
window_id,
|
||||
self.windows_next_repaint_times
|
||||
.insert(window_id, Instant::now());
|
||||
Ok(event_result)
|
||||
}
|
||||
EventResult::RepaintAt(window_id, repaint_time) => {
|
||||
self.windows_next_repaint_times.insert(
|
||||
window_id,
|
||||
self.windows_next_repaint_times
|
||||
.get(&window_id)
|
||||
.map_or(repaint_time, |last| (*last).min(repaint_time)),
|
||||
);
|
||||
Ok(event_result)
|
||||
}
|
||||
EventResult::Save => {
|
||||
save = true;
|
||||
Ok(event_result)
|
||||
}
|
||||
EventResult::Exit => {
|
||||
exit = true;
|
||||
Ok(event_result)
|
||||
}
|
||||
.get(&window_id)
|
||||
.map_or(repaint_time, |last| (*last).min(repaint_time)),
|
||||
);
|
||||
event_result
|
||||
}
|
||||
EventResult::Save => {
|
||||
save = true;
|
||||
event_result
|
||||
}
|
||||
EventResult::Exit => {
|
||||
exit = true;
|
||||
event_result
|
||||
}
|
||||
EventResult::CloseRequested => {
|
||||
// The windows need to be dropped whilst the event loop is running to allow for proper cleanup.
|
||||
self.winit_app.save_and_destroy();
|
||||
event_result
|
||||
}
|
||||
});
|
||||
|
||||
@@ -142,7 +150,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
log::error!("Exiting because of error: {err}");
|
||||
exit = true;
|
||||
self.return_result = Err(err);
|
||||
};
|
||||
}
|
||||
|
||||
if save {
|
||||
log::debug!("Received an EventResult::Save - saving app state");
|
||||
@@ -159,7 +167,6 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
|
||||
log::debug!("Exiting with return code 0");
|
||||
|
||||
#[allow(clippy::exit)]
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
@@ -174,7 +181,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
.retain(|window_id, repaint_time| {
|
||||
if now < *repaint_time {
|
||||
return true; // not yet ready
|
||||
};
|
||||
}
|
||||
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
@@ -190,7 +197,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
||||
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));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,7 +320,7 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
fn run_and_return(event_loop: &mut EventLoop<UserEvent>, winit_app: impl WinitApp) -> Result {
|
||||
use winit::platform::run_on_demand::EventLoopExtRunOnDemand;
|
||||
use winit::platform::run_on_demand::EventLoopExtRunOnDemand as _;
|
||||
|
||||
log::trace!("Entering the winit event loop (run_app_on_demand)…");
|
||||
|
||||
@@ -359,9 +366,22 @@ pub fn run_glow(
|
||||
run_and_exit(event_loop, glow_eframe)
|
||||
}
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
pub fn create_glow<'a>(
|
||||
app_name: &str,
|
||||
native_options: epi::NativeOptions,
|
||||
app_creator: epi::AppCreator<'a>,
|
||||
event_loop: &EventLoop<UserEvent>,
|
||||
) -> impl ApplicationHandler<UserEvent> + 'a {
|
||||
use super::glow_integration::GlowWinitApp;
|
||||
|
||||
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
WinitAppWrapper::new(glow_eframe, true)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub fn run_wgpu(
|
||||
app_name: &str,
|
||||
mut native_options: epi::NativeOptions,
|
||||
@@ -383,3 +403,120 @@ pub fn run_wgpu(
|
||||
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||
run_and_exit(event_loop, wgpu_eframe)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub fn create_wgpu<'a>(
|
||||
app_name: &str,
|
||||
native_options: epi::NativeOptions,
|
||||
app_creator: epi::AppCreator<'a>,
|
||||
event_loop: &EventLoop<UserEvent>,
|
||||
) -> impl ApplicationHandler<UserEvent> + 'a {
|
||||
use super::wgpu_integration::WgpuWinitApp;
|
||||
|
||||
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
WinitAppWrapper::new(wgpu_eframe, true)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A proxy to the eframe application that implements [`ApplicationHandler`].
|
||||
///
|
||||
/// This can be run directly on your own [`EventLoop`] by itself or with other
|
||||
/// windows you manage outside of eframe.
|
||||
pub struct EframeWinitApplication<'a> {
|
||||
wrapper: Box<dyn ApplicationHandler<UserEvent> + 'a>,
|
||||
control_flow: ControlFlow,
|
||||
}
|
||||
|
||||
impl ApplicationHandler<UserEvent> for EframeWinitApplication<'_> {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.resumed(event_loop);
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
window_id: winit::window::WindowId,
|
||||
event: winit::event::WindowEvent,
|
||||
) {
|
||||
self.wrapper.window_event(event_loop, window_id, event);
|
||||
}
|
||||
|
||||
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
|
||||
self.wrapper.new_events(event_loop, cause);
|
||||
}
|
||||
|
||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
|
||||
self.wrapper.user_event(event_loop, event);
|
||||
}
|
||||
|
||||
fn device_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
device_id: winit::event::DeviceId,
|
||||
event: winit::event::DeviceEvent,
|
||||
) {
|
||||
self.wrapper.device_event(event_loop, device_id, event);
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.about_to_wait(event_loop);
|
||||
self.control_flow = event_loop.control_flow();
|
||||
}
|
||||
|
||||
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.suspended(event_loop);
|
||||
}
|
||||
|
||||
fn exiting(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.exiting(event_loop);
|
||||
}
|
||||
|
||||
fn memory_warning(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.wrapper.memory_warning(event_loop);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> EframeWinitApplication<'a> {
|
||||
pub(crate) fn new<T: ApplicationHandler<UserEvent> + 'a>(app: T) -> Self {
|
||||
Self {
|
||||
wrapper: Box::new(app),
|
||||
control_flow: ControlFlow::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pump the `EventLoop` to check for and dispatch pending events to this application.
|
||||
///
|
||||
/// Returns either the exit code for the application or the final state of the [`ControlFlow`]
|
||||
/// after all events have been dispatched in this iteration.
|
||||
///
|
||||
/// This is useful when your [`EventLoop`] is not the main event loop for your application.
|
||||
/// See the `external_eventloop_async` example.
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub fn pump_eframe_app(
|
||||
&mut self,
|
||||
event_loop: &mut EventLoop<UserEvent>,
|
||||
timeout: Option<std::time::Duration>,
|
||||
) -> EframePumpStatus {
|
||||
use winit::platform::pump_events::{EventLoopExtPumpEvents as _, PumpStatus};
|
||||
|
||||
match event_loop.pump_app_events(timeout, self) {
|
||||
PumpStatus::Continue => EframePumpStatus::Continue(self.control_flow),
|
||||
PumpStatus::Exit(code) => EframePumpStatus::Exit(code),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Either an exit code or a [`ControlFlow`] from the [`ActiveEventLoop`].
|
||||
///
|
||||
/// The result of [`EframeWinitApplication::pump_eframe_app`].
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub enum EframePumpStatus {
|
||||
/// The final state of the [`ControlFlow`] after all events have been dispatched
|
||||
///
|
||||
/// Callers should perform the action that is appropriate for the [`ControlFlow`] value.
|
||||
Continue(ControlFlow),
|
||||
|
||||
/// The exit code for the application
|
||||
Exit(i32),
|
||||
}
|
||||
|
||||
@@ -15,18 +15,19 @@ use winit::{
|
||||
window::{Window, WindowId},
|
||||
};
|
||||
|
||||
use ahash::{HashMap, HashSet, HashSetExt};
|
||||
use ahash::HashMap;
|
||||
use egui::{
|
||||
DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, ViewportClass,
|
||||
ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput,
|
||||
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap,
|
||||
ViewportBuilder, ViewportClass, ViewportId, ViewportIdPair, ViewportIdSet, ViewportInfo,
|
||||
ViewportOutput,
|
||||
};
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui_winit::accesskit_winit;
|
||||
use winit_integration::UserEvent;
|
||||
|
||||
use crate::{
|
||||
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
|
||||
};
|
||||
|
||||
use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp};
|
||||
@@ -70,9 +71,10 @@ pub struct SharedState {
|
||||
painter: egui_wgpu::winit::Painter,
|
||||
viewport_from_window: HashMap<WindowId, ViewportId>,
|
||||
focused_viewport: Option<ViewportId>,
|
||||
resized_viewport: Option<ViewportId>,
|
||||
}
|
||||
|
||||
pub type Viewports = ViewportIdMap<Viewport>;
|
||||
pub type Viewports = egui::OrderedViewportIdMap<Viewport>;
|
||||
|
||||
pub struct Viewport {
|
||||
ids: ViewportIdPair,
|
||||
@@ -80,7 +82,7 @@ pub struct Viewport {
|
||||
builder: ViewportBuilder,
|
||||
deferred_commands: Vec<egui::viewport::ViewportCommand>,
|
||||
info: ViewportInfo,
|
||||
actions_requested: HashSet<ActionRequested>,
|
||||
actions_requested: Vec<ActionRequested>,
|
||||
|
||||
/// `None` for sync viewports.
|
||||
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
|
||||
@@ -182,19 +184,37 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
builder: ViewportBuilder,
|
||||
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
|
||||
profiling::function_scope!();
|
||||
#[allow(unsafe_code, unused_mut, unused_unsafe)]
|
||||
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
|
||||
egui_ctx.clone(),
|
||||
self.native_options.wgpu_options.clone(),
|
||||
self.native_options.multisampling.max(1) as _,
|
||||
egui_wgpu::depth_format_from_bits(
|
||||
self.native_options.depth_buffer,
|
||||
self.native_options.stencil_buffer,
|
||||
),
|
||||
self.native_options.viewport.transparent.unwrap_or(false),
|
||||
self.native_options.dithering,
|
||||
egui_wgpu::RendererOptions {
|
||||
msaa_samples: self.native_options.multisampling as _,
|
||||
depth_stencil_format: egui_wgpu::depth_format_from_bits(
|
||||
self.native_options.depth_buffer,
|
||||
self.native_options.stencil_buffer,
|
||||
),
|
||||
dithering: self.native_options.dithering,
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
|
||||
let mut viewport_info = ViewportInfo::default();
|
||||
egui_winit::update_viewport_info(&mut viewport_info, &egui_ctx, &window, true);
|
||||
|
||||
{
|
||||
// Tell egui right away about native_pixels_per_point etc,
|
||||
// so that the app knows about it during app creation:
|
||||
let pixels_per_point = egui_winit::pixels_per_point(&egui_ctx, &window);
|
||||
|
||||
egui_ctx.input_mut(|i| {
|
||||
i.raw
|
||||
.viewports
|
||||
.insert(ViewportId::ROOT, viewport_info.clone());
|
||||
i.pixels_per_point = pixels_per_point;
|
||||
});
|
||||
}
|
||||
|
||||
let window = Arc::new(window);
|
||||
|
||||
{
|
||||
@@ -236,7 +256,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(unused_mut)] // used for accesskit
|
||||
#[allow(unused_mut, clippy::allow_attributes)] // used for accesskit
|
||||
let mut egui_winit = egui_winit::State::new(
|
||||
egui_ctx.clone(),
|
||||
ViewportId::ROOT,
|
||||
@@ -249,7 +269,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
let event_loop_proxy = self.repaint_proxy.lock().clone();
|
||||
egui_winit.init_accesskit(&window, event_loop_proxy);
|
||||
egui_winit.init_accesskit(event_loop, &window, event_loop_proxy);
|
||||
}
|
||||
|
||||
let app_creator = std::mem::take(&mut self.app_creator)
|
||||
@@ -274,9 +294,6 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
let mut viewport_from_window = HashMap::default();
|
||||
viewport_from_window.insert(window.id(), ViewportId::ROOT);
|
||||
|
||||
let mut info = ViewportInfo::default();
|
||||
egui_winit::update_viewport_info(&mut info, &egui_ctx, &window, true);
|
||||
|
||||
let mut viewports = Viewports::default();
|
||||
viewports.insert(
|
||||
ViewportId::ROOT,
|
||||
@@ -285,7 +302,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
class: ViewportClass::Root,
|
||||
builder,
|
||||
deferred_commands: vec![],
|
||||
info,
|
||||
info: viewport_info,
|
||||
actions_requested: Default::default(),
|
||||
viewport_ui_cb: None,
|
||||
window: Some(window),
|
||||
@@ -299,6 +316,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
viewports,
|
||||
painter,
|
||||
focused_viewport: Some(ViewportId::ROOT),
|
||||
resized_viewport: None,
|
||||
}));
|
||||
|
||||
{
|
||||
@@ -333,10 +351,8 @@ impl WinitApp for WgpuWinitApp<'_> {
|
||||
.as_ref()
|
||||
.and_then(|r| {
|
||||
let shared = r.shared.borrow();
|
||||
shared
|
||||
.viewport_from_window
|
||||
.get(&window_id)
|
||||
.and_then(|id| shared.viewports.get(id).map(|v| v.window.clone()))
|
||||
let id = shared.viewport_from_window.get(&window_id)?;
|
||||
shared.viewports.get(id).map(|v| v.window.clone())
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
@@ -431,20 +447,20 @@ impl WinitApp for WgpuWinitApp<'_> {
|
||||
_: winit::event::DeviceId,
|
||||
event: winit::event::DeviceEvent,
|
||||
) -> crate::Result<EventResult> {
|
||||
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
|
||||
if let Some(running) = &mut self.running {
|
||||
let mut shared = running.shared.borrow_mut();
|
||||
if let Some(viewport) = shared
|
||||
.focused_viewport
|
||||
.and_then(|viewport| shared.viewports.get_mut(&viewport))
|
||||
{
|
||||
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
|
||||
egui_winit.on_mouse_motion(delta);
|
||||
}
|
||||
if let winit::event::DeviceEvent::MouseMotion { delta } = event
|
||||
&& let Some(running) = &mut self.running
|
||||
{
|
||||
let mut shared = running.shared.borrow_mut();
|
||||
if let Some(viewport) = shared
|
||||
.focused_viewport
|
||||
.and_then(|viewport| shared.viewports.get_mut(&viewport))
|
||||
{
|
||||
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
|
||||
egui_winit.on_mouse_motion(delta);
|
||||
}
|
||||
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,7 +479,8 @@ impl WinitApp for WgpuWinitApp<'_> {
|
||||
if let Some(running) = &mut self.running {
|
||||
Ok(running.on_window_event(window_id, &event))
|
||||
} else {
|
||||
Ok(EventResult::Wait)
|
||||
// running is removed to get ready for exiting
|
||||
Ok(EventResult::Exit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,14 +496,13 @@ impl WinitApp for WgpuWinitApp<'_> {
|
||||
if let Some(viewport) = viewport_from_window
|
||||
.get(&event.window_id)
|
||||
.and_then(|id| viewports.get_mut(id))
|
||||
&& let Some(egui_winit) = &mut viewport.egui_winit
|
||||
{
|
||||
if let Some(egui_winit) = &mut viewport.egui_winit {
|
||||
return Ok(winit_integration::on_accesskit_window_event(
|
||||
egui_winit,
|
||||
event.window_id,
|
||||
&event.window_event,
|
||||
));
|
||||
}
|
||||
return Ok(winit_integration::on_accesskit_window_event(
|
||||
egui_winit,
|
||||
event.window_id,
|
||||
&event.window_event,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,10 +580,10 @@ impl WgpuWinitRunning<'_> {
|
||||
if viewport.viewport_ui_cb.is_none() {
|
||||
// This will only happen if this is an immediate viewport.
|
||||
// That means that the viewport cannot be rendered by itself and needs his parent to be rendered.
|
||||
if let Some(viewport) = viewports.get(&viewport.ids.parent) {
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
if let Some(viewport) = viewports.get(&viewport.ids.parent)
|
||||
&& let Some(window) = viewport.window.as_ref()
|
||||
{
|
||||
return Ok(EventResult::RepaintNext(window.id()));
|
||||
}
|
||||
return Ok(EventResult::Wait);
|
||||
}
|
||||
@@ -680,7 +696,7 @@ impl WgpuWinitRunning<'_> {
|
||||
screenshot_commands,
|
||||
);
|
||||
|
||||
for action in viewport.actions_requested.drain() {
|
||||
for action in viewport.actions_requested.drain(..) {
|
||||
match action {
|
||||
ActionRequested::Screenshot { .. } => {
|
||||
// already handled above
|
||||
@@ -731,17 +747,17 @@ impl WgpuWinitRunning<'_> {
|
||||
|
||||
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
|
||||
|
||||
if let Some(window) = window {
|
||||
if window.is_minimized() == Some(true) {
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
if let Some(window) = window
|
||||
&& window.is_minimized() == Some(true)
|
||||
{
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
|
||||
if integration.should_close() {
|
||||
Ok(EventResult::Exit)
|
||||
Ok(EventResult::CloseRequested)
|
||||
} else {
|
||||
Ok(EventResult::Wait)
|
||||
}
|
||||
@@ -762,37 +778,68 @@ impl WgpuWinitRunning<'_> {
|
||||
let viewport_id = shared.viewport_from_window.get(&window_id).copied();
|
||||
|
||||
// On Windows, if a window is resized by the user, it should repaint synchronously, inside the
|
||||
// event handler.
|
||||
//
|
||||
// If this is not done, the compositor will assume that the window does not want to redraw,
|
||||
// and continue ahead.
|
||||
// event handler. If this is not done, the compositor will assume that the window does not want
|
||||
// to redraw and continue ahead.
|
||||
//
|
||||
// In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver
|
||||
// new frames to the compositor in time.
|
||||
//
|
||||
// The flickering is technically glutin or glow's fault, but we should be responding properly
|
||||
// new frames to the compositor in time. The flickering is technically glutin or glow's fault, but we should be responding properly
|
||||
// to resizes anyway, as doing so avoids dropping frames.
|
||||
//
|
||||
// See: https://github.com/emilk/egui/issues/903
|
||||
let mut repaint_asap = false;
|
||||
|
||||
// On MacOS the asap repaint is not enough. The drawn frames must be synchronized with
|
||||
// the CoreAnimation transactions driving the window resize process.
|
||||
//
|
||||
// Thus, Painter, responsible for wgpu surfaces and their resize, has to be notified of the
|
||||
// resize lifecycle, yet winit does not provide any events for that. To work around,
|
||||
// the last resized viewport is tracked until any next non-resize event is received.
|
||||
//
|
||||
// Accidental state change during the resize process due to an unexpected event fire
|
||||
// is ok, state will switch back upon next resize event.
|
||||
//
|
||||
// See: https://github.com/emilk/egui/issues/903
|
||||
if let Some(id) = viewport_id
|
||||
&& shared.resized_viewport == viewport_id
|
||||
{
|
||||
shared.painter.on_window_resize_state_change(id, false);
|
||||
shared.resized_viewport = None;
|
||||
}
|
||||
|
||||
match event {
|
||||
winit::event::WindowEvent::Focused(new_focused) => {
|
||||
shared.focused_viewport = new_focused.then(|| viewport_id).flatten();
|
||||
winit::event::WindowEvent::Focused(focused) => {
|
||||
let focused = if cfg!(target_os = "macos")
|
||||
&& let Some(viewport_id) = viewport_id
|
||||
&& let Some(viewport) = shared.viewports.get(&viewport_id)
|
||||
&& let Some(window) = &viewport.window
|
||||
{
|
||||
// TODO(emilk): remove this work-around once we update winit
|
||||
// https://github.com/rust-windowing/winit/issues/4371
|
||||
// https://github.com/emilk/egui/issues/7588
|
||||
window.has_focus()
|
||||
} else {
|
||||
*focused
|
||||
};
|
||||
|
||||
shared.focused_viewport = focused.then_some(viewport_id).flatten();
|
||||
}
|
||||
|
||||
winit::event::WindowEvent::Resized(physical_size) => {
|
||||
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
||||
// See: https://github.com/rust-windowing/winit/issues/208
|
||||
// This solves an issue where the app would panic when minimizing on Windows.
|
||||
if let Some(viewport_id) = viewport_id {
|
||||
if let (Some(width), Some(height)) = (
|
||||
if let Some(id) = viewport_id
|
||||
&& let (Some(width), Some(height)) = (
|
||||
NonZeroU32::new(physical_size.width),
|
||||
NonZeroU32::new(physical_size.height),
|
||||
) {
|
||||
repaint_asap = true;
|
||||
shared.painter.on_window_resized(viewport_id, width, height);
|
||||
)
|
||||
{
|
||||
if shared.resized_viewport != viewport_id {
|
||||
shared.resized_viewport = viewport_id;
|
||||
shared.painter.on_window_resize_state_change(id, true);
|
||||
}
|
||||
shared.painter.on_window_resized(id, width, height);
|
||||
repaint_asap = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -801,42 +848,41 @@ impl WgpuWinitRunning<'_> {
|
||||
log::debug!(
|
||||
"Received WindowEvent::CloseRequested for main viewport - shutting down."
|
||||
);
|
||||
return EventResult::Exit;
|
||||
return EventResult::CloseRequested;
|
||||
}
|
||||
|
||||
log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}");
|
||||
|
||||
if let Some(viewport_id) = viewport_id {
|
||||
if let Some(viewport) = shared.viewports.get_mut(&viewport_id) {
|
||||
// Tell viewport it should close:
|
||||
viewport.info.events.push(egui::ViewportEvent::Close);
|
||||
if let Some(viewport_id) = viewport_id
|
||||
&& let Some(viewport) = shared.viewports.get_mut(&viewport_id)
|
||||
{
|
||||
// Tell viewport it should close:
|
||||
viewport.info.events.push(egui::ViewportEvent::Close);
|
||||
|
||||
// We may need to repaint both us and our parent to close the window,
|
||||
// and perhaps twice (once to notice the close-event, once again to enforce it).
|
||||
// `request_repaint_of` does a double-repaint though:
|
||||
integration.egui_ctx.request_repaint_of(viewport_id);
|
||||
integration.egui_ctx.request_repaint_of(viewport.ids.parent);
|
||||
}
|
||||
// We may need to repaint both us and our parent to close the window,
|
||||
// and perhaps twice (once to notice the close-event, once again to enforce it).
|
||||
// `request_repaint_of` does a double-repaint though:
|
||||
integration.egui_ctx.request_repaint_of(viewport_id);
|
||||
integration.egui_ctx.request_repaint_of(viewport.ids.parent);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
let event_response = viewport_id
|
||||
.and_then(|viewport_id| {
|
||||
shared.viewports.get_mut(&viewport_id).and_then(|viewport| {
|
||||
Some(integration.on_window_event(
|
||||
viewport.window.as_deref()?,
|
||||
viewport.egui_winit.as_mut()?,
|
||||
event,
|
||||
))
|
||||
})
|
||||
let viewport = shared.viewports.get_mut(&viewport_id)?;
|
||||
Some(integration.on_window_event(
|
||||
viewport.window.as_deref()?,
|
||||
viewport.egui_winit.as_mut()?,
|
||||
event,
|
||||
))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if integration.should_close() {
|
||||
EventResult::Exit
|
||||
EventResult::CloseRequested
|
||||
} else if event_response.repaint {
|
||||
if repaint_asap {
|
||||
EventResult::RepaintNow(window_id)
|
||||
@@ -1035,10 +1081,10 @@ fn render_immediate_viewport(
|
||||
}
|
||||
|
||||
pub(crate) fn remove_viewports_not_in(
|
||||
viewports: &mut ViewportIdMap<Viewport>,
|
||||
viewports: &mut Viewports,
|
||||
painter: &mut egui_wgpu::winit::Painter,
|
||||
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
|
||||
viewport_output: &ViewportIdMap<ViewportOutput>,
|
||||
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||
) {
|
||||
let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect();
|
||||
|
||||
@@ -1051,8 +1097,8 @@ pub(crate) fn remove_viewports_not_in(
|
||||
/// Add new viewports, and update existing ones:
|
||||
fn handle_viewport_output(
|
||||
egui_ctx: &egui::Context,
|
||||
viewport_output: &ViewportIdMap<ViewportOutput>,
|
||||
viewports: &mut ViewportIdMap<Viewport>,
|
||||
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||
viewports: &mut Viewports,
|
||||
painter: &mut egui_wgpu::winit::Painter,
|
||||
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
|
||||
) {
|
||||
@@ -1089,13 +1135,13 @@ fn handle_viewport_output(
|
||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||
if cfg!(target_os = "linux") {
|
||||
let new_inner_size = window.inner_size();
|
||||
if new_inner_size != old_inner_size {
|
||||
if let (Some(width), Some(height)) = (
|
||||
if new_inner_size != old_inner_size
|
||||
&& let (Some(width), Some(height)) = (
|
||||
NonZeroU32::new(new_inner_size.width),
|
||||
NonZeroU32::new(new_inner_size.height),
|
||||
) {
|
||||
painter.on_window_resized(viewport_id, width, height);
|
||||
}
|
||||
)
|
||||
{
|
||||
painter.on_window_resized(viewport_id, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1112,6 +1158,8 @@ fn initialize_or_update_viewport<'a>(
|
||||
viewport_ui_cb: Option<Arc<dyn Fn(&egui::Context) + Send + Sync>>,
|
||||
painter: &mut egui_wgpu::winit::Painter,
|
||||
) -> &'a mut Viewport {
|
||||
use std::collections::btree_map::Entry;
|
||||
|
||||
profiling::function_scope!();
|
||||
|
||||
if builder.icon.is_none() {
|
||||
@@ -1122,7 +1170,7 @@ fn initialize_or_update_viewport<'a>(
|
||||
}
|
||||
|
||||
match viewports.entry(ids.this) {
|
||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||
Entry::Vacant(entry) => {
|
||||
// New viewport:
|
||||
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
|
||||
entry.insert(Viewport {
|
||||
@@ -1131,14 +1179,14 @@ fn initialize_or_update_viewport<'a>(
|
||||
builder,
|
||||
deferred_commands: vec![],
|
||||
info: Default::default(),
|
||||
actions_requested: HashSet::new(),
|
||||
actions_requested: Vec::new(),
|
||||
viewport_ui_cb,
|
||||
window: None,
|
||||
egui_winit: None,
|
||||
})
|
||||
}
|
||||
|
||||
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
||||
Entry::Occupied(mut entry) => {
|
||||
// Patch an existing viewport:
|
||||
let viewport = entry.get_mut();
|
||||
|
||||
|
||||
@@ -124,6 +124,25 @@ pub enum EventResult {
|
||||
/// Causes a save of the client state when the persistence feature is enabled.
|
||||
Save,
|
||||
|
||||
/// Starts the process of ending eframe execution whilst allowing for proper
|
||||
/// clean up of resources.
|
||||
///
|
||||
/// # Warning
|
||||
/// This event **must** occur before [`Exit`] to correctly exit eframe code.
|
||||
/// If in doubt, return this event.
|
||||
///
|
||||
/// [`Exit`]: [EventResult::Exit]
|
||||
CloseRequested,
|
||||
|
||||
/// The event loop will exit, now.
|
||||
/// The correct circumstance to return this event is in response to a winit "Destroyed" event.
|
||||
///
|
||||
/// # Warning
|
||||
/// The [`CloseRequested`] **must** occur before this event to ensure that winit
|
||||
/// is able to remove any open windows. Otherwise the window(s) will remain open
|
||||
/// until the program terminates.
|
||||
///
|
||||
/// [`CloseRequested`]: EventResult::CloseRequested
|
||||
Exit,
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ impl Stopwatch {
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
assert!(self.start.is_none());
|
||||
assert!(self.start.is_none(), "Stopwatch already running");
|
||||
self.start = Some(Instant::now());
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ impl Stopwatch {
|
||||
}
|
||||
|
||||
pub fn resume(&mut self) {
|
||||
assert!(self.start.is_none());
|
||||
assert!(self.start.is_none(), "Stopwatch still running");
|
||||
self.start = Some(Instant::now());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use egui::{TexturesDelta, UserData, ViewportCommand};
|
||||
|
||||
use crate::{epi, App};
|
||||
use crate::{App, epi, web::web_painter::WebPainter};
|
||||
|
||||
use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter, NeedRepaint};
|
||||
use super::{NeedRepaint, now_sec, text_agent::TextAgent};
|
||||
|
||||
pub struct AppRunner {
|
||||
#[allow(dead_code)]
|
||||
#[allow(dead_code, clippy::allow_attributes)]
|
||||
pub(crate) web_options: crate::WebOptions,
|
||||
pub(crate) frame: epi::Frame,
|
||||
egui_ctx: egui::Context,
|
||||
painter: super::ActiveWebPainter,
|
||||
painter: Box<dyn WebPainter>,
|
||||
pub(crate) input: super::WebInput,
|
||||
app: Box<dyn epi::App>,
|
||||
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
|
||||
@@ -34,6 +34,10 @@ impl Drop for AppRunner {
|
||||
impl AppRunner {
|
||||
/// # Errors
|
||||
/// Failure to initialize WebGL renderer, or failure to create app.
|
||||
#[cfg_attr(
|
||||
not(feature = "wgpu_no_default_features"),
|
||||
expect(clippy::unused_async)
|
||||
)]
|
||||
pub async fn new(
|
||||
canvas: web_sys::HtmlCanvasElement,
|
||||
web_options: crate::WebOptions,
|
||||
@@ -41,7 +45,41 @@ impl AppRunner {
|
||||
text_agent: TextAgent,
|
||||
) -> Result<Self, String> {
|
||||
let egui_ctx = egui::Context::default();
|
||||
let painter = super::ActiveWebPainter::new(egui_ctx.clone(), canvas, &web_options).await?;
|
||||
|
||||
#[allow(clippy::allow_attributes, unused_assignments)]
|
||||
#[cfg(feature = "glow")]
|
||||
let mut gl = None;
|
||||
|
||||
#[allow(clippy::allow_attributes, unused_assignments)]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
let mut wgpu_render_state = None;
|
||||
|
||||
let painter = match web_options.renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
epi::Renderer::Glow => {
|
||||
log::debug!("Using the glow renderer");
|
||||
let painter = super::web_painter_glow::WebPainterGlow::new(
|
||||
egui_ctx.clone(),
|
||||
canvas,
|
||||
&web_options,
|
||||
)?;
|
||||
gl = Some(painter.gl().clone());
|
||||
Box::new(painter) as Box<dyn WebPainter>
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
epi::Renderer::Wgpu => {
|
||||
log::debug!("Using the wgpu renderer");
|
||||
let painter = super::web_painter_wgpu::WebPainterWgpu::new(
|
||||
egui_ctx.clone(),
|
||||
canvas,
|
||||
&web_options,
|
||||
)
|
||||
.await?;
|
||||
wgpu_render_state = painter.render_state();
|
||||
Box::new(painter) as Box<dyn WebPainter>
|
||||
}
|
||||
};
|
||||
|
||||
let info = epi::IntegrationInfo {
|
||||
web_info: epi::WebInfo {
|
||||
@@ -65,21 +103,27 @@ impl AppRunner {
|
||||
o.zoom_factor = 1.0;
|
||||
});
|
||||
|
||||
// Tell egui right away about native_pixels_per_point
|
||||
// so that the app knows about it during app creation:
|
||||
egui_ctx.input_mut(|i| {
|
||||
let viewport_info = i.raw.viewports.entry(egui::ViewportId::ROOT).or_default();
|
||||
viewport_info.native_pixels_per_point = Some(super::native_pixels_per_point());
|
||||
i.pixels_per_point = super::native_pixels_per_point();
|
||||
});
|
||||
|
||||
let cc = epi::CreationContext {
|
||||
egui_ctx: egui_ctx.clone(),
|
||||
integration_info: info.clone(),
|
||||
storage: Some(&storage),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
gl: Some(painter.gl().clone()),
|
||||
gl: gl.clone(),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
get_proc_address: None,
|
||||
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
wgpu_render_state: painter.render_state(),
|
||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
||||
wgpu_render_state: None,
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state: wgpu_render_state.clone(),
|
||||
};
|
||||
let app = app_creator(&cc).map_err(|err| err.to_string())?;
|
||||
|
||||
@@ -88,15 +132,14 @@ impl AppRunner {
|
||||
storage: Some(Box::new(storage)),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
gl: Some(painter.gl().clone()),
|
||||
gl,
|
||||
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
wgpu_render_state: painter.render_state(),
|
||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
||||
wgpu_render_state: None,
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state,
|
||||
};
|
||||
|
||||
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();
|
||||
let needs_repaint: std::sync::Arc<NeedRepaint> =
|
||||
std::sync::Arc::new(NeedRepaint::new(web_options.max_fps));
|
||||
{
|
||||
let needs_repaint = needs_repaint.clone();
|
||||
egui_ctx.set_request_repaint_callback(move |info| {
|
||||
@@ -304,8 +347,6 @@ impl AppRunner {
|
||||
}
|
||||
|
||||
fn handle_platform_output(&self, platform_output: egui::PlatformOutput) {
|
||||
#![allow(deprecated)]
|
||||
|
||||
#[cfg(feature = "web_screen_reader")]
|
||||
if self.egui_ctx.options(|o| o.screen_reader) {
|
||||
super::screen_reader::speak(&platform_output.events_description());
|
||||
@@ -314,13 +355,10 @@ impl AppRunner {
|
||||
let egui::PlatformOutput {
|
||||
commands,
|
||||
cursor_icon,
|
||||
open_url,
|
||||
copied_text,
|
||||
events: _, // already handled
|
||||
mutable_text_under_cursor: _, // TODO(#4569): https://github.com/emilk/egui/issues/4569
|
||||
ime,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update: _, // not currently implemented
|
||||
accesskit_update: _, // not currently implemented
|
||||
num_completed_passes: _, // handled by `Context::run`
|
||||
request_discard_reasons: _, // handled by `Context::run`
|
||||
} = platform_output;
|
||||
@@ -341,14 +379,6 @@ impl AppRunner {
|
||||
|
||||
super::set_cursor_icon(cursor_icon);
|
||||
|
||||
if let Some(open) = open_url {
|
||||
super::open_url(&open.url, open.new_tab);
|
||||
}
|
||||
|
||||
if !copied_text.is_empty() {
|
||||
super::set_clipboard_text(&copied_text);
|
||||
}
|
||||
|
||||
if self.has_focus() {
|
||||
// The eframe app has focus.
|
||||
if ime.is_some() {
|
||||
|
||||
@@ -11,9 +11,15 @@ use super::percent_decode;
|
||||
/// Data gathered between frames.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct WebInput {
|
||||
/// Required because we don't get a position on touched
|
||||
/// Required because we don't get a position on touchend
|
||||
pub primary_touch: Option<egui::TouchId>,
|
||||
|
||||
/// Helps to track the delta scale from gesture events
|
||||
pub accumulated_scale: f32,
|
||||
|
||||
/// Helps to track the delta rotation from gesture events
|
||||
pub accumulated_rotation: f32,
|
||||
|
||||
/// The raw input to `egui`.
|
||||
pub raw: egui::RawInput,
|
||||
}
|
||||
@@ -50,11 +56,20 @@ impl WebInput {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Stores when to do the next repaint.
|
||||
pub(crate) struct NeedRepaint(Mutex<f64>);
|
||||
pub(crate) struct NeedRepaint {
|
||||
/// Time in seconds when the next repaint should happen.
|
||||
next_repaint: Mutex<f64>,
|
||||
|
||||
impl Default for NeedRepaint {
|
||||
fn default() -> Self {
|
||||
Self(Mutex::new(f64::NEG_INFINITY)) // start with a repaint
|
||||
/// Rate limit for repaint. 0 means "unlimited". The rate may still be limited by vsync.
|
||||
max_fps: u32,
|
||||
}
|
||||
|
||||
impl NeedRepaint {
|
||||
pub fn new(max_fps: Option<u32>) -> Self {
|
||||
Self {
|
||||
next_repaint: Mutex::new(f64::NEG_INFINITY), // start with a repaint
|
||||
max_fps: max_fps.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,25 +77,43 @@ impl NeedRepaint {
|
||||
/// Returns the time (in [`now_sec`] scale) when
|
||||
/// we should next repaint.
|
||||
pub fn when_to_repaint(&self) -> f64 {
|
||||
*self.0.lock()
|
||||
*self.next_repaint.lock()
|
||||
}
|
||||
|
||||
/// Unschedule repainting.
|
||||
pub fn clear(&self) {
|
||||
*self.0.lock() = f64::INFINITY;
|
||||
*self.next_repaint.lock() = f64::INFINITY;
|
||||
}
|
||||
|
||||
pub fn repaint_after(&self, num_seconds: f64) {
|
||||
let mut repaint_time = self.0.lock();
|
||||
*repaint_time = repaint_time.min(super::now_sec() + num_seconds);
|
||||
let mut time = super::now_sec() + num_seconds;
|
||||
time = self.round_repaint_time_to_rate(time);
|
||||
let mut repaint_time = self.next_repaint.lock();
|
||||
*repaint_time = repaint_time.min(time);
|
||||
}
|
||||
|
||||
/// Request a repaint. Depending on the presence of rate limiting, this may not be instant.
|
||||
pub fn repaint(&self) {
|
||||
let time = self.round_repaint_time_to_rate(super::now_sec());
|
||||
let mut repaint_time = self.next_repaint.lock();
|
||||
*repaint_time = repaint_time.min(time);
|
||||
}
|
||||
|
||||
pub fn repaint_asap(&self) {
|
||||
*self.next_repaint.lock() = f64::NEG_INFINITY;
|
||||
}
|
||||
|
||||
pub fn needs_repaint(&self) -> bool {
|
||||
self.when_to_repaint() <= super::now_sec()
|
||||
}
|
||||
|
||||
pub fn repaint_asap(&self) {
|
||||
*self.0.lock() = f64::NEG_INFINITY;
|
||||
fn round_repaint_time_to_rate(&self, time: f64) -> f64 {
|
||||
if self.max_fps == 0 {
|
||||
time
|
||||
} else {
|
||||
let interval = 1.0 / self.max_fps as f64;
|
||||
(time / interval).ceil() * interval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use web_sys::EventTarget;
|
||||
|
||||
use crate::web::string_from_js_value;
|
||||
|
||||
use super::{
|
||||
button_from_mouse_event, location_hash, modifiers_from_kb_event, modifiers_from_mouse_event,
|
||||
modifiers_from_wheel_event, native_pixels_per_point, pos_from_mouse_event,
|
||||
prefers_color_scheme_dark, primary_touch_pos, push_touches, text_from_keyboard_event,
|
||||
theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast, JsValue, WebRunner,
|
||||
DEBUG_RESIZE,
|
||||
AppRunner, Closure, DEBUG_RESIZE, JsCast as _, JsValue, WebRunner, button_from_mouse_event,
|
||||
location_hash, modifiers_from_kb_event, modifiers_from_mouse_event, modifiers_from_wheel_event,
|
||||
native_pixels_per_point, pos_from_mouse_event, prefers_color_scheme, primary_touch_pos,
|
||||
push_touches, text_from_keyboard_event, translate_key,
|
||||
};
|
||||
|
||||
use js_sys::Reflect;
|
||||
use web_sys::{Document, EventTarget, ShadowRoot};
|
||||
|
||||
// TODO(emilk): there are more calls to `prevent_default` and `stop_propagation`
|
||||
// than what is probably needed.
|
||||
|
||||
@@ -102,6 +102,7 @@ pub(crate) fn install_event_handlers(runner_ref: &WebRunner) -> Result<(), JsVal
|
||||
install_touchcancel(runner_ref, &canvas)?;
|
||||
|
||||
install_wheel(runner_ref, &canvas)?;
|
||||
install_gesture(runner_ref, &canvas)?;
|
||||
install_drag_and_drop(runner_ref, &canvas)?;
|
||||
install_window_events(runner_ref, &window)?;
|
||||
install_color_scheme_change_event(runner_ref, &window)?;
|
||||
@@ -139,15 +140,20 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J
|
||||
{
|
||||
if let Some(text) = text_from_keyboard_event(&event) {
|
||||
let egui_event = egui::Event::Text(text);
|
||||
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
let should_stop_propagation =
|
||||
(runner.web_options.should_stop_propagation)(&egui_event);
|
||||
let should_prevent_default =
|
||||
(runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// If this is indeed text, then prevent any other action.
|
||||
event.prevent_default();
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
|
||||
// Use web options to tell if the event should be propagated to parent elements.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
}
|
||||
@@ -158,7 +164,7 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
|
||||
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
|
||||
pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
let has_focus = runner.input.raw.focused;
|
||||
if !has_focus {
|
||||
@@ -184,7 +190,7 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
|
||||
repeat: false, // egui will fill this in for us!
|
||||
modifiers,
|
||||
};
|
||||
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
@@ -201,7 +207,7 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
|
||||
}
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
}
|
||||
@@ -221,9 +227,10 @@ fn should_prevent_default_for_key(
|
||||
|
||||
// Prevent cmd/ctrl plus these keys from triggering the default browser action:
|
||||
let keys = [
|
||||
egui::Key::O, // open
|
||||
egui::Key::P, // print (cmd-P is common for command palette)
|
||||
egui::Key::S, // save
|
||||
egui::Key::Comma, // cmd-, opens options on macOS, which egui apps may wanna "steal"
|
||||
egui::Key::O, // open
|
||||
egui::Key::P, // print (cmd-P is common for command palette)
|
||||
egui::Key::S, // save
|
||||
];
|
||||
for key in keys {
|
||||
if egui_key == key && (modifiers.ctrl || modifiers.command || modifiers.mac_cmd) {
|
||||
@@ -256,12 +263,12 @@ fn install_keyup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
|
||||
runner_ref.add_event_listener(target, "keyup", on_keyup)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
|
||||
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
|
||||
pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
let modifiers = modifiers_from_kb_event(&event);
|
||||
runner.input.raw.modifiers = modifiers;
|
||||
|
||||
let mut propagate_event = false;
|
||||
let mut should_stop_propagation = true;
|
||||
|
||||
if let Some(key) = translate_key(&event.key()) {
|
||||
let egui_event = egui::Event::Key {
|
||||
@@ -271,7 +278,7 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
repeat: false,
|
||||
modifiers,
|
||||
};
|
||||
propagate_event |= (runner.web_options.should_propagate_event)(&egui_event);
|
||||
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
}
|
||||
|
||||
@@ -282,6 +289,8 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
// See https://github.com/emilk/egui/issues/4724
|
||||
|
||||
let keys_down = runner.egui_ctx().input(|i| i.keys_down.clone());
|
||||
|
||||
#[expect(clippy::iter_over_hash_type)]
|
||||
for key in keys_down {
|
||||
let egui_event = egui::Event::Key {
|
||||
key,
|
||||
@@ -290,7 +299,7 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
repeat: false,
|
||||
modifiers,
|
||||
};
|
||||
propagate_event |= (runner.web_options.should_propagate_event)(&egui_event);
|
||||
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
}
|
||||
}
|
||||
@@ -299,70 +308,91 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
let has_focus = runner.input.raw.focused;
|
||||
if has_focus && !propagate_event {
|
||||
if has_focus && should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
}
|
||||
|
||||
fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
runner_ref.add_event_listener(target, "paste", |event: web_sys::ClipboardEvent, runner| {
|
||||
if !runner.input.raw.focused {
|
||||
return; // The eframe app is not interested
|
||||
}
|
||||
|
||||
if let Some(data) = event.clipboard_data() {
|
||||
if let Ok(text) = data.get_data("text") {
|
||||
let text = text.replace("\r\n", "\n");
|
||||
|
||||
let mut should_propagate = false;
|
||||
if !text.is_empty() && runner.input.raw.focused {
|
||||
let mut should_stop_propagation = true;
|
||||
let mut should_prevent_default = true;
|
||||
if !text.is_empty() {
|
||||
let egui_event = egui::Event::Paste(text);
|
||||
should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
should_stop_propagation =
|
||||
(runner.web_options.should_stop_propagation)(&egui_event);
|
||||
should_prevent_default =
|
||||
(runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
}
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
runner_ref.add_event_listener(target, "cut", |event: web_sys::ClipboardEvent, runner| {
|
||||
if runner.input.raw.focused {
|
||||
runner.input.raw.events.push(egui::Event::Cut);
|
||||
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
// event callback, which is why we run the app logic here and now:
|
||||
runner.logic();
|
||||
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
if !runner.input.raw.focused {
|
||||
return; // The eframe app is not interested
|
||||
}
|
||||
|
||||
runner.input.raw.events.push(egui::Event::Cut);
|
||||
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
// event callback, which is why we run the app logic here and now:
|
||||
runner.logic();
|
||||
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !(runner.web_options.should_propagate_event)(&egui::Event::Cut) {
|
||||
if (runner.web_options.should_stop_propagation)(&egui::Event::Cut) {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if (runner.web_options.should_prevent_default)(&egui::Event::Cut) {
|
||||
event.prevent_default();
|
||||
}
|
||||
})?;
|
||||
|
||||
runner_ref.add_event_listener(target, "copy", |event: web_sys::ClipboardEvent, runner| {
|
||||
if runner.input.raw.focused {
|
||||
runner.input.raw.events.push(egui::Event::Copy);
|
||||
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
// event callback, which is why we run the app logic here and now:
|
||||
runner.logic();
|
||||
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
if !runner.input.raw.focused {
|
||||
return; // The eframe app is not interested
|
||||
}
|
||||
|
||||
runner.input.raw.events.push(egui::Event::Copy);
|
||||
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
// event callback, which is why we run the app logic here and now:
|
||||
runner.logic();
|
||||
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !(runner.web_options.should_propagate_event)(&egui::Event::Copy) {
|
||||
if (runner.web_options.should_stop_propagation)(&egui::Event::Copy) {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if (runner.web_options.should_prevent_default)(&egui::Event::Copy) {
|
||||
event.prevent_default();
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -380,7 +410,7 @@ fn install_window_events(runner_ref: &WebRunner, window: &EventTarget) -> Result
|
||||
|
||||
// No need to subscribe to "resize": we already subscribe to the canvas
|
||||
// size using a ResizeObserver, and we also subscribe to DPR changes of the monitor.
|
||||
for event_name in &["load", "pagehide", "pageshow"] {
|
||||
for event_name in &["load", "pagehide", "pageshow", "popstate"] {
|
||||
runner_ref.add_event_listener(window, event_name, move |_: web_sys::Event, runner| {
|
||||
if DEBUG_RESIZE {
|
||||
log::debug!("{event_name:?}");
|
||||
@@ -444,16 +474,19 @@ fn install_color_scheme_change_event(
|
||||
runner_ref: &WebRunner,
|
||||
window: &web_sys::Window,
|
||||
) -> Result<(), JsValue> {
|
||||
if let Some(media_query_list) = prefers_color_scheme_dark(window)? {
|
||||
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
|
||||
&media_query_list,
|
||||
"change",
|
||||
|event, runner| {
|
||||
let theme = theme_from_dark_mode(event.matches());
|
||||
runner.input.raw.system_theme = Some(theme);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
for theme in [egui::Theme::Dark, egui::Theme::Light] {
|
||||
if let Some(media_query_list) = prefers_color_scheme(window, theme)? {
|
||||
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
|
||||
&media_query_list,
|
||||
"change",
|
||||
|_event, runner| {
|
||||
if let Some(theme) = super::system_theme() {
|
||||
runner.input.raw.system_theme = Some(theme);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
}
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -484,7 +517,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
|
||||
|event: web_sys::PointerEvent, runner: &mut AppRunner| {
|
||||
let modifiers = modifiers_from_mouse_event(&event);
|
||||
runner.input.raw.modifiers = modifiers;
|
||||
let mut should_propagate = false;
|
||||
let mut should_stop_propagation = true;
|
||||
if let Some(button) = button_from_mouse_event(&event) {
|
||||
let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx());
|
||||
let modifiers = runner.input.raw.modifiers;
|
||||
@@ -494,7 +527,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
|
||||
pressed: true,
|
||||
modifiers,
|
||||
};
|
||||
should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
@@ -506,7 +539,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
|
||||
}
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
// Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here.
|
||||
@@ -536,7 +569,10 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
pressed: false,
|
||||
modifiers,
|
||||
};
|
||||
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
let should_stop_propagation =
|
||||
(runner.web_options.should_stop_propagation)(&egui_event);
|
||||
let should_prevent_default =
|
||||
(runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
|
||||
// Previously on iOS, the canvas would not receive focus on
|
||||
@@ -555,10 +591,12 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
event.prevent_default();
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
}
|
||||
@@ -570,10 +608,17 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
/// Returns true if the cursor is above the canvas, or if we're dragging something.
|
||||
/// Pass in the position in browser viewport coordinates (usually event.clientX/Y).
|
||||
fn is_interested_in_pointer_event(runner: &AppRunner, pos: egui::Pos2) -> bool {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
let is_hovering_canvas = document
|
||||
.element_from_point(pos.x, pos.y)
|
||||
.is_some_and(|element| element.eq(runner.canvas()));
|
||||
let root_node = runner.canvas().get_root_node();
|
||||
|
||||
let element_at_point = if let Some(document) = root_node.dyn_ref::<Document>() {
|
||||
document.element_from_point(pos.x, pos.y)
|
||||
} else if let Some(shadow) = root_node.dyn_ref::<ShadowRoot>() {
|
||||
shadow.element_from_point(pos.x, pos.y)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let is_hovering_canvas = element_at_point.is_some_and(|element| element.eq(runner.canvas()));
|
||||
let is_pointer_down = runner
|
||||
.egui_ctx()
|
||||
.input(|i| i.pointer.any_down() || i.any_touches());
|
||||
@@ -593,15 +638,19 @@ fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
egui::pos2(event.client_x() as f32, event.client_y() as f32),
|
||||
) {
|
||||
let egui_event = egui::Event::PointerMoved(pos);
|
||||
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
runner.needs_repaint.repaint();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -615,10 +664,13 @@ fn install_mouseleave(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !(runner.web_options.should_propagate_event)(&egui::Event::PointerGone) {
|
||||
if (runner.web_options.should_stop_propagation)(&egui::Event::PointerGone) {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if (runner.web_options.should_prevent_default)(&egui::Event::PointerGone) {
|
||||
event.prevent_default();
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -628,7 +680,8 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
|
||||
target,
|
||||
"touchstart",
|
||||
|event: web_sys::TouchEvent, runner| {
|
||||
let mut should_propagate = false;
|
||||
let mut should_stop_propagation = true;
|
||||
let mut should_prevent_default = true;
|
||||
if let Some((pos, _)) = primary_touch_pos(runner, &event) {
|
||||
let egui_event = egui::Event::PointerButton {
|
||||
pos,
|
||||
@@ -636,7 +689,8 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
|
||||
pressed: true,
|
||||
modifiers: runner.input.raw.modifiers,
|
||||
};
|
||||
should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
}
|
||||
|
||||
@@ -644,10 +698,13 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -660,17 +717,23 @@ fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
egui::pos2(touch.client_x() as f32, touch.client_y() as f32),
|
||||
) {
|
||||
let egui_event = egui::Event::PointerMoved(pos);
|
||||
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
let should_stop_propagation =
|
||||
(runner.web_options.should_stop_propagation)(&egui_event);
|
||||
let should_prevent_default =
|
||||
(runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
|
||||
push_touches(runner, egui::TouchPhase::Move, &event);
|
||||
runner.needs_repaint.repaint_asap();
|
||||
runner.needs_repaint.repaint();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -684,18 +747,23 @@ fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
egui::pos2(touch.client_x() as f32, touch.client_y() as f32),
|
||||
) {
|
||||
// First release mouse to click:
|
||||
let mut should_propagate = false;
|
||||
let mut should_stop_propagation = true;
|
||||
let mut should_prevent_default = true;
|
||||
let egui_event = egui::Event::PointerButton {
|
||||
pos,
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed: false,
|
||||
modifiers: runner.input.raw.modifiers,
|
||||
};
|
||||
should_propagate |= (runner.web_options.should_propagate_event)(&egui_event);
|
||||
should_stop_propagation &=
|
||||
(runner.web_options.should_stop_propagation)(&egui_event);
|
||||
should_prevent_default &= (runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
// Then remove hover effect:
|
||||
should_propagate |=
|
||||
(runner.web_options.should_propagate_event)(&egui::Event::PointerGone);
|
||||
should_stop_propagation &=
|
||||
(runner.web_options.should_stop_propagation)(&egui::Event::PointerGone);
|
||||
should_prevent_default &=
|
||||
(runner.web_options.should_prevent_default)(&egui::Event::PointerGone);
|
||||
runner.input.raw.events.push(egui::Event::PointerGone);
|
||||
|
||||
push_touches(runner, egui::TouchPhase::End, &event);
|
||||
@@ -703,10 +771,13 @@ fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
|
||||
// Fix virtual keyboard IOS
|
||||
// Need call focus at the same time of event
|
||||
@@ -748,7 +819,7 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
|
||||
|
||||
let egui_event = if modifiers.ctrl && !runner.input.raw.modifiers.ctrl {
|
||||
// The browser is saying the ctrl key is down, but it isn't _really_.
|
||||
// This happens on pinch-to-zoom on a Mac trackpad.
|
||||
// This happens on pinch-to-zoom on multitouch trackpads
|
||||
// egui will treat ctrl+scroll as zoom, so it all works.
|
||||
// However, we explicitly handle it here in order to better match the pinch-to-zoom
|
||||
// speed of a native app, without being sensitive to egui's `scroll_zoom_speed` setting.
|
||||
@@ -760,19 +831,91 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
|
||||
unit,
|
||||
delta,
|
||||
modifiers,
|
||||
phase: egui::TouchPhase::Move,
|
||||
}
|
||||
};
|
||||
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
|
||||
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
|
||||
runner.input.raw.events.push(egui_event);
|
||||
|
||||
runner.needs_repaint.repaint();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
|
||||
if should_prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn install_gesture(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
runner_ref.add_event_listener(target, "gesturestart", |event: web_sys::Event, runner| {
|
||||
runner.input.accumulated_scale = 1.0;
|
||||
runner.input.accumulated_rotation = 0.0;
|
||||
handle_gesture(event, runner);
|
||||
})?;
|
||||
runner_ref.add_event_listener(target, "gesturechange", handle_gesture)?;
|
||||
runner_ref.add_event_listener(target, "gestureend", |event: web_sys::Event, runner| {
|
||||
handle_gesture(event, runner);
|
||||
runner.input.accumulated_scale = 1.0;
|
||||
runner.input.accumulated_rotation = 0.0;
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
|
||||
fn handle_gesture(event: web_sys::Event, runner: &mut AppRunner) {
|
||||
// GestureEvent is a non-standard API, so this attempts to get the relevant fields if they exist.
|
||||
let new_scale = Reflect::get(&event, &JsValue::from_str("scale"))
|
||||
.ok()
|
||||
.and_then(|scale| scale.as_f64())
|
||||
.map_or(1.0, |scale| scale as f32);
|
||||
let new_rotation = Reflect::get(&event, &JsValue::from_str("rotation"))
|
||||
.ok()
|
||||
.and_then(|rotation| rotation.as_f64())
|
||||
.map_or(0.0, |rotation| rotation.to_radians() as f32);
|
||||
|
||||
let scale_delta = new_scale / runner.input.accumulated_scale;
|
||||
let rotation_delta = new_rotation - runner.input.accumulated_rotation;
|
||||
runner.input.accumulated_scale *= scale_delta;
|
||||
runner.input.accumulated_rotation += rotation_delta;
|
||||
|
||||
let mut should_stop_propagation = true;
|
||||
let mut should_prevent_default = true;
|
||||
|
||||
if scale_delta != 1.0 {
|
||||
let zoom_event = egui::Event::Zoom(scale_delta);
|
||||
|
||||
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&zoom_event);
|
||||
should_prevent_default &= (runner.web_options.should_prevent_default)(&zoom_event);
|
||||
runner.input.raw.events.push(zoom_event);
|
||||
}
|
||||
|
||||
if rotation_delta != 0.0 {
|
||||
let rotate_event = egui::Event::Rotate(rotation_delta);
|
||||
|
||||
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&rotate_event);
|
||||
should_prevent_default &= (runner.web_options.should_prevent_default)(&rotate_event);
|
||||
runner.input.raw.events.push(rotate_event);
|
||||
}
|
||||
|
||||
if scale_delta != 1.0 || rotation_delta != 0.0 {
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if !should_propagate {
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
event.prevent_default();
|
||||
})
|
||||
|
||||
if should_prevent_default {
|
||||
// Prevents a simulated ctrl-scroll event for zoom
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
@@ -856,7 +999,10 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to read file: {:?}", err);
|
||||
log::error!(
|
||||
"Failed to read file: {}",
|
||||
string_from_js_value(&err)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -925,7 +1071,7 @@ impl ResizeObserverContext {
|
||||
// we rely on the resize observer to trigger the first `request_animation_frame`:
|
||||
if let Err(err) = runner_ref.request_animation_frame() {
|
||||
log::error!("{}", super::string_from_js_value(&err));
|
||||
};
|
||||
}
|
||||
} else {
|
||||
log::warn!("ResizeObserverContext callback: failed to lock runner");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{canvas_content_rect, AppRunner};
|
||||
use super::{AppRunner, canvas_content_rect};
|
||||
|
||||
pub fn pos_from_mouse_event(
|
||||
canvas: &web_sys::HtmlCanvasElement,
|
||||
|
||||
@@ -23,25 +23,22 @@ pub use panic_handler::{PanicHandler, PanicSummary};
|
||||
pub use web_logger::WebLogger;
|
||||
pub use web_runner::WebRunner;
|
||||
|
||||
#[cfg(not(any(feature = "glow", feature = "wgpu")))]
|
||||
#[cfg(not(any(feature = "glow", feature = "wgpu_no_default_features")))]
|
||||
compile_error!("You must enable either the 'glow' or 'wgpu' feature");
|
||||
|
||||
mod web_painter;
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
mod web_painter_glow;
|
||||
#[cfg(feature = "glow")]
|
||||
pub(crate) type ActiveWebPainter = web_painter_glow::WebPainterGlow;
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
mod web_painter_wgpu;
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
|
||||
|
||||
pub use backend::*;
|
||||
|
||||
use egui::Theme;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::MediaQueryList;
|
||||
use web_sys::{Document, MediaQueryList, Node};
|
||||
|
||||
use input::{
|
||||
button_from_mouse_event, modifiers_from_kb_event, modifiers_from_mouse_event,
|
||||
@@ -64,18 +61,22 @@ pub(crate) fn string_from_js_value(value: &JsValue) -> String {
|
||||
/// - `<a>`/`<area>` with an `href` attribute
|
||||
/// - `<input>`/`<select>`/`<textarea>`/`<button>` which aren't `disabled`
|
||||
/// - any other element with a `tabindex` attribute
|
||||
pub(crate) fn focused_element() -> Option<web_sys::Element> {
|
||||
web_sys::window()?
|
||||
.document()?
|
||||
.active_element()?
|
||||
.dyn_into()
|
||||
.ok()
|
||||
pub(crate) fn focused_element(root: &Node) -> Option<web_sys::Element> {
|
||||
if let Some(document) = root.dyn_ref::<Document>() {
|
||||
document.active_element()
|
||||
} else if let Some(shadow) = root.dyn_ref::<web_sys::ShadowRoot>() {
|
||||
shadow.active_element()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_focus<T: JsCast>(element: &T) -> bool {
|
||||
fn try_has_focus<T: JsCast>(element: &T) -> Option<bool> {
|
||||
let element = element.dyn_ref::<web_sys::Element>()?;
|
||||
let focused_element = focused_element()?;
|
||||
let root = element.get_root_node();
|
||||
|
||||
let focused_element = focused_element(&root)?;
|
||||
Some(element == &focused_element)
|
||||
}
|
||||
try_has_focus(element).unwrap_or(false)
|
||||
@@ -109,24 +110,31 @@ pub fn native_pixels_per_point() -> f32 {
|
||||
///
|
||||
/// `None` means unknown.
|
||||
pub fn system_theme() -> Option<egui::Theme> {
|
||||
let dark_mode = prefers_color_scheme_dark(&web_sys::window()?)
|
||||
.ok()??
|
||||
.matches();
|
||||
Some(theme_from_dark_mode(dark_mode))
|
||||
}
|
||||
|
||||
fn prefers_color_scheme_dark(window: &web_sys::Window) -> Result<Option<MediaQueryList>, JsValue> {
|
||||
window.match_media("(prefers-color-scheme: dark)")
|
||||
}
|
||||
|
||||
fn theme_from_dark_mode(dark_mode: bool) -> egui::Theme {
|
||||
if dark_mode {
|
||||
egui::Theme::Dark
|
||||
let window = web_sys::window()?;
|
||||
if does_prefer_color_scheme(&window, Theme::Dark) == Some(true) {
|
||||
Some(Theme::Dark)
|
||||
} else if does_prefer_color_scheme(&window, Theme::Light) == Some(true) {
|
||||
Some(Theme::Light)
|
||||
} else {
|
||||
egui::Theme::Light
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn does_prefer_color_scheme(window: &web_sys::Window, theme: Theme) -> Option<bool> {
|
||||
Some(prefers_color_scheme(window, theme).ok()??.matches())
|
||||
}
|
||||
|
||||
fn prefers_color_scheme(
|
||||
window: &web_sys::Window,
|
||||
theme: Theme,
|
||||
) -> Result<Option<MediaQueryList>, JsValue> {
|
||||
let theme = match theme {
|
||||
Theme::Dark => "dark",
|
||||
Theme::Light => "light",
|
||||
};
|
||||
window.match_media(format!("(prefers-color-scheme: {theme})").as_str())
|
||||
}
|
||||
|
||||
/// Returns the canvas in client coordinates.
|
||||
fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect {
|
||||
let bounding_rect = canvas.get_bounding_client_rect();
|
||||
@@ -277,7 +285,7 @@ fn create_clipboard_item(mime: &str, bytes: &[u8]) -> Result<web_sys::ClipboardI
|
||||
let items = js_sys::Object::new();
|
||||
|
||||
// SAFETY: I hope so
|
||||
#[allow(unsafe_code, unused_unsafe)] // Weird false positive
|
||||
#[expect(unsafe_code, unused_unsafe)] // Weird false positive
|
||||
unsafe {
|
||||
js_sys::Reflect::set(&items, &JsValue::from_str(mime), &blob)?
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{Document, Node};
|
||||
|
||||
use super::{AppRunner, WebRunner};
|
||||
|
||||
@@ -14,7 +15,7 @@ pub struct TextAgent {
|
||||
|
||||
impl TextAgent {
|
||||
/// Attach the agent to the document.
|
||||
pub fn attach(runner_ref: &WebRunner) -> Result<Self, JsValue> {
|
||||
pub fn attach(runner_ref: &WebRunner, root: Node) -> Result<Self, JsValue> {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
// create an `<input>` element
|
||||
@@ -37,7 +38,17 @@ impl TextAgent {
|
||||
style.set_property("position", "absolute")?;
|
||||
style.set_property("top", "0")?;
|
||||
style.set_property("left", "0")?;
|
||||
document.body().unwrap().append_child(&input)?;
|
||||
|
||||
if root.has_type::<Document>() {
|
||||
// root object is a document, append to its body
|
||||
root.dyn_into::<Document>()?
|
||||
.body()
|
||||
.unwrap()
|
||||
.append_child(&input)?;
|
||||
} else {
|
||||
// append input into root directly
|
||||
root.append_child(&input)?;
|
||||
}
|
||||
|
||||
// attach event listeners
|
||||
|
||||
@@ -168,7 +179,7 @@ impl TextAgent {
|
||||
|
||||
if let Err(err) = self.input.focus() {
|
||||
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blur(&self) {
|
||||
@@ -180,7 +191,7 @@ impl TextAgent {
|
||||
|
||||
if let Err(err) = self.input.blur() {
|
||||
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ mod console {
|
||||
/// * `tokio-1.24.1/src/runtime/runtime.rs`
|
||||
/// * `rerun/src/main.rs`
|
||||
/// * `core/src/ops/function.rs`
|
||||
#[allow(dead_code)] // only used on web and in tests
|
||||
#[allow(dead_code, clippy::allow_attributes)] // only used on web and in tests
|
||||
fn shorten_file_path(file_path: &str) -> &str {
|
||||
if let Some(i) = file_path.rfind("/src/") {
|
||||
if let Some(prev_slash) = file_path[..i].rfind('/') {
|
||||
@@ -126,12 +126,17 @@ fn shorten_file_path(file_path: &str) -> &str {
|
||||
#[test]
|
||||
fn test_shorten_file_path() {
|
||||
for (before, after) in [
|
||||
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
|
||||
(
|
||||
"/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
|
||||
"tokio-1.24.1/src/runtime/runtime.rs",
|
||||
),
|
||||
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
|
||||
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
|
||||
(
|
||||
"/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
|
||||
"core/src/ops/function.rs",
|
||||
),
|
||||
("/weird/path/file.rs", "/weird/path/file.rs"),
|
||||
]
|
||||
{
|
||||
] {
|
||||
assert_eq!(shorten_file_path(before), after);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use egui::{Event, UserData, ViewportId};
|
||||
use egui_glow::glow;
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsCast as _;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
@@ -20,14 +20,15 @@ impl WebPainterGlow {
|
||||
self.painter.gl()
|
||||
}
|
||||
|
||||
pub async fn new(
|
||||
pub fn new(
|
||||
_ctx: egui::Context,
|
||||
canvas: HtmlCanvasElement,
|
||||
options: &WebOptions,
|
||||
) -> Result<Self, String> {
|
||||
let (gl, shader_prefix) =
|
||||
init_glow_context_from_canvas(&canvas, options.webgl_context_option)?;
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm
|
||||
let gl = std::sync::Arc::new(gl);
|
||||
|
||||
let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering)
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::web_painter::WebPainter;
|
||||
use crate::WebOptions;
|
||||
use egui::{Event, UserData, ViewportId};
|
||||
use egui_wgpu::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
|
||||
use egui_wgpu::{RenderState, SurfaceErrorAction};
|
||||
use egui_wgpu::{
|
||||
RenderState, SurfaceErrorAction,
|
||||
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
||||
};
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
use super::web_painter::WebPainter;
|
||||
|
||||
pub(crate) struct WebPainterWgpu {
|
||||
canvas: HtmlCanvasElement,
|
||||
surface: wgpu::Surface<'static>,
|
||||
surface_configuration: wgpu::SurfaceConfiguration,
|
||||
render_state: Option<RenderState>,
|
||||
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
|
||||
depth_format: Option<wgpu::TextureFormat>,
|
||||
depth_stencil_format: Option<wgpu::TextureFormat>,
|
||||
depth_texture_view: Option<wgpu::TextureView>,
|
||||
screen_capture_state: Option<CaptureState>,
|
||||
capture_tx: CaptureSender,
|
||||
@@ -23,7 +25,6 @@ pub(crate) struct WebPainterWgpu {
|
||||
}
|
||||
|
||||
impl WebPainterWgpu {
|
||||
#[allow(unused)] // only used if `wgpu` is the only active feature.
|
||||
pub fn render_state(&self) -> Option<RenderState> {
|
||||
self.render_state.clone()
|
||||
}
|
||||
@@ -35,7 +36,7 @@ impl WebPainterWgpu {
|
||||
height_in_pixels: u32,
|
||||
) -> Option<wgpu::TextureView> {
|
||||
let device = &render_state.device;
|
||||
self.depth_format.map(|depth_format| {
|
||||
self.depth_stencil_format.map(|depth_stencil_format| {
|
||||
device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_depth_texture"),
|
||||
@@ -47,19 +48,18 @@ impl WebPainterWgpu {
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: depth_format,
|
||||
format: depth_stencil_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[depth_format],
|
||||
view_formats: &[depth_stencil_format],
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(unused)] // only used if `wgpu` is the only active feature.
|
||||
pub async fn new(
|
||||
ctx: egui::Context,
|
||||
canvas: web_sys::HtmlCanvasElement,
|
||||
options: &WebOptions,
|
||||
options: &crate::WebOptions,
|
||||
) -> Result<Self, String> {
|
||||
log::debug!("Creating wgpu painter");
|
||||
|
||||
@@ -68,15 +68,17 @@ impl WebPainterWgpu {
|
||||
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
|
||||
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
|
||||
|
||||
let depth_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);
|
||||
let depth_stencil_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);
|
||||
|
||||
let render_state = RenderState::create(
|
||||
&options.wgpu_options,
|
||||
&instance,
|
||||
Some(&surface),
|
||||
depth_format,
|
||||
1,
|
||||
options.dithering,
|
||||
egui_wgpu::RendererOptions {
|
||||
dithering: options.dithering,
|
||||
depth_stencil_format,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|err| err.to_string())?;
|
||||
@@ -101,7 +103,7 @@ impl WebPainterWgpu {
|
||||
render_state: Some(render_state),
|
||||
surface,
|
||||
surface_configuration,
|
||||
depth_format,
|
||||
depth_stencil_format,
|
||||
depth_texture_view: None,
|
||||
on_surface_error: options.wgpu_options.on_surface_error.clone(),
|
||||
screen_capture_state: None,
|
||||
@@ -236,6 +238,7 @@ impl WebPainter for WebPainterWgpu {
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
|
||||
wgpu::RenderPassDepthStencilAttachment {
|
||||
@@ -274,18 +277,11 @@ impl WebPainter for WebPainterWgpu {
|
||||
&mut encoder,
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Some((output_frame, capture_buffer))
|
||||
};
|
||||
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for id in &textures_delta.free {
|
||||
renderer.free_texture(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the commands: both the main buffer and user-defined ones.
|
||||
render_state
|
||||
.queue
|
||||
@@ -307,6 +303,16 @@ impl WebPainter for WebPainterWgpu {
|
||||
frame.present();
|
||||
}
|
||||
|
||||
// Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
|
||||
// Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
|
||||
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for id in &textures_delta.free {
|
||||
renderer.free_texture(id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{epi, App};
|
||||
use crate::{App, epi};
|
||||
|
||||
use super::{
|
||||
AppRunner, PanicHandler,
|
||||
events::{self, ResizeObserverContext},
|
||||
text_agent::TextAgent,
|
||||
AppRunner, PanicHandler,
|
||||
};
|
||||
|
||||
/// This is how `eframe` runs your web application
|
||||
@@ -37,7 +37,7 @@ pub struct WebRunner {
|
||||
|
||||
impl WebRunner {
|
||||
/// Will install a panic handler that will catch and log any panics
|
||||
#[allow(clippy::new_without_default)]
|
||||
#[expect(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
let panic_handler = PanicHandler::install();
|
||||
|
||||
@@ -73,7 +73,7 @@ impl WebRunner {
|
||||
|
||||
{
|
||||
// First set up the app runner:
|
||||
let text_agent = TextAgent::attach(self)?;
|
||||
let text_agent = TextAgent::attach(self, canvas.get_root_node())?;
|
||||
let app_runner =
|
||||
AppRunner::new(canvas.clone(), web_options, app_creator, text_agent).await?;
|
||||
self.app_runner.replace(Some(app_runner));
|
||||
@@ -280,7 +280,7 @@ struct TargetEvent {
|
||||
closure: Closure<dyn FnMut(web_sys::Event)>,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[expect(unused)]
|
||||
struct IntervalHandle {
|
||||
handle: i32,
|
||||
closure: Closure<dyn FnMut()>,
|
||||
@@ -289,7 +289,7 @@ struct IntervalHandle {
|
||||
enum EventToUnsubscribe {
|
||||
TargetEvent(TargetEvent),
|
||||
|
||||
#[allow(unused)]
|
||||
#[expect(unused)]
|
||||
IntervalHandle(IntervalHandle),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user