mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 12:50:04 -04:00
Move all crates into a crates directory (#1940)
This commit is contained in:
833
crates/eframe/src/epi.rs
Normal file
833
crates/eframe/src/epi.rs
Normal file
@@ -0,0 +1,833 @@
|
||||
//! Platform-agnostic interface for writing apps using [`egui`] (epi = egui programming interface).
|
||||
//!
|
||||
//! `epi` provides interfaces for window management and serialization.
|
||||
//!
|
||||
//! Start by looking at the [`App`] trait, and implement [`App::update`].
|
||||
|
||||
#![warn(missing_docs)] // Let's keep `epi` well-documented.
|
||||
|
||||
/// This is how your app is created.
|
||||
///
|
||||
/// You can use the [`CreationContext`] to setup egui, restore state, setup OpenGL things, etc.
|
||||
pub type AppCreator = Box<dyn FnOnce(&CreationContext<'_>) -> Box<dyn App>>;
|
||||
|
||||
/// Data that is passed to [`AppCreator`] that can be used to setup and initialize your app.
|
||||
pub struct CreationContext<'s> {
|
||||
/// The egui Context.
|
||||
///
|
||||
/// You can use this to customize the look of egui, e.g to call [`egui::Context::set_fonts`],
|
||||
/// [`egui::Context::set_visuals`] etc.
|
||||
pub egui_ctx: egui::Context,
|
||||
|
||||
/// Information about the surrounding environment.
|
||||
pub integration_info: IntegrationInfo,
|
||||
|
||||
/// You can use the storage to restore app state(requires the "persistence" feature).
|
||||
pub storage: Option<&'s dyn Storage>,
|
||||
|
||||
/// The [`glow::Context`] allows you to initialize OpenGL resources (e.g. shaders) that
|
||||
/// you might want to use later from a [`egui::PaintCallback`].
|
||||
///
|
||||
/// Only available when compiling with the `glow` feature and using [`Renderer::Glow`].
|
||||
#[cfg(feature = "glow")]
|
||||
pub gl: Option<std::sync::Arc<glow::Context>>,
|
||||
|
||||
/// The underlying WGPU render state.
|
||||
///
|
||||
/// 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")]
|
||||
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// 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/eframe).
|
||||
pub trait App {
|
||||
/// Called each time the UI needs repainting, which may be many times per second.
|
||||
///
|
||||
/// Put your widgets into a [`egui::SidePanel`], [`egui::TopBottomPanel`], [`egui::CentralPanel`], [`egui::Window`] or [`egui::Area`].
|
||||
///
|
||||
/// The [`egui::Context`] can be cloned and saved if you like.
|
||||
///
|
||||
/// To force a repaint, call [`egui::Context::request_repaint`] at any time (e.g. from another thread).
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame);
|
||||
|
||||
/// Called on shutdown, and perhaps at regular intervals. Allows you to save state.
|
||||
///
|
||||
/// Only called when the "persistence" feature is enabled.
|
||||
///
|
||||
/// On web the state is stored to "Local Storage".
|
||||
/// On native the path is picked using [`directories_next::ProjectDirs::data_dir`](https://docs.rs/directories-next/2.0.0/directories_next/struct.ProjectDirs.html#method.data_dir) which is:
|
||||
/// * Linux: `/home/UserName/.local/share/APPNAME`
|
||||
/// * macOS: `/Users/UserName/Library/Application Support/APPNAME`
|
||||
/// * Windows: `C:\Users\UserName\AppData\Roaming\APPNAME`
|
||||
///
|
||||
/// where `APPNAME` is what is given to `eframe::run_native`.
|
||||
fn save(&mut self, _storage: &mut dyn Storage) {}
|
||||
|
||||
/// Called before an exit that can be aborted.
|
||||
/// By returning `false` the exit will be aborted. To continue the exit return `true`.
|
||||
///
|
||||
/// A scenario where this method will be run is after pressing the close button on a native
|
||||
/// window, which allows you to ask the user whether they want to do something before exiting.
|
||||
/// See the example at <https://github.com/emilk/egui/blob/master/examples/confirm_exit/> for practical usage.
|
||||
///
|
||||
/// It will _not_ be called on the web or when the window is forcefully closed.
|
||||
fn on_exit_event(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Called once on shutdown, after [`Self::save`].
|
||||
///
|
||||
/// If you need to abort an exit use [`Self::on_exit_event`].
|
||||
///
|
||||
/// To get a [`glow`] context you need to compile with the `glow` feature flag,
|
||||
/// and run eframe with the glow backend.
|
||||
#[cfg(feature = "glow")]
|
||||
fn on_exit(&mut self, _gl: Option<&glow::Context>) {}
|
||||
|
||||
/// Called once on shutdown, after [`Self::save`].
|
||||
///
|
||||
/// If you need to abort an exit use [`Self::on_exit_event`].
|
||||
#[cfg(not(feature = "glow"))]
|
||||
fn on_exit(&mut self) {}
|
||||
|
||||
// ---------
|
||||
// Settings:
|
||||
|
||||
/// Time between automatic calls to [`Self::save`]
|
||||
fn auto_save_interval(&self) -> std::time::Duration {
|
||||
std::time::Duration::from_secs(30)
|
||||
}
|
||||
|
||||
/// The size limit of the web app canvas.
|
||||
///
|
||||
/// By default the max size is [`egui::Vec2::INFINITY`], i.e. unlimited.
|
||||
///
|
||||
/// A large canvas can lead to bad frame rates on some older browsers on some platforms
|
||||
/// (see <https://bugzilla.mozilla.org/show_bug.cgi?id=1010527#c0>).
|
||||
fn max_size_points(&self) -> egui::Vec2 {
|
||||
egui::Vec2::INFINITY
|
||||
}
|
||||
|
||||
/// Background color for the app, e.g. what is sent to `gl.clearColor`.
|
||||
/// This is the background of your windows if you don't set a central panel.
|
||||
fn clear_color(&self, _visuals: &egui::Visuals) -> egui::Rgba {
|
||||
// NOTE: a bright gray makes the shadows of the windows look weird.
|
||||
// We use a bit of transparency so that if the user switches on the
|
||||
// `transparent()` option they get immediate results.
|
||||
egui::Color32::from_rgba_unmultiplied(12, 12, 12, 180).into()
|
||||
|
||||
// _visuals.window_fill() would also be a natural choice
|
||||
}
|
||||
|
||||
/// Controls whether or not the native window position and size will be
|
||||
/// persisted (only if the "persistence" feature is enabled).
|
||||
fn persist_native_window(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Controls whether or not the egui memory (window positions etc) will be
|
||||
/// persisted (only if the "persistence" feature is enabled).
|
||||
fn persist_egui_memory(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// If `true` a warm-up call to [`Self::update`] will be issued where
|
||||
/// `ctx.memory().everything_is_visible()` will be set to `true`.
|
||||
///
|
||||
/// This can help pre-caching resources loaded by different parts of the UI, preventing stutter later on.
|
||||
///
|
||||
/// In this warm-up call, all painted shapes will be ignored.
|
||||
///
|
||||
/// The default is `false`, and it is unlikely you will want to change this.
|
||||
fn warm_up_enabled(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Called each time after the rendering the UI.
|
||||
///
|
||||
/// Can be used to access pixel data with `get_pixels`
|
||||
fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &Frame) {}
|
||||
}
|
||||
|
||||
/// Selects the level of hardware graphics acceleration.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum HardwareAcceleration {
|
||||
/// Require graphics acceleration.
|
||||
Required,
|
||||
|
||||
/// Prefer graphics acceleration, but fall back to software.
|
||||
Preferred,
|
||||
|
||||
/// Do NOT use graphics acceleration.
|
||||
///
|
||||
/// On some platforms (MacOS) this is ignored and treated the same as [`Self::Preferred`].
|
||||
Off,
|
||||
}
|
||||
|
||||
/// Options controlling the behavior of a native window.
|
||||
///
|
||||
/// Only a single native window is currently supported.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Clone)]
|
||||
pub struct NativeOptions {
|
||||
/// Sets whether or not the window will always be on top of other windows.
|
||||
pub always_on_top: bool,
|
||||
|
||||
/// Show window in maximized mode
|
||||
pub maximized: bool,
|
||||
|
||||
/// On desktop: add window decorations (i.e. a frame around your app)?
|
||||
/// If false it will be difficult to move and resize the app.
|
||||
pub decorated: bool,
|
||||
|
||||
/// Start in (borderless) fullscreen?
|
||||
///
|
||||
/// Default: `false`.
|
||||
pub fullscreen: bool,
|
||||
|
||||
/// On Windows: enable drag and drop support. Drag and drop can
|
||||
/// not be disabled on other platforms.
|
||||
///
|
||||
/// See [winit's documentation][drag_and_drop] for information on why you
|
||||
/// might want to disable this on windows.
|
||||
///
|
||||
/// [drag_and_drop]: https://docs.rs/winit/latest/x86_64-pc-windows-msvc/winit/platform/windows/trait.WindowBuilderExtWindows.html#tymethod.with_drag_and_drop
|
||||
pub drag_and_drop_support: bool,
|
||||
|
||||
/// The application icon, e.g. in the Windows task bar etc.
|
||||
///
|
||||
/// This doesn't work on Mac and on Wayland.
|
||||
/// See <https://docs.rs/winit/latest/winit/window/struct.Window.html#method.set_window_icon> for more.
|
||||
pub icon_data: Option<IconData>,
|
||||
|
||||
/// The initial (inner) position of the native window in points (logical pixels).
|
||||
pub initial_window_pos: Option<egui::Pos2>,
|
||||
|
||||
/// The initial inner size of the native window in points (logical pixels).
|
||||
pub initial_window_size: Option<egui::Vec2>,
|
||||
|
||||
/// The minimum inner window size
|
||||
pub min_window_size: Option<egui::Vec2>,
|
||||
|
||||
/// The maximum inner window size
|
||||
pub max_window_size: Option<egui::Vec2>,
|
||||
|
||||
/// Should the app window be resizable?
|
||||
pub resizable: bool,
|
||||
|
||||
/// On desktop: make the window transparent.
|
||||
/// You control the transparency with [`App::clear_color()`].
|
||||
/// You should avoid having a [`egui::CentralPanel`], or make sure its frame is also transparent.
|
||||
pub transparent: bool,
|
||||
|
||||
/// Turn on vertical syncing, limiting the FPS to the display refresh rate.
|
||||
///
|
||||
/// The default is `true`.
|
||||
pub vsync: bool,
|
||||
|
||||
/// Set the level of the multisampling anti-aliasing (MSAA).
|
||||
///
|
||||
/// Must be a power-of-two. Higher = more smooth 3D.
|
||||
///
|
||||
/// A value of `0` turns it off (default).
|
||||
///
|
||||
/// `egui` already performs anti-aliasing via "feathering"
|
||||
/// (controlled by [`egui::epaint::TessellationOptions`]),
|
||||
/// but if you are embedding 3D in egui you may want to turn on multisampling.
|
||||
pub multisampling: u16,
|
||||
|
||||
/// Sets the number of bits in the depth buffer.
|
||||
///
|
||||
/// `egui` doesn't need the depth buffer, so the default value is 0.
|
||||
pub depth_buffer: u8,
|
||||
|
||||
/// Sets the number of bits in the stencil buffer.
|
||||
///
|
||||
/// `egui` doesn't need the stencil buffer, so the default value is 0.
|
||||
pub stencil_buffer: u8,
|
||||
|
||||
/// Specify wether or not hardware acceleration is preferred, required, or not.
|
||||
///
|
||||
/// Default: [`HardwareAcceleration::Preferred`].
|
||||
pub hardware_acceleration: HardwareAcceleration,
|
||||
|
||||
/// What rendering backend to use.
|
||||
pub renderer: Renderer,
|
||||
|
||||
/// If the `dark-light` feature is enabled:
|
||||
///
|
||||
/// Try to detect and follow the system preferred setting for dark vs light mode.
|
||||
///
|
||||
/// By default, this is `true` on Mac and Windows, but `false` on Linux
|
||||
/// due to <https://github.com/frewsxcv/rust-dark-light/issues/17>.
|
||||
///
|
||||
/// See also [`Self::default_theme`].
|
||||
pub follow_system_theme: bool,
|
||||
|
||||
/// Which theme to use in case [`Self::follow_system_theme`] is `false`
|
||||
/// or the `dark-light` feature is disabled.
|
||||
///
|
||||
/// Default: `Theme::Dark`.
|
||||
pub default_theme: Theme,
|
||||
|
||||
/// This controls what happens when you close the main eframe window.
|
||||
///
|
||||
/// If `true`, execution will continue after the eframe window is closed.
|
||||
/// If `false`, the app will close once the eframe window is closed.
|
||||
///
|
||||
/// This is `true` by default, and the `false` option is only there
|
||||
/// so we can revert if we find any bugs.
|
||||
///
|
||||
/// This feature was introduced in <https://github.com/emilk/egui/pull/1889>.
|
||||
///
|
||||
/// When `true`, [`winit::platform::run_return::EventLoopExtRunReturn::run_return`] is used.
|
||||
/// When `false`, [`winit::event_loop::EventLoop::run`] is used.
|
||||
pub run_and_return: bool,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Default for NativeOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
always_on_top: false,
|
||||
maximized: false,
|
||||
decorated: true,
|
||||
fullscreen: false,
|
||||
drag_and_drop_support: true,
|
||||
icon_data: None,
|
||||
initial_window_pos: None,
|
||||
initial_window_size: None,
|
||||
min_window_size: None,
|
||||
max_window_size: None,
|
||||
resizable: true,
|
||||
transparent: false,
|
||||
vsync: true,
|
||||
multisampling: 0,
|
||||
depth_buffer: 0,
|
||||
stencil_buffer: 0,
|
||||
hardware_acceleration: HardwareAcceleration::Preferred,
|
||||
renderer: Renderer::default(),
|
||||
follow_system_theme: cfg!(target_os = "macos") || cfg!(target_os = "windows"),
|
||||
default_theme: Theme::Dark,
|
||||
run_and_return: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl NativeOptions {
|
||||
/// The theme used by the system.
|
||||
#[cfg(feature = "dark-light")]
|
||||
pub fn system_theme(&self) -> Option<Theme> {
|
||||
if self.follow_system_theme {
|
||||
crate::profile_scope!("dark_light::detect");
|
||||
match dark_light::detect() {
|
||||
dark_light::Mode::Dark => Some(Theme::Dark),
|
||||
dark_light::Mode::Light => Some(Theme::Light),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The theme used by the system.
|
||||
#[cfg(not(feature = "dark-light"))]
|
||||
pub fn system_theme(&self) -> Option<Theme> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Options when using `eframe` in a web page.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct WebOptions {
|
||||
/// Try to detect and follow the system preferred setting for dark vs light mode.
|
||||
///
|
||||
/// See also [`Self::default_theme`].
|
||||
///
|
||||
/// Default: `true`.
|
||||
pub follow_system_theme: bool,
|
||||
|
||||
/// Which theme to use in case [`Self::follow_system_theme`] is `false`
|
||||
/// or system theme detection fails.
|
||||
///
|
||||
/// Default: `Theme::Dark`.
|
||||
pub default_theme: Theme,
|
||||
|
||||
/// Which version of WebGl context to select
|
||||
///
|
||||
/// Default: [`WebGlContextOption::BestFirst`].
|
||||
pub webgl_context_option: WebGlContextOption,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl Default for WebOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
follow_system_theme: true,
|
||||
default_theme: Theme::Dark,
|
||||
webgl_context_option: WebGlContextOption::BestFirst,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Dark or Light theme.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum Theme {
|
||||
/// Dark mode: light text on a dark background.
|
||||
Dark,
|
||||
|
||||
/// Light mode: dark text on a light background.
|
||||
Light,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
/// Get the egui visuals corresponding to this theme.
|
||||
///
|
||||
/// Use with [`egui::Context::set_visuals`].
|
||||
pub fn egui_visuals(self) -> egui::Visuals {
|
||||
match self {
|
||||
Self::Dark => egui::Visuals::dark(),
|
||||
Self::Light => egui::Visuals::light(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// WebGL Context options
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum WebGlContextOption {
|
||||
/// Force Use WebGL1.
|
||||
WebGl1,
|
||||
|
||||
/// Force use WebGL2.
|
||||
WebGl2,
|
||||
|
||||
/// Use WebGl2 first.
|
||||
BestFirst,
|
||||
|
||||
/// Use WebGl1 first
|
||||
CompatibilityFirst,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// What rendering backend to use.
|
||||
///
|
||||
/// You need to enable the "glow" and "wgpu" features to have a choice.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
||||
pub enum Renderer {
|
||||
/// Use [`egui_glow`] renderer for [`glow`](https://github.com/grovesNL/glow).
|
||||
#[cfg(feature = "glow")]
|
||||
Glow,
|
||||
|
||||
/// Use [`egui_wgpu`] renderer for [`wgpu`](https://github.com/gfx-rs/wgpu).
|
||||
#[cfg(feature = "wgpu")]
|
||||
Wgpu,
|
||||
}
|
||||
|
||||
impl Default for Renderer {
|
||||
fn default() -> Self {
|
||||
#[cfg(feature = "glow")]
|
||||
return Self::Glow;
|
||||
|
||||
#[cfg(not(feature = "glow"))]
|
||||
#[cfg(feature = "wgpu")]
|
||||
return Self::Wgpu;
|
||||
|
||||
#[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'");
|
||||
}
|
||||
}
|
||||
|
||||
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")]
|
||||
Self::Wgpu => "wgpu".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Renderer {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(name: &str) -> Result<Self, String> {
|
||||
match name.to_lowercase().as_str() {
|
||||
#[cfg(feature = "glow")]
|
||||
"glow" => Ok(Self::Glow),
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
"wgpu" => Ok(Self::Wgpu),
|
||||
|
||||
_ => Err(format!("eframe renderer {name:?} is not available. Make sure that the corresponding eframe feature is enabled."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Image data for an application icon.
|
||||
#[derive(Clone)]
|
||||
pub struct IconData {
|
||||
/// RGBA pixels, unmultiplied.
|
||||
pub rgba: Vec<u8>,
|
||||
|
||||
/// Image width. This should be a multiple of 4.
|
||||
pub width: u32,
|
||||
|
||||
/// Image height. This should be a multiple of 4.
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
/// Represents the surroundings of your app.
|
||||
///
|
||||
/// It provides methods to inspect the surroundings (are we on the web?),
|
||||
/// allocate textures, and change settings (e.g. window size).
|
||||
pub struct Frame {
|
||||
/// Information about the integration.
|
||||
pub(crate) info: IntegrationInfo,
|
||||
|
||||
/// Where the app can issue commands back to the integration.
|
||||
pub(crate) output: backend::AppOutput,
|
||||
|
||||
/// A place where you can store custom data in a way that persists when you restart the app.
|
||||
pub(crate) storage: Option<Box<dyn Storage>>,
|
||||
|
||||
/// A reference to the underlying [`glow`] (OpenGL) context.
|
||||
#[cfg(feature = "glow")]
|
||||
pub(crate) gl: Option<std::sync::Arc<glow::Context>>,
|
||||
|
||||
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub(crate) wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// True if you are in a web environment.
|
||||
///
|
||||
/// Equivalent to `cfg!(target_arch = "wasm32")`
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn is_web(&self) -> bool {
|
||||
cfg!(target_arch = "wasm32")
|
||||
}
|
||||
|
||||
/// Information about the integration.
|
||||
pub fn info(&self) -> IntegrationInfo {
|
||||
self.info.clone()
|
||||
}
|
||||
|
||||
/// A place where you can store custom data in a way that persists when you restart the app.
|
||||
pub fn storage(&self) -> Option<&dyn Storage> {
|
||||
self.storage.as_deref()
|
||||
}
|
||||
|
||||
/// A place where you can store custom data in a way that persists when you restart the app.
|
||||
pub fn storage_mut(&mut self) -> Option<&mut (dyn Storage + 'static)> {
|
||||
self.storage.as_deref_mut()
|
||||
}
|
||||
|
||||
/// A reference to the underlying [`glow`] (OpenGL) context.
|
||||
///
|
||||
/// This can be used, for instance, to:
|
||||
/// * Render things to offscreen buffers.
|
||||
/// * Read the pixel buffer from the previous frame (`glow::Context::read_pixels`).
|
||||
/// * Render things behind the egui windows.
|
||||
///
|
||||
/// Note that all egui painting is deferred to after the call to [`App::update`]
|
||||
/// ([`egui`] only collects [`egui::Shape`]s and then eframe paints them all in one go later on).
|
||||
///
|
||||
/// To get a [`glow`] context you need to compile with the `glow` feature flag,
|
||||
/// and run eframe using [`Renderer::Glow`].
|
||||
#[cfg(feature = "glow")]
|
||||
pub fn gl(&self) -> Option<&std::sync::Arc<glow::Context>> {
|
||||
self.gl.as_ref()
|
||||
}
|
||||
|
||||
/// The underlying WGPU render state.
|
||||
///
|
||||
/// 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")]
|
||||
pub fn wgpu_render_state(&self) -> Option<&egui_wgpu::RenderState> {
|
||||
self.wgpu_render_state.as_ref()
|
||||
}
|
||||
|
||||
/// Signal the app to stop/exit/quit the app (only works for native apps, not web apps).
|
||||
/// The framework will not quit immediately, but at the end of the this frame.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn quit(&mut self) {
|
||||
self.output.quit = true;
|
||||
}
|
||||
|
||||
/// Set the desired inner size of the window (in egui points).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn set_window_size(&mut self, size: egui::Vec2) {
|
||||
self.output.window_size = Some(size);
|
||||
}
|
||||
|
||||
/// Set the desired title of the window.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn set_window_title(&mut self, title: &str) {
|
||||
self.output.window_title = Some(title.to_owned());
|
||||
}
|
||||
|
||||
/// Set whether to show window decorations (i.e. a frame around you app).
|
||||
///
|
||||
/// If false it will be difficult to move and resize the app.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn set_decorations(&mut self, decorated: bool) {
|
||||
self.output.decorated = Some(decorated);
|
||||
}
|
||||
|
||||
/// Turn borderless fullscreen on/off (native only).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn set_fullscreen(&mut self, fullscreen: bool) {
|
||||
self.output.fullscreen = Some(fullscreen);
|
||||
}
|
||||
|
||||
/// set the position of the outer window.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn set_window_pos(&mut self, pos: egui::Pos2) {
|
||||
self.output.window_pos = Some(pos);
|
||||
}
|
||||
|
||||
/// When called, the native window will follow the
|
||||
/// movement of the cursor while the primary mouse button is down.
|
||||
///
|
||||
/// Does not work on the web.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn drag_window(&mut self) {
|
||||
self.output.drag_window = true;
|
||||
}
|
||||
|
||||
/// Set the visibility of the window.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn set_visible(&mut self, visible: bool) {
|
||||
self.output.visible = Some(visible);
|
||||
}
|
||||
|
||||
/// for integrations only: call once per frame
|
||||
pub(crate) fn take_app_output(&mut self) -> backend::AppOutput {
|
||||
std::mem::take(&mut self.output)
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about the web environment (if applicable).
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct WebInfo {
|
||||
/// Information about the URL.
|
||||
pub location: Location,
|
||||
}
|
||||
|
||||
/// Information about the application's main window, if available.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WindowInfo {
|
||||
/// Coordinates of the window's outer top left corner, relative to the top left corner of the first display.
|
||||
///
|
||||
/// Unit: egui points (logical pixels).
|
||||
///
|
||||
/// `None` = unknown.
|
||||
pub position: Option<egui::Pos2>,
|
||||
|
||||
/// Are we in fullscreen mode?
|
||||
pub fullscreen: bool,
|
||||
|
||||
/// Window inner size in egui points (logical pixels).
|
||||
pub size: egui::Vec2,
|
||||
}
|
||||
|
||||
/// Information about the URL.
|
||||
///
|
||||
/// Everything has been percent decoded (`%20` -> ` ` etc).
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Location {
|
||||
/// The full URL (`location.href`) without the hash.
|
||||
///
|
||||
/// Example: `"http://www.example.com:80/index.html?foo=bar"`.
|
||||
pub url: String,
|
||||
|
||||
/// `location.protocol`
|
||||
///
|
||||
/// Example: `"http:"`.
|
||||
pub protocol: String,
|
||||
|
||||
/// `location.host`
|
||||
///
|
||||
/// Example: `"example.com:80"`.
|
||||
pub host: String,
|
||||
|
||||
/// `location.hostname`
|
||||
///
|
||||
/// Example: `"example.com"`.
|
||||
pub hostname: String,
|
||||
|
||||
/// `location.port`
|
||||
///
|
||||
/// Example: `"80"`.
|
||||
pub port: String,
|
||||
|
||||
/// The "#fragment" part of "www.example.com/index.html?query#fragment".
|
||||
///
|
||||
/// Note that the leading `#` is included in the string.
|
||||
/// Also known as "hash-link" or "anchor".
|
||||
pub hash: String,
|
||||
|
||||
/// The "query" part of "www.example.com/index.html?query#fragment".
|
||||
///
|
||||
/// Note that the leading `?` is NOT included in the string.
|
||||
///
|
||||
/// Use [`Self::query_map`] to get the parsed version of it.
|
||||
pub query: String,
|
||||
|
||||
/// The parsed "query" part of "www.example.com/index.html?query#fragment".
|
||||
///
|
||||
/// "foo=42&bar%20" is parsed as `{"foo": "42", "bar ": ""}`
|
||||
pub query_map: std::collections::BTreeMap<String, String>,
|
||||
|
||||
/// `location.origin`
|
||||
///
|
||||
/// Example: `"http://www.example.com:80"`.
|
||||
pub origin: String,
|
||||
}
|
||||
|
||||
/// Information about the integration passed to the use app each frame.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IntegrationInfo {
|
||||
/// Information about the surrounding web environment.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub web_info: WebInfo,
|
||||
|
||||
/// Does the OS use dark or light mode?
|
||||
///
|
||||
/// `None` means "don't know".
|
||||
pub system_theme: Option<Theme>,
|
||||
|
||||
/// Seconds of cpu usage (in seconds) of UI code on the previous frame.
|
||||
/// `None` if this is the first frame.
|
||||
pub cpu_usage: Option<f32>,
|
||||
|
||||
/// The OS native pixels-per-point
|
||||
pub native_pixels_per_point: Option<f32>,
|
||||
|
||||
/// The position and size of the native window.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub window_info: WindowInfo,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A place where you can store custom data in a way that persists when you restart the app.
|
||||
///
|
||||
/// On the web this is backed by [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
|
||||
/// On desktop this is backed by the file system.
|
||||
///
|
||||
/// See [`CreationContext::storage`] and [`App::save`].
|
||||
pub trait Storage {
|
||||
/// Get the value for the given key.
|
||||
fn get_string(&self, key: &str) -> Option<String>;
|
||||
|
||||
/// Set the value for the given key.
|
||||
fn set_string(&mut self, key: &str, value: String);
|
||||
|
||||
/// write-to-disk or similar
|
||||
fn flush(&mut self);
|
||||
}
|
||||
|
||||
/// Stores nothing.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct DummyStorage {}
|
||||
|
||||
impl Storage for DummyStorage {
|
||||
fn get_string(&self, _key: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_string(&mut self, _key: &str, _value: String) {}
|
||||
|
||||
fn flush(&mut self) {}
|
||||
}
|
||||
|
||||
/// Get and deserialize the [RON](https://github.com/ron-rs/ron) stored at the given key.
|
||||
#[cfg(feature = "ron")]
|
||||
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
|
||||
storage
|
||||
.get_string(key)
|
||||
.and_then(|value| ron::from_str(&value).ok())
|
||||
}
|
||||
|
||||
/// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key.
|
||||
#[cfg(feature = "ron")]
|
||||
pub fn set_value<T: serde::Serialize>(storage: &mut dyn Storage, key: &str, value: &T) {
|
||||
match ron::ser::to_string(value) {
|
||||
Ok(string) => storage.set_string(key, string),
|
||||
Err(err) => tracing::error!("eframe failed to encode data using ron: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Storage`] key used for app
|
||||
pub const APP_KEY: &str = "app";
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// You only need to look here if you are writing a backend for `epi`.
|
||||
pub(crate) mod backend {
|
||||
/// Action that can be taken by the user app.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[must_use]
|
||||
pub struct AppOutput {
|
||||
/// Set to `true` to stop the app.
|
||||
/// This does nothing for web apps.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub quit: bool,
|
||||
|
||||
/// Set to some size to resize the outer window (e.g. glium window) to this size.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub window_size: Option<egui::Vec2>,
|
||||
|
||||
/// Set to some string to rename the outer window (e.g. glium window) to this title.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub window_title: Option<String>,
|
||||
|
||||
/// Set to some bool to change window decorations.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub decorated: Option<bool>,
|
||||
|
||||
/// Set to some bool to change window fullscreen.
|
||||
#[cfg(not(target_arch = "wasm32"))] // TODO: implement fullscreen on web
|
||||
pub fullscreen: Option<bool>,
|
||||
|
||||
/// Set to true to drag window while primary mouse button is down.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub drag_window: bool,
|
||||
|
||||
/// Set to some position to move the outer window (e.g. glium window) to this position
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub window_pos: Option<egui::Pos2>,
|
||||
|
||||
/// Set to some bool to change window visibility.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub visible: Option<bool>,
|
||||
}
|
||||
}
|
||||
204
crates/eframe/src/lib.rs
Normal file
204
crates/eframe/src/lib.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
//! eframe - the [`egui`] framework crate
|
||||
//!
|
||||
//! 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 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
|
||||
//! call [`crate::run_native`] from your `main.rs`, and/or call `eframe::start_web` from your `lib.rs`.
|
||||
//!
|
||||
//! ## Usage, native:
|
||||
//! ``` no_run
|
||||
//! use eframe::egui;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let native_options = eframe::NativeOptions::default();
|
||||
//! eframe::run_native("My egui App", native_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))));
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Default)]
|
||||
//! struct MyEguiApp {}
|
||||
//!
|
||||
//! impl MyEguiApp {
|
||||
//! fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
//! // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
|
||||
//! // Restore app state using cc.storage (requires the "persistence" feature).
|
||||
//! // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
|
||||
//! // for e.g. egui::PaintCallback.
|
||||
//! 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!");
|
||||
//! });
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Usage, web:
|
||||
//! ``` no_run
|
||||
//! #[cfg(target_arch = "wasm32")]
|
||||
//! use wasm_bindgen::prelude::*;
|
||||
//!
|
||||
//! /// Call this once from the HTML.
|
||||
//! #[cfg(target_arch = "wasm32")]
|
||||
//! #[wasm_bindgen]
|
||||
//! pub fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> {
|
||||
//! eframe::start_web(canvas_id, Box::new(|cc| Box::new(MyApp::new(cc))))
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Feature flags
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
|
||||
#![allow(clippy::needless_doctest_main)]
|
||||
|
||||
// Re-export all useful libraries:
|
||||
pub use {egui, egui::emath, egui::epaint};
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
pub use {egui_glow, glow};
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub use {egui_wgpu, wgpu};
|
||||
|
||||
mod epi;
|
||||
|
||||
// Re-export everything in `epi` so `eframe` users don't have to care about what `epi` is:
|
||||
pub use epi::*;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// When compiling for web
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod web;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm_bindgen;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use web::AppRunnerRef;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use web_sys;
|
||||
|
||||
/// Install event listeners to register different input events
|
||||
/// and start running the given app.
|
||||
///
|
||||
/// ``` no_run
|
||||
/// #[cfg(target_arch = "wasm32")]
|
||||
/// use wasm_bindgen::prelude::*;
|
||||
///
|
||||
/// /// This is the entry-point for all the web-assembly.
|
||||
/// /// This is called from the HTML.
|
||||
/// /// It loads the app, installs some callbacks, then returns.
|
||||
/// /// It returns a handle to the running app that can be stopped calling `AppRunner::stop_web`.
|
||||
/// /// You can add more callbacks like this if you want to call in to your code.
|
||||
/// #[cfg(target_arch = "wasm32")]
|
||||
/// #[wasm_bindgen]
|
||||
/// pub fn start(canvas_id: &str) -> Result<AppRunnerRef>, eframe::wasm_bindgen::JsValue> {
|
||||
/// let web_options = eframe::WebOptions::default();
|
||||
/// eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))))
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn start_web(
|
||||
canvas_id: &str,
|
||||
web_options: WebOptions,
|
||||
app_creator: AppCreator,
|
||||
) -> Result<AppRunnerRef, wasm_bindgen::JsValue> {
|
||||
let handle = web::start(canvas_id, web_options, app_creator)?;
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// When compiling natively
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod native;
|
||||
|
||||
/// This is how you start a native (desktop) app.
|
||||
///
|
||||
/// The first argument is name of your app, used for the title bar of the native window
|
||||
/// and the save location of persistence (see [`App::save`]).
|
||||
///
|
||||
/// Call from `fn main` like this:
|
||||
/// ``` no_run
|
||||
/// use eframe::egui;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let native_options = eframe::NativeOptions::default();
|
||||
/// eframe::run_native("MyApp", native_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))));
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Default)]
|
||||
/// struct MyEguiApp {}
|
||||
///
|
||||
/// impl MyEguiApp {
|
||||
/// fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
/// // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
|
||||
/// // Restore app state using cc.storage (requires the "persistence" feature).
|
||||
/// // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
|
||||
/// // for e.g. egui::PaintCallback.
|
||||
/// 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!");
|
||||
/// });
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: AppCreator) {
|
||||
let renderer = native_options.renderer;
|
||||
|
||||
match renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
Renderer::Glow => {
|
||||
tracing::debug!("Using the glow renderer");
|
||||
native::run::run_glow(app_name, &native_options, app_creator);
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
Renderer::Wgpu => {
|
||||
tracing::debug!("Using the wgpu renderer");
|
||||
native::run::run_wgpu(app_name, &native_options, app_creator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Profiling macro for feature "puffin"
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
macro_rules! profile_function {
|
||||
($($arg: tt)*) => {
|
||||
#[cfg(feature = "puffin")]
|
||||
puffin::profile_function!($($arg)*);
|
||||
};
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use profile_function;
|
||||
|
||||
/// Profiling macro for feature "puffin"
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
macro_rules! profile_scope {
|
||||
($($arg: tt)*) => {
|
||||
#[cfg(feature = "puffin")]
|
||||
puffin::profile_scope!($($arg)*);
|
||||
};
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) use profile_scope;
|
||||
377
crates/eframe/src/native/epi_integration.rs
Normal file
377
crates/eframe/src/native/epi_integration.rs
Normal file
@@ -0,0 +1,377 @@
|
||||
use crate::{epi, Theme, WindowInfo};
|
||||
use egui_winit::{native_pixels_per_point, WindowSettings};
|
||||
use winit::event_loop::EventLoopWindowTarget;
|
||||
|
||||
pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
|
||||
winit::dpi::LogicalSize {
|
||||
width: points.x as f64,
|
||||
height: points.y as f64,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_window_info(window: &winit::window::Window, pixels_per_point: f32) -> WindowInfo {
|
||||
let position = window
|
||||
.outer_position()
|
||||
.ok()
|
||||
.map(|pos| pos.to_logical::<f32>(pixels_per_point.into()))
|
||||
.map(|pos| egui::Pos2 { x: pos.x, y: pos.y });
|
||||
|
||||
let size = window
|
||||
.inner_size()
|
||||
.to_logical::<f32>(pixels_per_point.into());
|
||||
|
||||
WindowInfo {
|
||||
position,
|
||||
fullscreen: window.fullscreen().is_some(),
|
||||
size: egui::Vec2 {
|
||||
x: size.width,
|
||||
y: size.height,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_builder(
|
||||
native_options: &epi::NativeOptions,
|
||||
window_settings: &Option<WindowSettings>,
|
||||
) -> winit::window::WindowBuilder {
|
||||
let epi::NativeOptions {
|
||||
always_on_top,
|
||||
maximized,
|
||||
decorated,
|
||||
fullscreen,
|
||||
drag_and_drop_support,
|
||||
icon_data,
|
||||
initial_window_pos,
|
||||
initial_window_size,
|
||||
min_window_size,
|
||||
max_window_size,
|
||||
resizable,
|
||||
transparent,
|
||||
..
|
||||
} = native_options;
|
||||
|
||||
let window_icon = icon_data.clone().and_then(load_icon);
|
||||
|
||||
let mut window_builder = winit::window::WindowBuilder::new()
|
||||
.with_always_on_top(*always_on_top)
|
||||
.with_decorations(*decorated)
|
||||
.with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)))
|
||||
.with_maximized(*maximized)
|
||||
.with_resizable(*resizable)
|
||||
.with_transparent(*transparent)
|
||||
.with_window_icon(window_icon);
|
||||
|
||||
if let Some(min_size) = *min_window_size {
|
||||
window_builder = window_builder.with_min_inner_size(points_to_size(min_size));
|
||||
}
|
||||
if let Some(max_size) = *max_window_size {
|
||||
window_builder = window_builder.with_max_inner_size(points_to_size(max_size));
|
||||
}
|
||||
|
||||
window_builder = window_builder_drag_and_drop(window_builder, *drag_and_drop_support);
|
||||
|
||||
if let Some(window_settings) = window_settings {
|
||||
window_builder = window_settings.initialize_window(window_builder);
|
||||
} else {
|
||||
if let Some(pos) = *initial_window_pos {
|
||||
window_builder = window_builder.with_position(winit::dpi::PhysicalPosition {
|
||||
x: pos.x as f64,
|
||||
y: pos.y as f64,
|
||||
});
|
||||
}
|
||||
if let Some(initial_window_size) = *initial_window_size {
|
||||
window_builder = window_builder.with_inner_size(points_to_size(initial_window_size));
|
||||
}
|
||||
}
|
||||
|
||||
window_builder
|
||||
}
|
||||
|
||||
fn load_icon(icon_data: epi::IconData) -> Option<winit::window::Icon> {
|
||||
winit::window::Icon::from_rgba(icon_data.rgba, icon_data.width, icon_data.height).ok()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn window_builder_drag_and_drop(
|
||||
window_builder: winit::window::WindowBuilder,
|
||||
enable: bool,
|
||||
) -> winit::window::WindowBuilder {
|
||||
use winit::platform::windows::WindowBuilderExtWindows as _;
|
||||
window_builder.with_drag_and_drop(enable)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn window_builder_drag_and_drop(
|
||||
window_builder: winit::window::WindowBuilder,
|
||||
_enable: bool,
|
||||
) -> winit::window::WindowBuilder {
|
||||
// drag and drop can only be disabled on windows
|
||||
window_builder
|
||||
}
|
||||
|
||||
pub fn handle_app_output(
|
||||
window: &winit::window::Window,
|
||||
current_pixels_per_point: f32,
|
||||
app_output: epi::backend::AppOutput,
|
||||
) {
|
||||
let epi::backend::AppOutput {
|
||||
quit: _,
|
||||
window_size,
|
||||
window_title,
|
||||
decorated,
|
||||
fullscreen,
|
||||
drag_window,
|
||||
window_pos,
|
||||
visible,
|
||||
} = app_output;
|
||||
|
||||
if let Some(decorated) = decorated {
|
||||
window.set_decorations(decorated);
|
||||
}
|
||||
|
||||
if let Some(window_size) = window_size {
|
||||
window.set_inner_size(
|
||||
winit::dpi::PhysicalSize {
|
||||
width: (current_pixels_per_point * window_size.x).round(),
|
||||
height: (current_pixels_per_point * window_size.y).round(),
|
||||
}
|
||||
.to_logical::<f32>(native_pixels_per_point(window) as f64),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(fullscreen) = fullscreen {
|
||||
window.set_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)));
|
||||
}
|
||||
|
||||
if let Some(window_title) = window_title {
|
||||
window.set_title(&window_title);
|
||||
}
|
||||
|
||||
if let Some(window_pos) = window_pos {
|
||||
window.set_outer_position(winit::dpi::PhysicalPosition {
|
||||
x: window_pos.x as f64,
|
||||
y: window_pos.y as f64,
|
||||
});
|
||||
}
|
||||
|
||||
if drag_window {
|
||||
let _ = window.drag_window();
|
||||
}
|
||||
|
||||
if let Some(visible) = visible {
|
||||
window.set_visible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// For loading/saving app state and/or egui memory to disk.
|
||||
pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
|
||||
#[cfg(feature = "persistence")]
|
||||
if let Some(storage) = super::file_storage::FileStorage::from_app_name(_app_name) {
|
||||
return Some(Box::new(storage));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Everything needed to make a winit-based integration for [`epi`].
|
||||
pub struct EpiIntegration {
|
||||
pub frame: epi::Frame,
|
||||
last_auto_save: std::time::Instant,
|
||||
pub egui_ctx: egui::Context,
|
||||
pending_full_output: egui::FullOutput,
|
||||
egui_winit: egui_winit::State,
|
||||
/// When set, it is time to quit
|
||||
quit: bool,
|
||||
can_drag_window: bool,
|
||||
}
|
||||
|
||||
impl EpiIntegration {
|
||||
pub fn new<E>(
|
||||
event_loop: &EventLoopWindowTarget<E>,
|
||||
max_texture_side: usize,
|
||||
window: &winit::window::Window,
|
||||
system_theme: Option<Theme>,
|
||||
storage: Option<Box<dyn epi::Storage>>,
|
||||
#[cfg(feature = "glow")] gl: Option<std::sync::Arc<glow::Context>>,
|
||||
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
) -> Self {
|
||||
let egui_ctx = egui::Context::default();
|
||||
|
||||
*egui_ctx.memory() = load_egui_memory(storage.as_deref()).unwrap_or_default();
|
||||
|
||||
let frame = epi::Frame {
|
||||
info: epi::IntegrationInfo {
|
||||
system_theme,
|
||||
cpu_usage: None,
|
||||
native_pixels_per_point: Some(native_pixels_per_point(window)),
|
||||
window_info: read_window_info(window, egui_ctx.pixels_per_point()),
|
||||
},
|
||||
output: Default::default(),
|
||||
storage,
|
||||
#[cfg(feature = "glow")]
|
||||
gl,
|
||||
#[cfg(feature = "wgpu")]
|
||||
wgpu_render_state,
|
||||
};
|
||||
|
||||
let mut egui_winit = egui_winit::State::new(event_loop);
|
||||
egui_winit.set_max_texture_side(max_texture_side);
|
||||
let pixels_per_point = window.scale_factor() as f32;
|
||||
egui_winit.set_pixels_per_point(pixels_per_point);
|
||||
|
||||
Self {
|
||||
frame,
|
||||
last_auto_save: std::time::Instant::now(),
|
||||
egui_ctx,
|
||||
egui_winit,
|
||||
pending_full_output: Default::default(),
|
||||
quit: false,
|
||||
can_drag_window: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
||||
crate::profile_function!();
|
||||
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
|
||||
self.egui_ctx.memory().set_everything_is_visible(true);
|
||||
let full_output = self.update(app, window);
|
||||
self.pending_full_output.append(full_output); // Handle it next frame
|
||||
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
|
||||
self.egui_ctx.clear_animations();
|
||||
}
|
||||
|
||||
/// If `true`, it is time to shut down.
|
||||
pub fn should_quit(&self) -> bool {
|
||||
self.quit
|
||||
}
|
||||
|
||||
pub fn on_event(&mut self, app: &mut dyn epi::App, event: &winit::event::WindowEvent<'_>) {
|
||||
use winit::event::{ElementState, MouseButton, WindowEvent};
|
||||
|
||||
match event {
|
||||
WindowEvent::CloseRequested => self.quit = app.on_exit_event(),
|
||||
WindowEvent::Destroyed => self.quit = true,
|
||||
WindowEvent::MouseInput {
|
||||
button: MouseButton::Left,
|
||||
state: ElementState::Pressed,
|
||||
..
|
||||
} => self.can_drag_window = true,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.egui_winit.on_event(&self.egui_ctx, event);
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
window: &winit::window::Window,
|
||||
) -> egui::FullOutput {
|
||||
let frame_start = std::time::Instant::now();
|
||||
|
||||
self.frame.info.window_info = read_window_info(window, self.egui_ctx.pixels_per_point());
|
||||
let raw_input = self.egui_winit.take_egui_input(window);
|
||||
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
|
||||
crate::profile_scope!("App::update");
|
||||
app.update(egui_ctx, &mut self.frame);
|
||||
});
|
||||
self.pending_full_output.append(full_output);
|
||||
let full_output = std::mem::take(&mut self.pending_full_output);
|
||||
|
||||
{
|
||||
let mut app_output = self.frame.take_app_output();
|
||||
app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108
|
||||
self.can_drag_window = false;
|
||||
if app_output.quit {
|
||||
self.quit = app.on_exit_event();
|
||||
}
|
||||
handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
|
||||
}
|
||||
|
||||
let frame_time = (std::time::Instant::now() - frame_start).as_secs_f64() as f32;
|
||||
self.frame.info.cpu_usage = Some(frame_time);
|
||||
|
||||
full_output
|
||||
}
|
||||
|
||||
pub fn post_rendering(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
||||
let inner_size = window.inner_size();
|
||||
let window_size_px = [inner_size.width, inner_size.height];
|
||||
|
||||
app.post_rendering(window_size_px, &self.frame);
|
||||
}
|
||||
|
||||
pub fn handle_platform_output(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
platform_output: egui::PlatformOutput,
|
||||
) {
|
||||
self.egui_winit
|
||||
.handle_platform_output(window, &self.egui_ctx, platform_output);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Persistance stuff:
|
||||
|
||||
pub fn maybe_autosave(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
|
||||
let now = std::time::Instant::now();
|
||||
if now - self.last_auto_save > app.auto_save_interval() {
|
||||
self.save(app, window);
|
||||
self.last_auto_save = now;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&mut self, _app: &mut dyn epi::App, _window: &winit::window::Window) {
|
||||
#[cfg(feature = "persistence")]
|
||||
if let Some(storage) = self.frame.storage_mut() {
|
||||
crate::profile_function!();
|
||||
|
||||
if _app.persist_native_window() {
|
||||
crate::profile_scope!("native_window");
|
||||
epi::set_value(
|
||||
storage,
|
||||
STORAGE_WINDOW_KEY,
|
||||
&WindowSettings::from_display(_window),
|
||||
);
|
||||
}
|
||||
if _app.persist_egui_memory() {
|
||||
crate::profile_scope!("egui_memory");
|
||||
epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, &*self.egui_ctx.memory());
|
||||
}
|
||||
{
|
||||
crate::profile_scope!("App::save");
|
||||
_app.save(storage);
|
||||
}
|
||||
|
||||
crate::profile_scope!("Storage::flush");
|
||||
storage.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
const STORAGE_EGUI_MEMORY_KEY: &str = "egui";
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
const STORAGE_WINDOW_KEY: &str = "window";
|
||||
|
||||
pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<WindowSettings> {
|
||||
#[cfg(feature = "persistence")]
|
||||
{
|
||||
epi::get_value(_storage?, STORAGE_WINDOW_KEY)
|
||||
}
|
||||
#[cfg(not(feature = "persistence"))]
|
||||
None
|
||||
}
|
||||
|
||||
pub fn load_egui_memory(_storage: Option<&dyn epi::Storage>) -> Option<egui::Memory> {
|
||||
#[cfg(feature = "persistence")]
|
||||
{
|
||||
epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY)
|
||||
}
|
||||
#[cfg(not(feature = "persistence"))]
|
||||
None
|
||||
}
|
||||
116
crates/eframe/src/native/file_storage.rs
Normal file
116
crates/eframe/src/native/file_storage.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A key-value store backed by a [RON](https://github.com/ron-rs/ron) file on disk.
|
||||
/// Used to restore egui state, glium window position/size and app state.
|
||||
pub struct FileStorage {
|
||||
ron_filepath: PathBuf,
|
||||
kv: HashMap<String, String>,
|
||||
dirty: bool,
|
||||
last_save_join_handle: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Drop for FileStorage {
|
||||
fn drop(&mut self) {
|
||||
if let Some(join_handle) = self.last_save_join_handle.take() {
|
||||
join_handle.join().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileStorage {
|
||||
/// Store the state in this .ron file.
|
||||
pub fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
|
||||
let ron_filepath: PathBuf = ron_filepath.into();
|
||||
Self {
|
||||
kv: read_ron(&ron_filepath).unwrap_or_default(),
|
||||
ron_filepath,
|
||||
dirty: false,
|
||||
last_save_join_handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find a good place to put the files that the OS likes.
|
||||
pub fn from_app_name(app_name: &str) -> Option<Self> {
|
||||
if let Some(proj_dirs) = directories_next::ProjectDirs::from("", "", app_name) {
|
||||
let data_dir = proj_dirs.data_dir().to_path_buf();
|
||||
if let Err(err) = std::fs::create_dir_all(&data_dir) {
|
||||
tracing::warn!(
|
||||
"Saving disabled: Failed to create app path at {:?}: {}",
|
||||
data_dir,
|
||||
err
|
||||
);
|
||||
None
|
||||
} else {
|
||||
Some(Self::from_ron_filepath(data_dir.join("app.ron")))
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("Saving disabled: Failed to find path to data_dir.");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Storage for FileStorage {
|
||||
fn get_string(&self, key: &str) -> Option<String> {
|
||||
self.kv.get(key).cloned()
|
||||
}
|
||||
|
||||
fn set_string(&mut self, key: &str, value: String) {
|
||||
if self.kv.get(key) != Some(&value) {
|
||||
self.kv.insert(key.to_owned(), value);
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) {
|
||||
if self.dirty {
|
||||
self.dirty = false;
|
||||
|
||||
let file_path = self.ron_filepath.clone();
|
||||
let kv = self.kv.clone();
|
||||
|
||||
if let Some(join_handle) = self.last_save_join_handle.take() {
|
||||
// wait for previous save to complete.
|
||||
join_handle.join().ok();
|
||||
}
|
||||
|
||||
let join_handle = std::thread::spawn(move || {
|
||||
let file = std::fs::File::create(&file_path).unwrap();
|
||||
let config = Default::default();
|
||||
ron::ser::to_writer_pretty(file, &kv, config).unwrap();
|
||||
tracing::trace!("Persisted to {:?}", file_path);
|
||||
});
|
||||
|
||||
self.last_save_join_handle = Some(join_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn read_ron<T>(ron_path: impl AsRef<Path>) -> Option<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
match std::fs::File::open(ron_path) {
|
||||
Ok(file) => {
|
||||
let reader = std::io::BufReader::new(file);
|
||||
match ron::de::from_reader(reader) {
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to parse RON: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_err) => {
|
||||
// File probably doesn't exist. That's fine.
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
6
crates/eframe/src/native/mod.rs
Normal file
6
crates/eframe/src/native/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod epi_integration;
|
||||
pub mod run;
|
||||
|
||||
/// File storage which can be used by native backends.
|
||||
#[cfg(feature = "persistence")]
|
||||
pub mod file_storage;
|
||||
735
crates/eframe/src/native/run.rs
Normal file
735
crates/eframe/src/native/run.rs
Normal file
@@ -0,0 +1,735 @@
|
||||
//! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`].
|
||||
//! When making changes to one you often also want to apply it to the other.
|
||||
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use egui_winit::winit;
|
||||
use winit::event_loop::{ControlFlow, EventLoop};
|
||||
|
||||
use super::epi_integration::{self, EpiIntegration};
|
||||
use crate::epi;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RequestRepaintEvent;
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
#[allow(unsafe_code)]
|
||||
fn create_display(
|
||||
native_options: &NativeOptions,
|
||||
window_builder: winit::window::WindowBuilder,
|
||||
event_loop: &EventLoop<RequestRepaintEvent>,
|
||||
) -> (
|
||||
glutin::WindowedContext<glutin::PossiblyCurrent>,
|
||||
glow::Context,
|
||||
) {
|
||||
crate::profile_function!();
|
||||
|
||||
use crate::HardwareAcceleration;
|
||||
|
||||
let hardware_acceleration = match native_options.hardware_acceleration {
|
||||
HardwareAcceleration::Required => Some(true),
|
||||
HardwareAcceleration::Preferred => None,
|
||||
HardwareAcceleration::Off => Some(false),
|
||||
};
|
||||
|
||||
let gl_window = unsafe {
|
||||
glutin::ContextBuilder::new()
|
||||
.with_hardware_acceleration(hardware_acceleration)
|
||||
.with_depth_buffer(native_options.depth_buffer)
|
||||
.with_multisampling(native_options.multisampling)
|
||||
.with_srgb(true)
|
||||
.with_stencil_buffer(native_options.stencil_buffer)
|
||||
.with_vsync(native_options.vsync)
|
||||
.build_windowed(window_builder, event_loop)
|
||||
.unwrap()
|
||||
.make_current()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let gl = unsafe { glow::Context::from_loader_function(|s| gl_window.get_proc_address(s)) };
|
||||
|
||||
(gl_window, gl)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub use epi::NativeOptions;
|
||||
|
||||
enum EventResult {
|
||||
Wait,
|
||||
RepaintAsap,
|
||||
RepaintAt(Instant),
|
||||
Exit,
|
||||
}
|
||||
|
||||
trait WinitApp {
|
||||
fn is_focused(&self) -> bool;
|
||||
fn integration(&self) -> &EpiIntegration;
|
||||
fn window(&self) -> &winit::window::Window;
|
||||
fn save_and_destroy(&mut self);
|
||||
fn paint(&mut self) -> EventResult;
|
||||
fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult;
|
||||
}
|
||||
|
||||
/// Access a thread-local event loop.
|
||||
///
|
||||
/// We reuse the event-loop so we can support closing and opening an eframe window
|
||||
/// multiple times. This is just a limitation of winit.
|
||||
fn with_event_loop(f: impl FnOnce(&mut EventLoop<RequestRepaintEvent>)) {
|
||||
use std::cell::RefCell;
|
||||
thread_local!(static EVENT_LOOP: RefCell<EventLoop<RequestRepaintEvent>> = RefCell::new(winit::event_loop::EventLoopBuilder::with_user_event().build()));
|
||||
|
||||
EVENT_LOOP.with(|event_loop| {
|
||||
f(&mut *event_loop.borrow_mut());
|
||||
});
|
||||
}
|
||||
|
||||
fn run_and_return(event_loop: &mut EventLoop<RequestRepaintEvent>, mut winit_app: impl WinitApp) {
|
||||
use winit::platform::run_return::EventLoopExtRunReturn as _;
|
||||
|
||||
tracing::debug!("event_loop.run_return");
|
||||
|
||||
let mut next_repaint_time = Instant::now();
|
||||
|
||||
event_loop.run_return(|event, _, control_flow| {
|
||||
let event_result = match event {
|
||||
winit::event::Event::LoopDestroyed => EventResult::Exit,
|
||||
|
||||
// Platform-dependent event handlers to workaround a winit bug
|
||||
// See: https://github.com/rust-windowing/winit/issues/987
|
||||
// See: https://github.com/rust-windowing/winit/issues/1619
|
||||
winit::event::Event::RedrawEventsCleared if cfg!(windows) => {
|
||||
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
||||
winit_app.paint()
|
||||
}
|
||||
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => {
|
||||
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
||||
winit_app.paint()
|
||||
}
|
||||
|
||||
winit::event::Event::UserEvent(RequestRepaintEvent)
|
||||
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
|
||||
..
|
||||
}) => EventResult::RepaintAsap,
|
||||
|
||||
winit::event::Event::WindowEvent { window_id, .. }
|
||||
if window_id != winit_app.window().id() =>
|
||||
{
|
||||
// This can happen if we close a window, and then reopen a new one,
|
||||
// or if we have multiple windows open.
|
||||
EventResult::Wait
|
||||
}
|
||||
|
||||
event => winit_app.on_event(event),
|
||||
};
|
||||
|
||||
match event_result {
|
||||
EventResult::Wait => {}
|
||||
EventResult::RepaintAsap => {
|
||||
next_repaint_time = Instant::now();
|
||||
}
|
||||
EventResult::RepaintAt(repaint_time) => {
|
||||
next_repaint_time = next_repaint_time.min(repaint_time);
|
||||
}
|
||||
EventResult::Exit => {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
*control_flow = match next_repaint_time.checked_duration_since(Instant::now()) {
|
||||
None => {
|
||||
winit_app.window().request_redraw();
|
||||
ControlFlow::Poll
|
||||
}
|
||||
Some(time_until_next_repaint) => {
|
||||
ControlFlow::WaitUntil(Instant::now() + time_until_next_repaint)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tracing::debug!("eframe window closed");
|
||||
|
||||
winit_app.save_and_destroy();
|
||||
|
||||
drop(winit_app);
|
||||
|
||||
// Needed to clean the event_loop:
|
||||
event_loop.run_return(|_, _, control_flow| {
|
||||
control_flow.set_exit();
|
||||
});
|
||||
}
|
||||
|
||||
fn run_and_exit(
|
||||
event_loop: EventLoop<RequestRepaintEvent>,
|
||||
mut winit_app: impl WinitApp + 'static,
|
||||
) -> ! {
|
||||
tracing::debug!("event_loop.run");
|
||||
|
||||
let mut next_repaint_time = Instant::now();
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
let event_result = match event {
|
||||
winit::event::Event::LoopDestroyed => EventResult::Exit,
|
||||
|
||||
// Platform-dependent event handlers to workaround a winit bug
|
||||
// See: https://github.com/rust-windowing/winit/issues/987
|
||||
// See: https://github.com/rust-windowing/winit/issues/1619
|
||||
winit::event::Event::RedrawEventsCleared if cfg!(windows) => {
|
||||
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
||||
winit_app.paint()
|
||||
}
|
||||
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => {
|
||||
next_repaint_time = Instant::now() + Duration::from_secs(1_000_000_000);
|
||||
winit_app.paint()
|
||||
}
|
||||
|
||||
winit::event::Event::UserEvent(RequestRepaintEvent)
|
||||
| winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
|
||||
..
|
||||
}) => EventResult::RepaintAsap,
|
||||
|
||||
event => winit_app.on_event(event),
|
||||
};
|
||||
|
||||
match event_result {
|
||||
EventResult::Wait => {}
|
||||
EventResult::RepaintAsap => {
|
||||
next_repaint_time = Instant::now();
|
||||
}
|
||||
EventResult::RepaintAt(repaint_time) => {
|
||||
next_repaint_time = next_repaint_time.min(repaint_time);
|
||||
}
|
||||
EventResult::Exit => {
|
||||
tracing::debug!("Quitting…");
|
||||
winit_app.save_and_destroy();
|
||||
#[allow(clippy::exit)]
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
*control_flow = match next_repaint_time.checked_duration_since(Instant::now()) {
|
||||
None => {
|
||||
winit_app.window().request_redraw();
|
||||
ControlFlow::Poll
|
||||
}
|
||||
Some(time_until_next_repaint) => {
|
||||
ControlFlow::WaitUntil(Instant::now() + time_until_next_repaint)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Run an egui app
|
||||
#[cfg(feature = "glow")]
|
||||
mod glow_integration {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct GlowWinitApp {
|
||||
gl_window: glutin::WindowedContext<glutin::PossiblyCurrent>,
|
||||
gl: Arc<glow::Context>,
|
||||
painter: egui_glow::Painter,
|
||||
integration: epi_integration::EpiIntegration,
|
||||
app: Box<dyn epi::App>,
|
||||
is_focused: bool,
|
||||
}
|
||||
|
||||
impl GlowWinitApp {
|
||||
fn new(
|
||||
event_loop: &EventLoop<RequestRepaintEvent>,
|
||||
app_name: &str,
|
||||
native_options: &epi::NativeOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) -> Self {
|
||||
let storage = epi_integration::create_storage(app_name);
|
||||
let window_settings = epi_integration::load_window_settings(storage.as_deref());
|
||||
|
||||
let window_builder = epi_integration::window_builder(native_options, &window_settings)
|
||||
.with_title(app_name);
|
||||
let (gl_window, gl) = create_display(native_options, window_builder, event_loop);
|
||||
let gl = Arc::new(gl);
|
||||
|
||||
let painter = egui_glow::Painter::new(gl.clone(), None, "")
|
||||
.unwrap_or_else(|error| panic!("some OpenGL error occurred {}\n", error));
|
||||
|
||||
let system_theme = native_options.system_theme();
|
||||
let mut integration = epi_integration::EpiIntegration::new(
|
||||
event_loop,
|
||||
painter.max_texture_side(),
|
||||
gl_window.window(),
|
||||
system_theme,
|
||||
storage,
|
||||
Some(gl.clone()),
|
||||
#[cfg(feature = "wgpu")]
|
||||
None,
|
||||
);
|
||||
let theme = system_theme.unwrap_or(native_options.default_theme);
|
||||
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
||||
|
||||
{
|
||||
let event_loop_proxy = egui::mutex::Mutex::new(event_loop.create_proxy());
|
||||
integration.egui_ctx.set_request_repaint_callback(move || {
|
||||
event_loop_proxy.lock().send_event(RequestRepaintEvent).ok();
|
||||
});
|
||||
}
|
||||
|
||||
let mut app = app_creator(&epi::CreationContext {
|
||||
egui_ctx: integration.egui_ctx.clone(),
|
||||
integration_info: integration.frame.info(),
|
||||
storage: integration.frame.storage(),
|
||||
gl: Some(gl.clone()),
|
||||
#[cfg(feature = "wgpu")]
|
||||
wgpu_render_state: None,
|
||||
});
|
||||
|
||||
if app.warm_up_enabled() {
|
||||
integration.warm_up(app.as_mut(), gl_window.window());
|
||||
}
|
||||
|
||||
Self {
|
||||
gl_window,
|
||||
gl,
|
||||
painter,
|
||||
integration,
|
||||
app,
|
||||
is_focused: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WinitApp for GlowWinitApp {
|
||||
fn is_focused(&self) -> bool {
|
||||
self.is_focused
|
||||
}
|
||||
|
||||
fn integration(&self) -> &EpiIntegration {
|
||||
&self.integration
|
||||
}
|
||||
|
||||
fn window(&self) -> &winit::window::Window {
|
||||
self.gl_window.window()
|
||||
}
|
||||
|
||||
fn save_and_destroy(&mut self) {
|
||||
self.integration
|
||||
.save(&mut *self.app, self.gl_window.window());
|
||||
self.app.on_exit(Some(&self.gl));
|
||||
self.painter.destroy();
|
||||
}
|
||||
|
||||
fn paint(&mut self) -> EventResult {
|
||||
#[cfg(feature = "puffin")]
|
||||
puffin::GlobalProfiler::lock().new_frame();
|
||||
crate::profile_scope!("frame");
|
||||
|
||||
let Self {
|
||||
gl_window,
|
||||
gl,
|
||||
app,
|
||||
integration,
|
||||
painter,
|
||||
..
|
||||
} = self;
|
||||
let window = gl_window.window();
|
||||
|
||||
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
|
||||
|
||||
egui_glow::painter::clear(
|
||||
gl,
|
||||
screen_size_in_pixels,
|
||||
app.clear_color(&integration.egui_ctx.style().visuals),
|
||||
);
|
||||
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
repaint_after,
|
||||
textures_delta,
|
||||
shapes,
|
||||
} = integration.update(app.as_mut(), window);
|
||||
|
||||
integration.handle_platform_output(window, platform_output);
|
||||
|
||||
let clipped_primitives = {
|
||||
crate::profile_scope!("tessellate");
|
||||
integration.egui_ctx.tessellate(shapes)
|
||||
};
|
||||
|
||||
painter.paint_and_update_textures(
|
||||
screen_size_in_pixels,
|
||||
integration.egui_ctx.pixels_per_point(),
|
||||
&clipped_primitives,
|
||||
&textures_delta,
|
||||
);
|
||||
|
||||
integration.post_rendering(app.as_mut(), window);
|
||||
|
||||
{
|
||||
crate::profile_scope!("swap_buffers");
|
||||
gl_window.swap_buffers().unwrap();
|
||||
}
|
||||
|
||||
let control_flow = if integration.should_quit() {
|
||||
EventResult::Exit
|
||||
} else if repaint_after.is_zero() {
|
||||
EventResult::RepaintAsap
|
||||
} else if let Some(repaint_after_instant) =
|
||||
std::time::Instant::now().checked_add(repaint_after)
|
||||
{
|
||||
// if repaint_after is something huge and can't be added to Instant,
|
||||
// we will use `ControlFlow::Wait` instead.
|
||||
// technically, this might lead to some weird corner cases where the user *WANTS*
|
||||
// winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own
|
||||
// egui backend impl i guess.
|
||||
EventResult::RepaintAt(repaint_after_instant)
|
||||
} else {
|
||||
EventResult::Wait
|
||||
};
|
||||
|
||||
integration.maybe_autosave(app.as_mut(), window);
|
||||
|
||||
if !self.is_focused {
|
||||
// On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325
|
||||
// We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208
|
||||
// But we know if we are focused (in foreground). When minimized, we are not focused.
|
||||
// However, a user may want an egui with an animation in the background,
|
||||
// so we still need to repaint quite fast.
|
||||
crate::profile_scope!("bg_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
|
||||
control_flow
|
||||
}
|
||||
|
||||
fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult {
|
||||
match event {
|
||||
winit::event::Event::WindowEvent { event, .. } => {
|
||||
match &event {
|
||||
winit::event::WindowEvent::Focused(new_focused) => {
|
||||
self.is_focused = *new_focused;
|
||||
}
|
||||
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 physical_size.width > 0 && physical_size.height > 0 {
|
||||
self.gl_window.resize(*physical_size);
|
||||
}
|
||||
}
|
||||
winit::event::WindowEvent::ScaleFactorChanged {
|
||||
new_inner_size, ..
|
||||
} => {
|
||||
self.gl_window.resize(**new_inner_size);
|
||||
}
|
||||
winit::event::WindowEvent::CloseRequested
|
||||
if self.integration.should_quit() =>
|
||||
{
|
||||
return EventResult::Exit
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.integration.on_event(self.app.as_mut(), &event);
|
||||
|
||||
if self.integration.should_quit() {
|
||||
EventResult::Exit
|
||||
} else {
|
||||
// TODO(emilk): ask egui if the event warrants a repaint
|
||||
EventResult::RepaintAsap
|
||||
}
|
||||
}
|
||||
_ => EventResult::Wait,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_glow(
|
||||
app_name: &str,
|
||||
native_options: &epi::NativeOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) {
|
||||
if native_options.run_and_return {
|
||||
with_event_loop(|event_loop| {
|
||||
let glow_eframe =
|
||||
GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
run_and_return(event_loop, glow_eframe);
|
||||
});
|
||||
} else {
|
||||
let event_loop = winit::event_loop::EventLoopBuilder::with_user_event().build();
|
||||
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||
run_and_exit(event_loop, glow_eframe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
pub use glow_integration::run_glow;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
mod wgpu_integration {
|
||||
use super::*;
|
||||
|
||||
struct WgpuWinitApp {
|
||||
window: winit::window::Window,
|
||||
painter: egui_wgpu::winit::Painter<'static>,
|
||||
integration: epi_integration::EpiIntegration,
|
||||
app: Box<dyn epi::App>,
|
||||
is_focused: bool,
|
||||
}
|
||||
|
||||
impl WgpuWinitApp {
|
||||
fn new(
|
||||
event_loop: &EventLoop<RequestRepaintEvent>,
|
||||
app_name: &str,
|
||||
native_options: &epi::NativeOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) -> Self {
|
||||
let storage = epi_integration::create_storage(app_name);
|
||||
let window_settings = epi_integration::load_window_settings(storage.as_deref());
|
||||
|
||||
let window = epi_integration::window_builder(native_options, &window_settings)
|
||||
.with_title(app_name)
|
||||
.build(event_loop)
|
||||
.unwrap();
|
||||
|
||||
// SAFETY: `window` must outlive `painter`.
|
||||
#[allow(unsafe_code, unused_mut, unused_unsafe)]
|
||||
let painter = unsafe {
|
||||
let mut painter = egui_wgpu::winit::Painter::new(
|
||||
wgpu::Backends::PRIMARY | wgpu::Backends::GL,
|
||||
wgpu::PowerPreference::HighPerformance,
|
||||
wgpu::DeviceDescriptor {
|
||||
label: None,
|
||||
features: wgpu::Features::default(),
|
||||
limits: wgpu::Limits::default(),
|
||||
},
|
||||
wgpu::PresentMode::Fifo,
|
||||
native_options.multisampling.max(1) as _,
|
||||
);
|
||||
#[cfg(not(target_os = "android"))]
|
||||
painter.set_window(Some(&window));
|
||||
painter
|
||||
};
|
||||
|
||||
let wgpu_render_state = painter.render_state();
|
||||
|
||||
let system_theme = native_options.system_theme();
|
||||
let mut integration = epi_integration::EpiIntegration::new(
|
||||
event_loop,
|
||||
painter.max_texture_side().unwrap_or(2048),
|
||||
&window,
|
||||
system_theme,
|
||||
storage,
|
||||
#[cfg(feature = "glow")]
|
||||
None,
|
||||
wgpu_render_state.clone(),
|
||||
);
|
||||
let theme = system_theme.unwrap_or(native_options.default_theme);
|
||||
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
||||
|
||||
{
|
||||
let event_loop_proxy = egui::mutex::Mutex::new(event_loop.create_proxy());
|
||||
integration.egui_ctx.set_request_repaint_callback(move || {
|
||||
event_loop_proxy.lock().send_event(RequestRepaintEvent).ok();
|
||||
});
|
||||
}
|
||||
|
||||
let mut app = app_creator(&epi::CreationContext {
|
||||
egui_ctx: integration.egui_ctx.clone(),
|
||||
integration_info: integration.frame.info(),
|
||||
storage: integration.frame.storage(),
|
||||
#[cfg(feature = "glow")]
|
||||
gl: None,
|
||||
wgpu_render_state,
|
||||
});
|
||||
|
||||
if app.warm_up_enabled() {
|
||||
integration.warm_up(app.as_mut(), &window);
|
||||
}
|
||||
|
||||
Self {
|
||||
window,
|
||||
painter,
|
||||
integration,
|
||||
app,
|
||||
is_focused: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WinitApp for WgpuWinitApp {
|
||||
fn is_focused(&self) -> bool {
|
||||
self.is_focused
|
||||
}
|
||||
|
||||
fn integration(&self) -> &EpiIntegration {
|
||||
&self.integration
|
||||
}
|
||||
|
||||
fn window(&self) -> &winit::window::Window {
|
||||
&self.window
|
||||
}
|
||||
|
||||
fn save_and_destroy(&mut self) {
|
||||
self.integration.save(&mut *self.app, &self.window);
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
self.app.on_exit(None);
|
||||
|
||||
#[cfg(not(feature = "glow"))]
|
||||
self.app.on_exit();
|
||||
|
||||
self.painter.destroy();
|
||||
}
|
||||
|
||||
fn paint(&mut self) -> EventResult {
|
||||
#[cfg(feature = "puffin")]
|
||||
puffin::GlobalProfiler::lock().new_frame();
|
||||
crate::profile_scope!("frame");
|
||||
|
||||
let Self {
|
||||
window,
|
||||
app,
|
||||
integration,
|
||||
painter,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
repaint_after,
|
||||
textures_delta,
|
||||
shapes,
|
||||
} = integration.update(app.as_mut(), window);
|
||||
|
||||
integration.handle_platform_output(window, platform_output);
|
||||
|
||||
let clipped_primitives = {
|
||||
crate::profile_scope!("tessellate");
|
||||
integration.egui_ctx.tessellate(shapes)
|
||||
};
|
||||
|
||||
painter.paint_and_update_textures(
|
||||
integration.egui_ctx.pixels_per_point(),
|
||||
app.clear_color(&integration.egui_ctx.style().visuals),
|
||||
&clipped_primitives,
|
||||
&textures_delta,
|
||||
);
|
||||
|
||||
integration.post_rendering(app.as_mut(), window);
|
||||
|
||||
let control_flow = if integration.should_quit() {
|
||||
EventResult::Exit
|
||||
} else if repaint_after.is_zero() {
|
||||
EventResult::RepaintAsap
|
||||
} else if let Some(repaint_after_instant) =
|
||||
std::time::Instant::now().checked_add(repaint_after)
|
||||
{
|
||||
// if repaint_after is something huge and can't be added to Instant,
|
||||
// we will use `ControlFlow::Wait` instead.
|
||||
// technically, this might lead to some weird corner cases where the user *WANTS*
|
||||
// winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own
|
||||
// egui backend impl i guess.
|
||||
EventResult::RepaintAt(repaint_after_instant)
|
||||
} else {
|
||||
EventResult::Wait
|
||||
};
|
||||
|
||||
integration.maybe_autosave(app.as_mut(), window);
|
||||
|
||||
if !self.is_focused {
|
||||
// On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325
|
||||
// We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208
|
||||
// But we know if we are focused (in foreground). When minimized, we are not focused.
|
||||
// However, a user may want an egui with an animation in the background,
|
||||
// so we still need to repaint quite fast.
|
||||
crate::profile_scope!("bg_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
|
||||
control_flow
|
||||
}
|
||||
|
||||
fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult {
|
||||
match event {
|
||||
#[cfg(target_os = "android")]
|
||||
winit::event::Event::Resumed => unsafe {
|
||||
self.painter.set_window(Some(&self.window));
|
||||
EventResult::RepaintAsap
|
||||
},
|
||||
#[cfg(target_os = "android")]
|
||||
winit::event::Event::Suspended => unsafe {
|
||||
self.painter.set_window(None);
|
||||
EventResult::Wait
|
||||
},
|
||||
|
||||
winit::event::Event::WindowEvent { event, .. } => {
|
||||
match &event {
|
||||
winit::event::WindowEvent::Focused(new_focused) => {
|
||||
self.is_focused = *new_focused;
|
||||
}
|
||||
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 physical_size.width > 0 && physical_size.height > 0 {
|
||||
self.painter
|
||||
.on_window_resized(physical_size.width, physical_size.height);
|
||||
}
|
||||
}
|
||||
winit::event::WindowEvent::ScaleFactorChanged {
|
||||
new_inner_size, ..
|
||||
} => {
|
||||
self.painter
|
||||
.on_window_resized(new_inner_size.width, new_inner_size.height);
|
||||
}
|
||||
winit::event::WindowEvent::CloseRequested
|
||||
if self.integration.should_quit() =>
|
||||
{
|
||||
return EventResult::Exit
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
self.integration.on_event(self.app.as_mut(), &event);
|
||||
if self.integration.should_quit() {
|
||||
EventResult::Exit
|
||||
} else {
|
||||
// TODO(emilk): ask egui if the event warrants a repaint
|
||||
EventResult::RepaintAsap
|
||||
}
|
||||
}
|
||||
_ => EventResult::Wait,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_wgpu(
|
||||
app_name: &str,
|
||||
native_options: &epi::NativeOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) {
|
||||
if native_options.run_and_return {
|
||||
with_event_loop(|event_loop| {
|
||||
let wgpu_eframe =
|
||||
WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
|
||||
run_and_return(event_loop, wgpu_eframe);
|
||||
});
|
||||
} else {
|
||||
let event_loop = winit::event_loop::EventLoopBuilder::with_user_event().build();
|
||||
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||
run_and_exit(event_loop, wgpu_eframe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub use wgpu_integration::run_wgpu;
|
||||
560
crates/eframe/src/web/backend.rs
Normal file
560
crates/eframe/src/web/backend.rs
Normal file
@@ -0,0 +1,560 @@
|
||||
use super::{glow_wrapping::WrappedGlowPainter, *};
|
||||
|
||||
use crate::epi;
|
||||
|
||||
use egui::{
|
||||
mutex::{Mutex, MutexGuard},
|
||||
TexturesDelta,
|
||||
};
|
||||
pub use egui::{pos2, Color32};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Data gathered between frames.
|
||||
#[derive(Default)]
|
||||
pub struct WebInput {
|
||||
/// Required because we don't get a position on touched
|
||||
pub latest_touch_pos: Option<egui::Pos2>,
|
||||
|
||||
/// Required to maintain a stable touch position for multi-touch gestures.
|
||||
pub latest_touch_pos_id: Option<egui::TouchId>,
|
||||
|
||||
pub raw: egui::RawInput,
|
||||
}
|
||||
|
||||
impl WebInput {
|
||||
pub fn new_frame(&mut self, canvas_size: egui::Vec2) -> egui::RawInput {
|
||||
egui::RawInput {
|
||||
screen_rect: Some(egui::Rect::from_min_size(Default::default(), canvas_size)),
|
||||
pixels_per_point: Some(native_pixels_per_point()), // We ALWAYS use the native pixels-per-point
|
||||
time: Some(now_sec()),
|
||||
..self.raw.take()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
use std::sync::atomic::Ordering::SeqCst;
|
||||
|
||||
/// Stores when to do the next repaint.
|
||||
pub struct NeedRepaint(Mutex<f64>);
|
||||
|
||||
impl Default for NeedRepaint {
|
||||
fn default() -> Self {
|
||||
Self(Mutex::new(f64::NEG_INFINITY)) // start with a repaint
|
||||
}
|
||||
}
|
||||
|
||||
impl NeedRepaint {
|
||||
/// Returns the time (in [`now_sec`] scale) when
|
||||
/// we should next repaint.
|
||||
pub fn when_to_repaint(&self) -> f64 {
|
||||
*self.0.lock()
|
||||
}
|
||||
|
||||
/// Unschedule repainting.
|
||||
pub fn clear(&self) {
|
||||
*self.0.lock() = f64::INFINITY;
|
||||
}
|
||||
|
||||
pub fn repaint_after(&self, num_seconds: f64) {
|
||||
let mut repaint_time = self.0.lock();
|
||||
*repaint_time = repaint_time.min(now_sec() + num_seconds);
|
||||
}
|
||||
|
||||
pub fn repaint_asap(&self) {
|
||||
*self.0.lock() = f64::NEG_INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IsDestroyed(std::sync::atomic::AtomicBool);
|
||||
|
||||
impl Default for IsDestroyed {
|
||||
fn default() -> Self {
|
||||
Self(false.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IsDestroyed {
|
||||
pub fn fetch(&self) -> bool {
|
||||
self.0.load(SeqCst)
|
||||
}
|
||||
|
||||
pub fn set_true(&self) {
|
||||
self.0.store(true, SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn web_location() -> epi::Location {
|
||||
let location = web_sys::window().unwrap().location();
|
||||
|
||||
let hash = percent_decode(&location.hash().unwrap_or_default());
|
||||
|
||||
let query = location
|
||||
.search()
|
||||
.unwrap_or_default()
|
||||
.strip_prefix('?')
|
||||
.map(percent_decode)
|
||||
.unwrap_or_default();
|
||||
|
||||
let query_map = parse_query_map(&query)
|
||||
.iter()
|
||||
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
|
||||
.collect();
|
||||
|
||||
epi::Location {
|
||||
url: percent_decode(&location.href().unwrap_or_default()),
|
||||
protocol: percent_decode(&location.protocol().unwrap_or_default()),
|
||||
host: percent_decode(&location.host().unwrap_or_default()),
|
||||
hostname: percent_decode(&location.hostname().unwrap_or_default()),
|
||||
port: percent_decode(&location.port().unwrap_or_default()),
|
||||
hash,
|
||||
query,
|
||||
query_map,
|
||||
origin: percent_decode(&location.origin().unwrap_or_default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_query_map(query: &str) -> BTreeMap<&str, &str> {
|
||||
query
|
||||
.split('&')
|
||||
.filter_map(|pair| {
|
||||
if pair.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(if let Some((key, value)) = pair.split_once('=') {
|
||||
(key, value)
|
||||
} else {
|
||||
(pair, "")
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_query() {
|
||||
assert_eq!(parse_query_map(""), BTreeMap::default());
|
||||
assert_eq!(parse_query_map("foo"), BTreeMap::from_iter([("foo", "")]));
|
||||
assert_eq!(
|
||||
parse_query_map("foo=bar"),
|
||||
BTreeMap::from_iter([("foo", "bar")])
|
||||
);
|
||||
assert_eq!(
|
||||
parse_query_map("foo=bar&baz=42"),
|
||||
BTreeMap::from_iter([("foo", "bar"), ("baz", "42")])
|
||||
);
|
||||
assert_eq!(
|
||||
parse_query_map("foo&baz=42"),
|
||||
BTreeMap::from_iter([("foo", ""), ("baz", "42")])
|
||||
);
|
||||
assert_eq!(
|
||||
parse_query_map("foo&baz&&"),
|
||||
BTreeMap::from_iter([("foo", ""), ("baz", "")])
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub struct AppRunner {
|
||||
pub(crate) frame: epi::Frame,
|
||||
egui_ctx: egui::Context,
|
||||
painter: WrappedGlowPainter,
|
||||
pub(crate) input: WebInput,
|
||||
app: Box<dyn epi::App>,
|
||||
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
|
||||
pub(crate) is_destroyed: std::sync::Arc<IsDestroyed>,
|
||||
last_save_time: f64,
|
||||
screen_reader: super::screen_reader::ScreenReader,
|
||||
pub(crate) text_cursor_pos: Option<egui::Pos2>,
|
||||
pub(crate) mutable_text_under_cursor: bool,
|
||||
textures_delta: TexturesDelta,
|
||||
pub events_to_unsubscribe: Vec<EventToUnsubscribe>,
|
||||
}
|
||||
|
||||
impl Drop for AppRunner {
|
||||
fn drop(&mut self) {
|
||||
tracing::debug!("AppRunner has fully dropped");
|
||||
}
|
||||
}
|
||||
|
||||
impl AppRunner {
|
||||
pub fn new(
|
||||
canvas_id: &str,
|
||||
web_options: crate::WebOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) -> Result<Self, JsValue> {
|
||||
let painter = WrappedGlowPainter::new(canvas_id, web_options.webgl_context_option)
|
||||
.map_err(JsValue::from)?; // fail early
|
||||
|
||||
let system_theme = if web_options.follow_system_theme {
|
||||
super::system_theme()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let info = epi::IntegrationInfo {
|
||||
web_info: epi::WebInfo {
|
||||
location: web_location(),
|
||||
},
|
||||
system_theme,
|
||||
cpu_usage: None,
|
||||
native_pixels_per_point: Some(native_pixels_per_point()),
|
||||
};
|
||||
let storage = LocalStorage::default();
|
||||
|
||||
let egui_ctx = egui::Context::default();
|
||||
load_memory(&egui_ctx);
|
||||
|
||||
let theme = system_theme.unwrap_or(web_options.default_theme);
|
||||
egui_ctx.set_visuals(theme.egui_visuals());
|
||||
|
||||
let app = app_creator(&epi::CreationContext {
|
||||
egui_ctx: egui_ctx.clone(),
|
||||
integration_info: info.clone(),
|
||||
storage: Some(&storage),
|
||||
#[cfg(feature = "glow")]
|
||||
gl: Some(painter.painter.gl().clone()),
|
||||
#[cfg(feature = "wgpu")]
|
||||
wgpu_render_state: None,
|
||||
});
|
||||
|
||||
let frame = epi::Frame {
|
||||
info,
|
||||
output: Default::default(),
|
||||
storage: Some(Box::new(storage)),
|
||||
#[cfg(feature = "glow")]
|
||||
gl: Some(painter.gl().clone()),
|
||||
#[cfg(feature = "wgpu")]
|
||||
wgpu_render_state: None,
|
||||
};
|
||||
|
||||
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();
|
||||
{
|
||||
let needs_repaint = needs_repaint.clone();
|
||||
egui_ctx.set_request_repaint_callback(move || {
|
||||
needs_repaint.repaint_asap();
|
||||
});
|
||||
}
|
||||
|
||||
let mut runner = Self {
|
||||
frame,
|
||||
egui_ctx,
|
||||
painter,
|
||||
input: Default::default(),
|
||||
app,
|
||||
needs_repaint,
|
||||
is_destroyed: Default::default(),
|
||||
last_save_time: now_sec(),
|
||||
screen_reader: Default::default(),
|
||||
text_cursor_pos: None,
|
||||
mutable_text_under_cursor: false,
|
||||
textures_delta: Default::default(),
|
||||
events_to_unsubscribe: Default::default(),
|
||||
};
|
||||
|
||||
runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side());
|
||||
|
||||
Ok(runner)
|
||||
}
|
||||
|
||||
pub fn egui_ctx(&self) -> &egui::Context {
|
||||
&self.egui_ctx
|
||||
}
|
||||
|
||||
pub fn auto_save(&mut self) {
|
||||
let now = now_sec();
|
||||
let time_since_last_save = now - self.last_save_time;
|
||||
|
||||
if time_since_last_save > self.app.auto_save_interval().as_secs_f64() {
|
||||
if self.app.persist_egui_memory() {
|
||||
save_memory(&self.egui_ctx);
|
||||
}
|
||||
if let Some(storage) = self.frame.storage_mut() {
|
||||
self.app.save(storage);
|
||||
}
|
||||
self.last_save_time = now;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn canvas_id(&self) -> &str {
|
||||
self.painter.canvas_id()
|
||||
}
|
||||
|
||||
pub fn warm_up(&mut self) -> Result<(), JsValue> {
|
||||
if self.app.warm_up_enabled() {
|
||||
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
|
||||
self.egui_ctx.memory().set_everything_is_visible(true);
|
||||
self.logic()?;
|
||||
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
|
||||
self.egui_ctx.clear_animations();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn destroy(&mut self) -> Result<(), JsValue> {
|
||||
let is_destroyed_already = self.is_destroyed.fetch();
|
||||
|
||||
if is_destroyed_already {
|
||||
tracing::warn!("App was destroyed already");
|
||||
Ok(())
|
||||
} else {
|
||||
tracing::debug!("Destroying");
|
||||
for x in self.events_to_unsubscribe.drain(..) {
|
||||
x.unsubscribe()?;
|
||||
}
|
||||
|
||||
self.painter.destroy();
|
||||
self.is_destroyed.set_true();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns how long to wait until the next repaint.
|
||||
///
|
||||
/// Call [`Self::paint`] later to paint
|
||||
pub fn logic(&mut self) -> Result<(std::time::Duration, Vec<egui::ClippedPrimitive>), JsValue> {
|
||||
let frame_start = now_sec();
|
||||
|
||||
resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points());
|
||||
let canvas_size = canvas_size_in_points(self.canvas_id());
|
||||
let raw_input = self.input.new_frame(canvas_size);
|
||||
|
||||
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
|
||||
self.app.update(egui_ctx, &mut self.frame);
|
||||
});
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
repaint_after,
|
||||
textures_delta,
|
||||
shapes,
|
||||
} = full_output;
|
||||
|
||||
self.handle_platform_output(platform_output);
|
||||
self.textures_delta.append(textures_delta);
|
||||
let clipped_primitives = self.egui_ctx.tessellate(shapes);
|
||||
|
||||
{
|
||||
let app_output = self.frame.take_app_output();
|
||||
let epi::backend::AppOutput {} = app_output;
|
||||
}
|
||||
|
||||
self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32);
|
||||
Ok((repaint_after, clipped_primitives))
|
||||
}
|
||||
|
||||
pub fn clear_color_buffer(&self) {
|
||||
self.painter
|
||||
.clear(self.app.clear_color(&self.egui_ctx.style().visuals));
|
||||
}
|
||||
|
||||
/// Paint the results of the last call to [`Self::logic`].
|
||||
pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> {
|
||||
let textures_delta = std::mem::take(&mut self.textures_delta);
|
||||
|
||||
self.painter.paint_and_update_textures(
|
||||
clipped_primitives,
|
||||
self.egui_ctx.pixels_per_point(),
|
||||
&textures_delta,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
|
||||
if self.egui_ctx.options().screen_reader {
|
||||
self.screen_reader
|
||||
.speak(&platform_output.events_description());
|
||||
}
|
||||
|
||||
let egui::PlatformOutput {
|
||||
cursor_icon,
|
||||
open_url,
|
||||
copied_text,
|
||||
events: _, // already handled
|
||||
mutable_text_under_cursor,
|
||||
text_cursor_pos,
|
||||
} = platform_output;
|
||||
|
||||
set_cursor_icon(cursor_icon);
|
||||
if let Some(open) = open_url {
|
||||
super::open_url(&open.url, open.new_tab);
|
||||
}
|
||||
|
||||
#[cfg(web_sys_unstable_apis)]
|
||||
if !copied_text.is_empty() {
|
||||
set_clipboard_text(&copied_text);
|
||||
}
|
||||
|
||||
#[cfg(not(web_sys_unstable_apis))]
|
||||
let _ = copied_text;
|
||||
|
||||
self.mutable_text_under_cursor = mutable_text_under_cursor;
|
||||
|
||||
if self.text_cursor_pos != text_cursor_pos {
|
||||
text_agent::move_text_cursor(text_cursor_pos, self.canvas_id());
|
||||
self.text_cursor_pos = text_cursor_pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub type AppRunnerRef = Arc<Mutex<AppRunner>>;
|
||||
|
||||
pub struct TargetEvent {
|
||||
target: EventTarget,
|
||||
event_name: String,
|
||||
closure: Closure<dyn FnMut(web_sys::Event)>,
|
||||
}
|
||||
|
||||
pub struct IntervalHandle {
|
||||
pub handle: i32,
|
||||
pub closure: Closure<dyn FnMut()>,
|
||||
}
|
||||
|
||||
pub enum EventToUnsubscribe {
|
||||
TargetEvent(TargetEvent),
|
||||
#[allow(dead_code)]
|
||||
IntervalHandle(IntervalHandle),
|
||||
}
|
||||
|
||||
impl EventToUnsubscribe {
|
||||
pub fn unsubscribe(self) -> Result<(), JsValue> {
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
match self {
|
||||
EventToUnsubscribe::TargetEvent(handle) => {
|
||||
handle.target.remove_event_listener_with_callback(
|
||||
handle.event_name.as_str(),
|
||||
handle.closure.as_ref().unchecked_ref(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
EventToUnsubscribe::IntervalHandle(handle) => {
|
||||
let window = web_sys::window().unwrap();
|
||||
window.clear_interval_with_handle(handle.handle);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct AppRunnerContainer {
|
||||
pub runner: AppRunnerRef,
|
||||
|
||||
/// Set to `true` if there is a panic.
|
||||
/// Used to ignore callbacks after a panic.
|
||||
pub panicked: Arc<AtomicBool>,
|
||||
pub events: Vec<EventToUnsubscribe>,
|
||||
}
|
||||
|
||||
impl AppRunnerContainer {
|
||||
/// Convenience function to reduce boilerplate and ensure that all event handlers
|
||||
/// are dealt with in the same way
|
||||
///
|
||||
|
||||
#[must_use]
|
||||
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
|
||||
&mut self,
|
||||
target: &EventTarget,
|
||||
event_name: &'static str,
|
||||
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
|
||||
) -> Result<(), JsValue> {
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
// Create a JS closure based on the FnMut provided
|
||||
let closure = Closure::wrap({
|
||||
// Clone atomics
|
||||
let runner_ref = self.runner.clone();
|
||||
let panicked = self.panicked.clone();
|
||||
|
||||
Box::new(move |event: web_sys::Event| {
|
||||
// Only call the wrapped closure if the egui code has not panicked
|
||||
if !panicked.load(Ordering::SeqCst) {
|
||||
// Cast the event to the expected event type
|
||||
let event = event.unchecked_into::<E>();
|
||||
|
||||
closure(event, runner_ref.lock());
|
||||
}
|
||||
}) as Box<dyn FnMut(web_sys::Event)>
|
||||
});
|
||||
|
||||
// Add the event listener to the target
|
||||
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
|
||||
|
||||
let handle = TargetEvent {
|
||||
target: target.clone(),
|
||||
event_name: event_name.to_string(),
|
||||
closure,
|
||||
};
|
||||
|
||||
self.events.push(EventToUnsubscribe::TargetEvent(handle));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Install event listeners to register different input events
|
||||
/// and start running the given app.
|
||||
pub fn start(
|
||||
canvas_id: &str,
|
||||
web_options: crate::WebOptions,
|
||||
app_creator: epi::AppCreator,
|
||||
) -> Result<AppRunnerRef, JsValue> {
|
||||
let mut runner = AppRunner::new(canvas_id, web_options, app_creator)?;
|
||||
runner.warm_up()?;
|
||||
start_runner(runner)
|
||||
}
|
||||
|
||||
/// Install event listeners to register different input events
|
||||
/// and starts running the given [`AppRunner`].
|
||||
fn start_runner(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
|
||||
let mut runner_container = AppRunnerContainer {
|
||||
runner: Arc::new(Mutex::new(app_runner)),
|
||||
panicked: Arc::new(AtomicBool::new(false)),
|
||||
events: Vec::with_capacity(20),
|
||||
};
|
||||
|
||||
super::events::install_canvas_events(&mut runner_container)?;
|
||||
super::events::install_document_events(&mut runner_container)?;
|
||||
text_agent::install_text_agent(&mut runner_container)?;
|
||||
|
||||
super::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?;
|
||||
|
||||
// Disable all event handlers on panic
|
||||
let previous_hook = std::panic::take_hook();
|
||||
|
||||
runner_container.runner.lock().events_to_unsubscribe = runner_container.events;
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
tracing::info!("egui disabled all event handlers due to panic");
|
||||
runner_container.panicked.store(true, SeqCst);
|
||||
|
||||
// Propagate panic info to the previously registered panic hook
|
||||
previous_hook(panic_info);
|
||||
}));
|
||||
|
||||
Ok(runner_container.runner)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Default)]
|
||||
struct LocalStorage {}
|
||||
|
||||
impl epi::Storage for LocalStorage {
|
||||
fn get_string(&self, key: &str) -> Option<String> {
|
||||
local_storage_get(key)
|
||||
}
|
||||
|
||||
fn set_string(&mut self, key: &str, value: String) {
|
||||
local_storage_set(key, &value);
|
||||
}
|
||||
|
||||
fn flush(&mut self) {}
|
||||
}
|
||||
519
crates/eframe/src/web/events.rs
Normal file
519
crates/eframe/src/web/events.rs
Normal file
@@ -0,0 +1,519 @@
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
struct IsDestroyed(pub bool);
|
||||
|
||||
pub fn paint_and_schedule(
|
||||
runner_ref: &AppRunnerRef,
|
||||
panicked: Arc<AtomicBool>,
|
||||
) -> Result<(), JsValue> {
|
||||
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<IsDestroyed, JsValue> {
|
||||
let mut runner_lock = runner_ref.lock();
|
||||
let is_destroyed = runner_lock.is_destroyed.fetch();
|
||||
|
||||
if !is_destroyed && runner_lock.needs_repaint.when_to_repaint() <= now_sec() {
|
||||
runner_lock.needs_repaint.clear();
|
||||
runner_lock.clear_color_buffer();
|
||||
let (repaint_after, clipped_primitives) = runner_lock.logic()?;
|
||||
runner_lock.paint(&clipped_primitives)?;
|
||||
runner_lock
|
||||
.needs_repaint
|
||||
.repaint_after(repaint_after.as_secs_f64());
|
||||
runner_lock.auto_save();
|
||||
}
|
||||
|
||||
Ok(IsDestroyed(is_destroyed))
|
||||
}
|
||||
|
||||
fn request_animation_frame(
|
||||
runner_ref: AppRunnerRef,
|
||||
panicked: Arc<AtomicBool>,
|
||||
) -> Result<(), JsValue> {
|
||||
use wasm_bindgen::JsCast;
|
||||
let window = web_sys::window().unwrap();
|
||||
let closure = Closure::once(move || paint_and_schedule(&runner_ref, panicked));
|
||||
window.request_animation_frame(closure.as_ref().unchecked_ref())?;
|
||||
closure.forget(); // We must forget it, or else the callback is canceled on drop
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Only paint and schedule if there has been no panic
|
||||
if !panicked.load(Ordering::SeqCst) {
|
||||
let is_destroyed = paint_if_needed(runner_ref)?;
|
||||
if !is_destroyed.0 {
|
||||
request_animation_frame(runner_ref.clone(), panicked)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_document_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||
let window = web_sys::window().unwrap();
|
||||
let document = window.document().unwrap();
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&document,
|
||||
"keydown",
|
||||
|event: web_sys::KeyboardEvent, mut runner_lock| {
|
||||
if event.is_composing() || event.key_code() == 229 {
|
||||
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
|
||||
return;
|
||||
}
|
||||
|
||||
let modifiers = modifiers_from_event(&event);
|
||||
runner_lock.input.raw.modifiers = modifiers;
|
||||
|
||||
let key = event.key();
|
||||
|
||||
if let Some(key) = translate_key(&key) {
|
||||
runner_lock.input.raw.events.push(egui::Event::Key {
|
||||
key,
|
||||
pressed: true,
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
if !modifiers.ctrl
|
||||
&& !modifiers.command
|
||||
&& !should_ignore_key(&key)
|
||||
// When text agent is shown, it sends text event instead.
|
||||
&& text_agent::text_agent().hidden()
|
||||
{
|
||||
runner_lock.input.raw.events.push(egui::Event::Text(key));
|
||||
}
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
|
||||
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input();
|
||||
|
||||
let prevent_default = if matches!(event.key().as_str(), "Tab") {
|
||||
// Always prevent moving cursor to url bar.
|
||||
// egui wants to use tab to move to the next text field.
|
||||
true
|
||||
} else if egui_wants_keyboard {
|
||||
matches!(
|
||||
event.key().as_str(),
|
||||
"Backspace" // so we don't go back to previous page when deleting text
|
||||
| "ArrowDown" | "ArrowLeft" | "ArrowRight" | "ArrowUp" // cmd-left is "back" on Mac (https://github.com/emilk/egui/issues/58)
|
||||
)
|
||||
} else {
|
||||
// We never want to prevent:
|
||||
// * F5 / cmd-R (refresh)
|
||||
// * cmd-shift-C (debug tools)
|
||||
// * cmd/ctrl-c/v/x (or we stop copy/past/cut events)
|
||||
false
|
||||
};
|
||||
|
||||
// tracing::debug!(
|
||||
// "On key-down {:?}, egui_wants_keyboard: {}, prevent_default: {}",
|
||||
// event.key().as_str(),
|
||||
// egui_wants_keyboard,
|
||||
// prevent_default
|
||||
// );
|
||||
|
||||
if prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&document,
|
||||
"keyup",
|
||||
|event: web_sys::KeyboardEvent, mut runner_lock| {
|
||||
let modifiers = modifiers_from_event(&event);
|
||||
runner_lock.input.raw.modifiers = modifiers;
|
||||
if let Some(key) = translate_key(&event.key()) {
|
||||
runner_lock.input.raw.events.push(egui::Event::Key {
|
||||
key,
|
||||
pressed: false,
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
|
||||
#[cfg(web_sys_unstable_apis)]
|
||||
runner_container.add_event_listener(
|
||||
&document,
|
||||
"paste",
|
||||
|event: web_sys::ClipboardEvent, mut runner_lock| {
|
||||
if let Some(data) = event.clipboard_data() {
|
||||
if let Ok(text) = data.get_data("text") {
|
||||
let text = text.replace("\r\n", "\n");
|
||||
if !text.is_empty() {
|
||||
runner_lock.input.raw.events.push(egui::Event::Paste(text));
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
#[cfg(web_sys_unstable_apis)]
|
||||
runner_container.add_event_listener(
|
||||
&document,
|
||||
"cut",
|
||||
|_: web_sys::ClipboardEvent, mut runner_lock| {
|
||||
runner_lock.input.raw.events.push(egui::Event::Cut);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
|
||||
#[cfg(web_sys_unstable_apis)]
|
||||
runner_container.add_event_listener(
|
||||
&document,
|
||||
"copy",
|
||||
|_: web_sys::ClipboardEvent, mut runner_lock| {
|
||||
runner_lock.input.raw.events.push(egui::Event::Copy);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
|
||||
for event_name in &["load", "pagehide", "pageshow", "resize"] {
|
||||
runner_container.add_event_listener(
|
||||
&window,
|
||||
event_name,
|
||||
|_: web_sys::Event, runner_lock| {
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&window,
|
||||
"hashchange",
|
||||
|_: web_sys::Event, mut runner_lock| {
|
||||
// `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here
|
||||
runner_lock.frame.info.web_info.location.hash = location_hash();
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_canvas_events(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
|
||||
|
||||
{
|
||||
// By default, right-clicks open a context menu.
|
||||
// We don't want to do that (right clicks is handled by egui):
|
||||
let event_name = "contextmenu";
|
||||
|
||||
let closure =
|
||||
move |event: web_sys::MouseEvent,
|
||||
mut _runner_lock: egui::mutex::MutexGuard<AppRunner>| {
|
||||
event.prevent_default();
|
||||
};
|
||||
|
||||
runner_container.add_event_listener(&canvas, event_name, closure)?;
|
||||
}
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"mousedown",
|
||||
|event: web_sys::MouseEvent, mut runner_lock: egui::mutex::MutexGuard<AppRunner>| {
|
||||
if let Some(button) = button_from_mouse_event(&event) {
|
||||
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
|
||||
let modifiers = runner_lock.input.raw.modifiers;
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
button,
|
||||
pressed: true,
|
||||
modifiers,
|
||||
});
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
event.stop_propagation();
|
||||
// Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here.
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"mousemove",
|
||||
|event: web_sys::MouseEvent, mut runner_lock| {
|
||||
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::PointerMoved(pos));
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"mouseup",
|
||||
|event: web_sys::MouseEvent, mut runner_lock| {
|
||||
if let Some(button) = button_from_mouse_event(&event) {
|
||||
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
|
||||
let modifiers = runner_lock.input.raw.modifiers;
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
button,
|
||||
pressed: false,
|
||||
modifiers,
|
||||
});
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
|
||||
text_agent::update_text_agent(runner_lock);
|
||||
}
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"mouseleave",
|
||||
|event: web_sys::MouseEvent, mut runner_lock| {
|
||||
runner_lock.input.raw.events.push(egui::Event::PointerGone);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"touchstart",
|
||||
|event: web_sys::TouchEvent, mut runner_lock| {
|
||||
let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id;
|
||||
let pos =
|
||||
pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id);
|
||||
runner_lock.input.latest_touch_pos_id = latest_touch_pos_id;
|
||||
runner_lock.input.latest_touch_pos = Some(pos);
|
||||
let modifiers = runner_lock.input.raw.modifiers;
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed: true,
|
||||
modifiers,
|
||||
});
|
||||
|
||||
push_touches(&mut *runner_lock, egui::TouchPhase::Start, &event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"touchmove",
|
||||
|event: web_sys::TouchEvent, mut runner_lock| {
|
||||
let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id;
|
||||
let pos =
|
||||
pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id);
|
||||
runner_lock.input.latest_touch_pos_id = latest_touch_pos_id;
|
||||
runner_lock.input.latest_touch_pos = Some(pos);
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::PointerMoved(pos));
|
||||
|
||||
push_touches(&mut *runner_lock, egui::TouchPhase::Move, &event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"touchend",
|
||||
|event: web_sys::TouchEvent, mut runner_lock| {
|
||||
if let Some(pos) = runner_lock.input.latest_touch_pos {
|
||||
let modifiers = runner_lock.input.raw.modifiers;
|
||||
// First release mouse to click:
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed: false,
|
||||
modifiers,
|
||||
});
|
||||
// Then remove hover effect:
|
||||
runner_lock.input.raw.events.push(egui::Event::PointerGone);
|
||||
|
||||
push_touches(&mut *runner_lock, egui::TouchPhase::End, &event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
|
||||
// Finally, focus or blur text agent to toggle mobile keyboard:
|
||||
text_agent::update_text_agent(runner_lock);
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"touchcancel",
|
||||
|event: web_sys::TouchEvent, mut runner_lock| {
|
||||
push_touches(&mut runner_lock, egui::TouchPhase::Cancel, &event);
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"wheel",
|
||||
|event: web_sys::WheelEvent, mut runner_lock| {
|
||||
let scroll_multiplier = match event.delta_mode() {
|
||||
web_sys::WheelEvent::DOM_DELTA_PAGE => {
|
||||
canvas_size_in_points(runner_lock.canvas_id()).y
|
||||
}
|
||||
web_sys::WheelEvent::DOM_DELTA_LINE => {
|
||||
#[allow(clippy::let_and_return)]
|
||||
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in egui_glium / winit.
|
||||
points_per_scroll_line
|
||||
}
|
||||
_ => 1.0, // DOM_DELTA_PIXEL
|
||||
};
|
||||
|
||||
let mut delta =
|
||||
-scroll_multiplier * egui::vec2(event.delta_x() as f32, event.delta_y() as f32);
|
||||
|
||||
// Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed.
|
||||
// This if-statement is equivalent to how `Modifiers.command` is determined in
|
||||
// `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`].
|
||||
if event.ctrl_key() || event.meta_key() {
|
||||
let factor = (delta.y / 200.0).exp();
|
||||
runner_lock.input.raw.events.push(egui::Event::Zoom(factor));
|
||||
} else {
|
||||
if event.shift_key() {
|
||||
// Treat as horizontal scrolling.
|
||||
// Note: one Mac we already get horizontal scroll events when shift is down.
|
||||
delta = egui::vec2(delta.x + delta.y, 0.0);
|
||||
}
|
||||
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::Scroll(delta));
|
||||
}
|
||||
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"dragover",
|
||||
|event: web_sys::DragEvent, mut runner_lock| {
|
||||
if let Some(data_transfer) = event.data_transfer() {
|
||||
runner_lock.input.raw.hovered_files.clear();
|
||||
for i in 0..data_transfer.items().length() {
|
||||
if let Some(item) = data_transfer.items().get(i) {
|
||||
runner_lock.input.raw.hovered_files.push(egui::HoveredFile {
|
||||
mime: item.type_(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&canvas,
|
||||
"dragleave",
|
||||
|event: web_sys::DragEvent, mut runner_lock| {
|
||||
runner_lock.input.raw.hovered_files.clear();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(&canvas, "drop", {
|
||||
let runner_ref = runner_container.runner.clone();
|
||||
|
||||
move |event: web_sys::DragEvent, mut runner_lock| {
|
||||
if let Some(data_transfer) = event.data_transfer() {
|
||||
runner_lock.input.raw.hovered_files.clear();
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
// Unlock the runner so it can be locked after a future await point
|
||||
drop(runner_lock);
|
||||
|
||||
if let Some(files) = data_transfer.files() {
|
||||
for i in 0..files.length() {
|
||||
if let Some(file) = files.get(i) {
|
||||
let name = file.name();
|
||||
let last_modified = std::time::UNIX_EPOCH
|
||||
+ std::time::Duration::from_millis(file.last_modified() as u64);
|
||||
|
||||
tracing::debug!("Loading {:?} ({} bytes)…", name, file.size());
|
||||
|
||||
let future = wasm_bindgen_futures::JsFuture::from(file.array_buffer());
|
||||
|
||||
let runner_ref = runner_ref.clone();
|
||||
let future = async move {
|
||||
match future.await {
|
||||
Ok(array_buffer) => {
|
||||
let bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
|
||||
tracing::debug!(
|
||||
"Loaded {:?} ({} bytes).",
|
||||
name,
|
||||
bytes.len()
|
||||
);
|
||||
|
||||
// Re-lock the mutex on the other side of the await point
|
||||
let mut runner_lock = runner_ref.lock();
|
||||
runner_lock.input.raw.dropped_files.push(
|
||||
egui::DroppedFile {
|
||||
name,
|
||||
last_modified: Some(last_modified),
|
||||
bytes: Some(bytes.into()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to read file: {:?}", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
wasm_bindgen_futures::spawn_local(future);
|
||||
}
|
||||
}
|
||||
}
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
166
crates/eframe/src/web/glow_wrapping.rs
Normal file
166
crates/eframe/src/web/glow_wrapping.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use crate::WebGlContextOption;
|
||||
use egui::{ClippedPrimitive, Rgba};
|
||||
use egui_glow::glow;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use web_sys::{WebGl2RenderingContext, WebGlRenderingContext};
|
||||
|
||||
pub(crate) struct WrappedGlowPainter {
|
||||
pub(crate) canvas: HtmlCanvasElement,
|
||||
pub(crate) canvas_id: String,
|
||||
pub(crate) painter: egui_glow::Painter,
|
||||
}
|
||||
|
||||
impl WrappedGlowPainter {
|
||||
pub fn new(canvas_id: &str, options: WebGlContextOption) -> Result<Self, String> {
|
||||
let canvas = super::canvas_element_or_die(canvas_id);
|
||||
|
||||
let (gl, shader_prefix) = init_glow_context_from_canvas(&canvas, options)?;
|
||||
let gl = std::sync::Arc::new(gl);
|
||||
|
||||
let dimension = [canvas.width() as i32, canvas.height() as i32];
|
||||
let painter = egui_glow::Painter::new(gl, Some(dimension), shader_prefix)
|
||||
.map_err(|error| format!("Error starting glow painter: {}", error))?;
|
||||
|
||||
Ok(Self {
|
||||
canvas,
|
||||
canvas_id: canvas_id.to_owned(),
|
||||
painter,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WrappedGlowPainter {
|
||||
pub fn gl(&self) -> &std::sync::Arc<glow::Context> {
|
||||
self.painter.gl()
|
||||
}
|
||||
|
||||
pub fn max_texture_side(&self) -> usize {
|
||||
self.painter.max_texture_side()
|
||||
}
|
||||
|
||||
pub fn canvas_id(&self) -> &str {
|
||||
&self.canvas_id
|
||||
}
|
||||
|
||||
pub fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) {
|
||||
self.painter.set_texture(tex_id, delta);
|
||||
}
|
||||
|
||||
pub fn free_texture(&mut self, tex_id: egui::TextureId) {
|
||||
self.painter.free_texture(tex_id);
|
||||
}
|
||||
|
||||
pub fn clear(&self, clear_color: Rgba) {
|
||||
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
|
||||
egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
|
||||
}
|
||||
|
||||
pub fn paint_primitives(
|
||||
&mut self,
|
||||
clipped_primitives: &[ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
) -> Result<(), JsValue> {
|
||||
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
|
||||
self.painter
|
||||
.paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn paint_and_update_textures(
|
||||
&mut self,
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
) -> Result<(), JsValue> {
|
||||
for (id, image_delta) in &textures_delta.set {
|
||||
self.set_texture(*id, image_delta);
|
||||
}
|
||||
|
||||
self.paint_primitives(clipped_primitives, pixels_per_point)?;
|
||||
|
||||
for &id in &textures_delta.free {
|
||||
self.free_texture(id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn destroy(&mut self) {
|
||||
self.painter.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns glow context and shader prefix.
|
||||
fn init_glow_context_from_canvas(
|
||||
canvas: &HtmlCanvasElement,
|
||||
options: WebGlContextOption,
|
||||
) -> Result<(glow::Context, &'static str), String> {
|
||||
let result = match options {
|
||||
// Force use WebGl1
|
||||
WebGlContextOption::WebGl1 => init_webgl1(canvas),
|
||||
// Force use WebGl2
|
||||
WebGlContextOption::WebGl2 => init_webgl2(canvas),
|
||||
// Trying WebGl2 first
|
||||
WebGlContextOption::BestFirst => init_webgl2(canvas).or_else(|| init_webgl1(canvas)),
|
||||
// Trying WebGl1 first (useful for testing).
|
||||
WebGlContextOption::CompatibilityFirst => {
|
||||
init_webgl1(canvas).or_else(|| init_webgl2(canvas))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(result) = result {
|
||||
Ok(result)
|
||||
} else {
|
||||
Err("WebGL isn't supported".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn init_webgl1(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static str)> {
|
||||
let gl1_ctx = canvas
|
||||
.get_context("webgl")
|
||||
.expect("Failed to query about WebGL2 context");
|
||||
|
||||
let gl1_ctx = gl1_ctx?;
|
||||
tracing::debug!("WebGL1 selected.");
|
||||
|
||||
let gl1_ctx = gl1_ctx
|
||||
.dyn_into::<web_sys::WebGlRenderingContext>()
|
||||
.unwrap();
|
||||
|
||||
let shader_prefix = if super::webgl1_requires_brightening(&gl1_ctx) {
|
||||
tracing::debug!("Enabling webkitGTK brightening workaround.");
|
||||
"#define APPLY_BRIGHTENING_GAMMA"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let gl = glow::Context::from_webgl1_context(gl1_ctx);
|
||||
|
||||
Some((gl, shader_prefix))
|
||||
}
|
||||
|
||||
fn init_webgl2(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static str)> {
|
||||
let gl2_ctx = canvas
|
||||
.get_context("webgl2")
|
||||
.expect("Failed to query about WebGL2 context");
|
||||
|
||||
let gl2_ctx = gl2_ctx?;
|
||||
tracing::debug!("WebGL2 selected.");
|
||||
|
||||
let gl2_ctx = gl2_ctx
|
||||
.dyn_into::<web_sys::WebGl2RenderingContext>()
|
||||
.unwrap();
|
||||
let gl = glow::Context::from_webgl2_context(gl2_ctx);
|
||||
let shader_prefix = "";
|
||||
|
||||
Some((gl, shader_prefix))
|
||||
}
|
||||
|
||||
trait DummyWebGLConstructor {
|
||||
fn from_webgl1_context(context: web_sys::WebGlRenderingContext) -> Self;
|
||||
|
||||
fn from_webgl2_context(context: web_sys::WebGl2RenderingContext) -> Self;
|
||||
}
|
||||
212
crates/eframe/src/web/input.rs
Normal file
212
crates/eframe/src/web/input.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use super::{canvas_element, canvas_origin, AppRunner};
|
||||
|
||||
pub fn pos_from_mouse_event(canvas_id: &str, event: &web_sys::MouseEvent) -> egui::Pos2 {
|
||||
let canvas = canvas_element(canvas_id).unwrap();
|
||||
let rect = canvas.get_bounding_client_rect();
|
||||
egui::Pos2 {
|
||||
x: event.client_x() as f32 - rect.left() as f32,
|
||||
y: event.client_y() as f32 - rect.top() as f32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option<egui::PointerButton> {
|
||||
match event.button() {
|
||||
0 => Some(egui::PointerButton::Primary),
|
||||
1 => Some(egui::PointerButton::Middle),
|
||||
2 => Some(egui::PointerButton::Secondary),
|
||||
3 => Some(egui::PointerButton::Extra1),
|
||||
4 => Some(egui::PointerButton::Extra2),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A single touch is translated to a pointer movement. When a second touch is added, the pointer
|
||||
/// should not jump to a different position. Therefore, we do not calculate the average position
|
||||
/// of all touches, but we keep using the same touch as long as it is available.
|
||||
///
|
||||
/// `touch_id_for_pos` is the [`TouchId`](egui::TouchId) of the [`Touch`](web_sys::Touch) we previously used to determine the
|
||||
/// pointer position.
|
||||
pub fn pos_from_touch_event(
|
||||
canvas_id: &str,
|
||||
event: &web_sys::TouchEvent,
|
||||
touch_id_for_pos: &mut Option<egui::TouchId>,
|
||||
) -> egui::Pos2 {
|
||||
let touch_for_pos = if let Some(touch_id_for_pos) = touch_id_for_pos {
|
||||
// search for the touch we previously used for the position
|
||||
// (unfortunately, `event.touches()` is not a rust collection):
|
||||
(0..event.touches().length())
|
||||
.into_iter()
|
||||
.map(|i| event.touches().get(i).unwrap())
|
||||
.find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Use the touch found above or pick the first, or return a default position if there is no
|
||||
// touch at all. (The latter is not expected as the current method is only called when there is
|
||||
// at least one touch.)
|
||||
touch_for_pos
|
||||
.or_else(|| event.touches().get(0))
|
||||
.map_or(Default::default(), |touch| {
|
||||
*touch_id_for_pos = Some(egui::TouchId::from(touch.identifier()));
|
||||
pos_from_touch(canvas_origin(canvas_id), &touch)
|
||||
})
|
||||
}
|
||||
|
||||
fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 {
|
||||
egui::Pos2 {
|
||||
x: touch.page_x() as f32 - canvas_origin.x as f32,
|
||||
y: touch.page_y() as f32 - canvas_origin.y as f32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) {
|
||||
let canvas_origin = canvas_origin(runner.canvas_id());
|
||||
for touch_idx in 0..event.changed_touches().length() {
|
||||
if let Some(touch) = event.changed_touches().item(touch_idx) {
|
||||
runner.input.raw.events.push(egui::Event::Touch {
|
||||
device_id: egui::TouchDeviceId(0),
|
||||
id: egui::TouchId::from(touch.identifier()),
|
||||
phase,
|
||||
pos: pos_from_touch(canvas_origin, &touch),
|
||||
force: touch.force(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Web sends all keys as strings, so it is up to us to figure out if it is
|
||||
/// a real text input or the name of a key.
|
||||
pub fn should_ignore_key(key: &str) -> bool {
|
||||
let is_function_key = key.starts_with('F') && key.len() > 1;
|
||||
is_function_key
|
||||
|| matches!(
|
||||
key,
|
||||
"Alt"
|
||||
| "ArrowDown"
|
||||
| "ArrowLeft"
|
||||
| "ArrowRight"
|
||||
| "ArrowUp"
|
||||
| "Backspace"
|
||||
| "CapsLock"
|
||||
| "ContextMenu"
|
||||
| "Control"
|
||||
| "Delete"
|
||||
| "End"
|
||||
| "Enter"
|
||||
| "Esc"
|
||||
| "Escape"
|
||||
| "GroupNext" // https://github.com/emilk/egui/issues/510
|
||||
| "Help"
|
||||
| "Home"
|
||||
| "Insert"
|
||||
| "Meta"
|
||||
| "NumLock"
|
||||
| "PageDown"
|
||||
| "PageUp"
|
||||
| "Pause"
|
||||
| "ScrollLock"
|
||||
| "Shift"
|
||||
| "Tab"
|
||||
)
|
||||
}
|
||||
|
||||
/// Web sends all all keys as strings, so it is up to us to figure out if it is
|
||||
/// a real text input or the name of a key.
|
||||
pub fn translate_key(key: &str) -> Option<egui::Key> {
|
||||
match key {
|
||||
"ArrowDown" => Some(egui::Key::ArrowDown),
|
||||
"ArrowLeft" => Some(egui::Key::ArrowLeft),
|
||||
"ArrowRight" => Some(egui::Key::ArrowRight),
|
||||
"ArrowUp" => Some(egui::Key::ArrowUp),
|
||||
|
||||
"Esc" | "Escape" => Some(egui::Key::Escape),
|
||||
"Tab" => Some(egui::Key::Tab),
|
||||
"Backspace" => Some(egui::Key::Backspace),
|
||||
"Enter" => Some(egui::Key::Enter),
|
||||
"Space" | " " => Some(egui::Key::Space),
|
||||
|
||||
"Help" | "Insert" => Some(egui::Key::Insert),
|
||||
"Delete" => Some(egui::Key::Delete),
|
||||
"Home" => Some(egui::Key::Home),
|
||||
"End" => Some(egui::Key::End),
|
||||
"PageUp" => Some(egui::Key::PageUp),
|
||||
"PageDown" => Some(egui::Key::PageDown),
|
||||
|
||||
"0" => Some(egui::Key::Num0),
|
||||
"1" => Some(egui::Key::Num1),
|
||||
"2" => Some(egui::Key::Num2),
|
||||
"3" => Some(egui::Key::Num3),
|
||||
"4" => Some(egui::Key::Num4),
|
||||
"5" => Some(egui::Key::Num5),
|
||||
"6" => Some(egui::Key::Num6),
|
||||
"7" => Some(egui::Key::Num7),
|
||||
"8" => Some(egui::Key::Num8),
|
||||
"9" => Some(egui::Key::Num9),
|
||||
|
||||
"a" | "A" => Some(egui::Key::A),
|
||||
"b" | "B" => Some(egui::Key::B),
|
||||
"c" | "C" => Some(egui::Key::C),
|
||||
"d" | "D" => Some(egui::Key::D),
|
||||
"e" | "E" => Some(egui::Key::E),
|
||||
"f" | "F" => Some(egui::Key::F),
|
||||
"g" | "G" => Some(egui::Key::G),
|
||||
"h" | "H" => Some(egui::Key::H),
|
||||
"i" | "I" => Some(egui::Key::I),
|
||||
"j" | "J" => Some(egui::Key::J),
|
||||
"k" | "K" => Some(egui::Key::K),
|
||||
"l" | "L" => Some(egui::Key::L),
|
||||
"m" | "M" => Some(egui::Key::M),
|
||||
"n" | "N" => Some(egui::Key::N),
|
||||
"o" | "O" => Some(egui::Key::O),
|
||||
"p" | "P" => Some(egui::Key::P),
|
||||
"q" | "Q" => Some(egui::Key::Q),
|
||||
"r" | "R" => Some(egui::Key::R),
|
||||
"s" | "S" => Some(egui::Key::S),
|
||||
"t" | "T" => Some(egui::Key::T),
|
||||
"u" | "U" => Some(egui::Key::U),
|
||||
"v" | "V" => Some(egui::Key::V),
|
||||
"w" | "W" => Some(egui::Key::W),
|
||||
"x" | "X" => Some(egui::Key::X),
|
||||
"y" | "Y" => Some(egui::Key::Y),
|
||||
"z" | "Z" => Some(egui::Key::Z),
|
||||
|
||||
"F1" => Some(egui::Key::F1),
|
||||
"F2" => Some(egui::Key::F2),
|
||||
"F3" => Some(egui::Key::F3),
|
||||
"F4" => Some(egui::Key::F4),
|
||||
"F5" => Some(egui::Key::F5),
|
||||
"F6" => Some(egui::Key::F6),
|
||||
"F7" => Some(egui::Key::F7),
|
||||
"F8" => Some(egui::Key::F8),
|
||||
"F9" => Some(egui::Key::F9),
|
||||
"F10" => Some(egui::Key::F10),
|
||||
"F11" => Some(egui::Key::F11),
|
||||
"F12" => Some(egui::Key::F12),
|
||||
"F13" => Some(egui::Key::F13),
|
||||
"F14" => Some(egui::Key::F14),
|
||||
"F15" => Some(egui::Key::F15),
|
||||
"F16" => Some(egui::Key::F16),
|
||||
"F17" => Some(egui::Key::F17),
|
||||
"F18" => Some(egui::Key::F18),
|
||||
"F19" => Some(egui::Key::F19),
|
||||
"F20" => Some(egui::Key::F20),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers {
|
||||
egui::Modifiers {
|
||||
alt: event.alt_key(),
|
||||
ctrl: event.ctrl_key(),
|
||||
shift: event.shift_key(),
|
||||
|
||||
// Ideally we should know if we are running or mac or not,
|
||||
// but this works good enough for now.
|
||||
mac_cmd: event.meta_key(),
|
||||
|
||||
// Ideally we should know if we are running or mac or not,
|
||||
// but this works good enough for now.
|
||||
command: event.ctrl_key() || event.meta_key(),
|
||||
}
|
||||
}
|
||||
285
crates/eframe/src/web/mod.rs
Normal file
285
crates/eframe/src/web/mod.rs
Normal file
@@ -0,0 +1,285 @@
|
||||
//! [`egui`] bindings for web apps (compiling to WASM).
|
||||
|
||||
#![allow(clippy::missing_errors_doc)] // So many `-> Result<_, JsValue>`
|
||||
|
||||
pub mod backend;
|
||||
mod events;
|
||||
mod glow_wrapping;
|
||||
mod input;
|
||||
pub mod screen_reader;
|
||||
pub mod storage;
|
||||
mod text_agent;
|
||||
|
||||
pub use backend::*;
|
||||
pub use events::*;
|
||||
pub use storage::*;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use egui::Vec2;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::EventTarget;
|
||||
|
||||
use input::*;
|
||||
|
||||
use crate::Theme;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Current time in seconds (since undefined point in time).
|
||||
///
|
||||
/// Monotonically increasing.
|
||||
pub fn now_sec() -> f64 {
|
||||
web_sys::window()
|
||||
.expect("should have a Window")
|
||||
.performance()
|
||||
.expect("should have a Performance")
|
||||
.now()
|
||||
/ 1000.0
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn screen_size_in_native_points() -> Option<egui::Vec2> {
|
||||
let window = web_sys::window()?;
|
||||
Some(egui::vec2(
|
||||
window.inner_width().ok()?.as_f64()? as f32,
|
||||
window.inner_height().ok()?.as_f64()? as f32,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn native_pixels_per_point() -> f32 {
|
||||
let pixels_per_point = web_sys::window().unwrap().device_pixel_ratio() as f32;
|
||||
if pixels_per_point > 0.0 && pixels_per_point.is_finite() {
|
||||
pixels_per_point
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn system_theme() -> Option<Theme> {
|
||||
let dark_mode = web_sys::window()?
|
||||
.match_media("(prefers-color-scheme: dark)")
|
||||
.ok()??
|
||||
.matches();
|
||||
Some(if dark_mode { Theme::Dark } else { Theme::Light })
|
||||
}
|
||||
|
||||
pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
|
||||
use wasm_bindgen::JsCast;
|
||||
let document = web_sys::window()?.document()?;
|
||||
let canvas = document.get_element_by_id(canvas_id)?;
|
||||
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
|
||||
}
|
||||
|
||||
pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
|
||||
canvas_element(canvas_id)
|
||||
.unwrap_or_else(|| panic!("Failed to find canvas with id '{}'", canvas_id))
|
||||
}
|
||||
|
||||
fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
|
||||
let rect = canvas_element(canvas_id)
|
||||
.unwrap()
|
||||
.get_bounding_client_rect();
|
||||
egui::pos2(rect.left() as f32, rect.top() as f32)
|
||||
}
|
||||
|
||||
pub fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 {
|
||||
let canvas = canvas_element(canvas_id).unwrap();
|
||||
let pixels_per_point = native_pixels_per_point();
|
||||
egui::vec2(
|
||||
canvas.width() as f32 / pixels_per_point,
|
||||
canvas.height() as f32 / pixels_per_point,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2) -> Option<()> {
|
||||
let canvas = canvas_element(canvas_id)?;
|
||||
let parent = canvas.parent_element()?;
|
||||
|
||||
let width = parent.scroll_width();
|
||||
let height = parent.scroll_height();
|
||||
|
||||
let canvas_real_size = Vec2 {
|
||||
x: width as f32,
|
||||
y: height as f32,
|
||||
};
|
||||
|
||||
let pixels_per_point = native_pixels_per_point();
|
||||
|
||||
let max_size_pixels = pixels_per_point * max_size_points;
|
||||
|
||||
let canvas_size_pixels = pixels_per_point * canvas_real_size;
|
||||
let canvas_size_pixels = canvas_size_pixels.min(max_size_pixels);
|
||||
let canvas_size_points = canvas_size_pixels / pixels_per_point;
|
||||
|
||||
// Make sure that the height and width are always even numbers.
|
||||
// otherwise, the page renders blurry on some platforms.
|
||||
// See https://github.com/emilk/egui/issues/103
|
||||
fn round_to_even(v: f32) -> f32 {
|
||||
(v / 2.0).round() * 2.0
|
||||
}
|
||||
|
||||
canvas
|
||||
.style()
|
||||
.set_property(
|
||||
"width",
|
||||
&format!("{}px", round_to_even(canvas_size_points.x)),
|
||||
)
|
||||
.ok()?;
|
||||
canvas
|
||||
.style()
|
||||
.set_property(
|
||||
"height",
|
||||
&format!("{}px", round_to_even(canvas_size_points.y)),
|
||||
)
|
||||
.ok()?;
|
||||
canvas.set_width(round_to_even(canvas_size_pixels.x) as u32);
|
||||
canvas.set_height(round_to_even(canvas_size_pixels.y) as u32);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> {
|
||||
let document = web_sys::window()?.document()?;
|
||||
document
|
||||
.body()?
|
||||
.style()
|
||||
.set_property("cursor", cursor_web_name(cursor))
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[cfg(web_sys_unstable_apis)]
|
||||
pub fn set_clipboard_text(s: &str) {
|
||||
if let Some(window) = web_sys::window() {
|
||||
if let Some(clipboard) = window.navigator().clipboard() {
|
||||
let promise = clipboard.write_text(s);
|
||||
let future = wasm_bindgen_futures::JsFuture::from(promise);
|
||||
let future = async move {
|
||||
if let Err(err) = future.await {
|
||||
tracing::error!("Copy/cut action denied: {:?}", err);
|
||||
}
|
||||
};
|
||||
wasm_bindgen_futures::spawn_local(future);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_web_name(cursor: egui::CursorIcon) -> &'static str {
|
||||
match cursor {
|
||||
egui::CursorIcon::Alias => "alias",
|
||||
egui::CursorIcon::AllScroll => "all-scroll",
|
||||
egui::CursorIcon::Cell => "cell",
|
||||
egui::CursorIcon::ContextMenu => "context-menu",
|
||||
egui::CursorIcon::Copy => "copy",
|
||||
egui::CursorIcon::Crosshair => "crosshair",
|
||||
egui::CursorIcon::Default => "default",
|
||||
egui::CursorIcon::Grab => "grab",
|
||||
egui::CursorIcon::Grabbing => "grabbing",
|
||||
egui::CursorIcon::Help => "help",
|
||||
egui::CursorIcon::Move => "move",
|
||||
egui::CursorIcon::NoDrop => "no-drop",
|
||||
egui::CursorIcon::None => "none",
|
||||
egui::CursorIcon::NotAllowed => "not-allowed",
|
||||
egui::CursorIcon::PointingHand => "pointer",
|
||||
egui::CursorIcon::Progress => "progress",
|
||||
egui::CursorIcon::ResizeHorizontal => "ew-resize",
|
||||
egui::CursorIcon::ResizeNeSw => "nesw-resize",
|
||||
egui::CursorIcon::ResizeNwSe => "nwse-resize",
|
||||
egui::CursorIcon::ResizeVertical => "ns-resize",
|
||||
|
||||
egui::CursorIcon::ResizeEast => "e-resize",
|
||||
egui::CursorIcon::ResizeSouthEast => "se-resize",
|
||||
egui::CursorIcon::ResizeSouth => "s-resize",
|
||||
egui::CursorIcon::ResizeSouthWest => "sw-resize",
|
||||
egui::CursorIcon::ResizeWest => "w-resize",
|
||||
egui::CursorIcon::ResizeNorthWest => "nw-resize",
|
||||
egui::CursorIcon::ResizeNorth => "n-resize",
|
||||
egui::CursorIcon::ResizeNorthEast => "ne-resize",
|
||||
egui::CursorIcon::ResizeColumn => "col-resize",
|
||||
egui::CursorIcon::ResizeRow => "row-resize",
|
||||
|
||||
egui::CursorIcon::Text => "text",
|
||||
egui::CursorIcon::VerticalText => "vertical-text",
|
||||
egui::CursorIcon::Wait => "wait",
|
||||
egui::CursorIcon::ZoomIn => "zoom-in",
|
||||
egui::CursorIcon::ZoomOut => "zoom-out",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_url(url: &str, new_tab: bool) -> Option<()> {
|
||||
let name = if new_tab { "_blank" } else { "_self" };
|
||||
|
||||
web_sys::window()?
|
||||
.open_with_url_and_target(url, name)
|
||||
.ok()?;
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// e.g. "#fragment" part of "www.example.com/index.html#fragment",
|
||||
///
|
||||
/// Percent decoded
|
||||
pub fn location_hash() -> String {
|
||||
percent_decode(
|
||||
&web_sys::window()
|
||||
.unwrap()
|
||||
.location()
|
||||
.hash()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn percent_decode(s: &str) -> String {
|
||||
percent_encoding::percent_decode_str(s)
|
||||
.decode_utf8_lossy()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub(crate) fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool {
|
||||
// See https://github.com/emilk/egui/issues/794
|
||||
|
||||
// detect WebKitGTK
|
||||
|
||||
// WebKitGTK use WebKit default unmasked vendor and renderer
|
||||
// but safari use same vendor and renderer
|
||||
// so exclude "Mac OS X" user-agent.
|
||||
let user_agent = web_sys::window().unwrap().navigator().user_agent().unwrap();
|
||||
!user_agent.contains("Mac OS X") && is_safari_and_webkit_gtk(gl)
|
||||
}
|
||||
|
||||
/// detecting Safari and `webkitGTK`.
|
||||
///
|
||||
/// Safari and `webkitGTK` use unmasked renderer :Apple GPU
|
||||
///
|
||||
/// If we detect safari or `webkitGTKs` returns true.
|
||||
///
|
||||
/// This function used to avoid displaying linear color with `sRGB` supported systems.
|
||||
fn is_safari_and_webkit_gtk(gl: &web_sys::WebGlRenderingContext) -> bool {
|
||||
// This call produces a warning in Firefox ("WEBGL_debug_renderer_info is deprecated in Firefox and will be removed.")
|
||||
// but unless we call it we get errors in Chrome when we call `get_parameter` below.
|
||||
// TODO(emilk): do something smart based on user agent?
|
||||
if gl
|
||||
.get_extension("WEBGL_debug_renderer_info")
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
if let Ok(renderer) =
|
||||
gl.get_parameter(web_sys::WebglDebugRendererInfo::UNMASKED_RENDERER_WEBGL)
|
||||
{
|
||||
if let Some(renderer) = renderer.as_string() {
|
||||
if renderer.contains("Apple") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
49
crates/eframe/src/web/screen_reader.rs
Normal file
49
crates/eframe/src/web/screen_reader.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
pub struct ScreenReader {
|
||||
#[cfg(feature = "tts")]
|
||||
tts: Option<tts::Tts>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tts"))]
|
||||
#[allow(clippy::derivable_impls)] // False positive
|
||||
impl Default for ScreenReader {
|
||||
fn default() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tts")]
|
||||
impl Default for ScreenReader {
|
||||
fn default() -> Self {
|
||||
let tts = match tts::Tts::default() {
|
||||
Ok(screen_reader) => {
|
||||
tracing::debug!("Initialized screen reader.");
|
||||
Some(screen_reader)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to load screen reader: {}", err);
|
||||
None
|
||||
}
|
||||
};
|
||||
Self { tts }
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenReader {
|
||||
#[cfg(not(feature = "tts"))]
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn speak(&mut self, _text: &str) {}
|
||||
|
||||
#[cfg(feature = "tts")]
|
||||
pub fn speak(&mut self, text: &str) {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(tts) = &mut self.tts {
|
||||
tracing::debug!("Speaking: {:?}", text);
|
||||
let interrupt = true;
|
||||
if let Err(err) = tts.speak(text, interrupt) {
|
||||
tracing::warn!("Failed to read: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
crates/eframe/src/web/storage.rs
Normal file
43
crates/eframe/src/web/storage.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
fn local_storage() -> Option<web_sys::Storage> {
|
||||
web_sys::window()?.local_storage().ok()?
|
||||
}
|
||||
|
||||
pub fn local_storage_get(key: &str) -> Option<String> {
|
||||
local_storage().map(|storage| storage.get_item(key).ok())??
|
||||
}
|
||||
|
||||
pub fn local_storage_set(key: &str, value: &str) {
|
||||
local_storage().map(|storage| storage.set_item(key, value));
|
||||
}
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
pub fn load_memory(ctx: &egui::Context) {
|
||||
if let Some(memory_string) = local_storage_get("egui_memory_ron") {
|
||||
match ron::from_str(&memory_string) {
|
||||
Ok(memory) => {
|
||||
*ctx.memory() = memory;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to parse memory RON: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "persistence"))]
|
||||
pub fn load_memory(_: &egui::Context) {}
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
pub fn save_memory(ctx: &egui::Context) {
|
||||
match ron::to_string(&*ctx.memory()) {
|
||||
Ok(ron) => {
|
||||
local_storage_set("egui_memory_ron", &ron);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to serialize memory as RON: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "persistence"))]
|
||||
pub fn save_memory(_: &egui::Context) {}
|
||||
228
crates/eframe/src/web/text_agent.rs
Normal file
228
crates/eframe/src/web/text_agent.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
//! The text agent is an `<input>` element used to trigger
|
||||
//! mobile keyboard and IME input.
|
||||
|
||||
use super::{canvas_element, AppRunner, AppRunnerContainer};
|
||||
use egui::mutex::MutexGuard;
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
static AGENT_ID: &str = "egui_text_agent";
|
||||
|
||||
pub fn text_agent() -> web_sys::HtmlInputElement {
|
||||
use wasm_bindgen::JsCast;
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.document()
|
||||
.unwrap()
|
||||
.get_element_by_id(AGENT_ID)
|
||||
.unwrap()
|
||||
.dyn_into()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Text event handler,
|
||||
pub fn install_text_agent(runner_container: &mut AppRunnerContainer) -> Result<(), JsValue> {
|
||||
use wasm_bindgen::JsCast;
|
||||
let window = web_sys::window().unwrap();
|
||||
let document = window.document().unwrap();
|
||||
let body = document.body().expect("document should have a body");
|
||||
let input = document
|
||||
.create_element("input")?
|
||||
.dyn_into::<web_sys::HtmlInputElement>()?;
|
||||
let input = std::rc::Rc::new(input);
|
||||
input.set_id(AGENT_ID);
|
||||
let is_composing = Rc::new(Cell::new(false));
|
||||
{
|
||||
let style = input.style();
|
||||
// Transparent
|
||||
style.set_property("opacity", "0").unwrap();
|
||||
// Hide under canvas
|
||||
style.set_property("z-index", "-1").unwrap();
|
||||
}
|
||||
// Set size as small as possible, in case user may click on it.
|
||||
input.set_size(1);
|
||||
input.set_autofocus(true);
|
||||
input.set_hidden(true);
|
||||
|
||||
// When IME is off
|
||||
runner_container.add_event_listener(&input, "input", {
|
||||
let input_clone = input.clone();
|
||||
let is_composing = is_composing.clone();
|
||||
|
||||
move |_event: web_sys::InputEvent, mut runner_lock| {
|
||||
let text = input_clone.value();
|
||||
if !text.is_empty() && !is_composing.get() {
|
||||
input_clone.set_value("");
|
||||
runner_lock.input.raw.events.push(egui::Event::Text(text));
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
{
|
||||
// When IME is on, handle composition event
|
||||
runner_container.add_event_listener(&input, "compositionstart", {
|
||||
let input_clone = input.clone();
|
||||
let is_composing = is_composing.clone();
|
||||
|
||||
move |_event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
|
||||
is_composing.set(true);
|
||||
input_clone.set_value("");
|
||||
|
||||
runner_lock
|
||||
.input
|
||||
.raw
|
||||
.events
|
||||
.push(egui::Event::CompositionStart);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
})?;
|
||||
|
||||
runner_container.add_event_listener(
|
||||
&input,
|
||||
"compositionupdate",
|
||||
move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
|
||||
if let Some(event) = event.data().map(egui::Event::CompositionUpdate) {
|
||||
runner_lock.input.raw.events.push(event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
runner_container.add_event_listener(&input, "compositionend", {
|
||||
let input_clone = input.clone();
|
||||
|
||||
move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
|
||||
is_composing.set(false);
|
||||
input_clone.set_value("");
|
||||
|
||||
if let Some(event) = event.data().map(egui::Event::CompositionEnd) {
|
||||
runner_lock.input.raw.events.push(event);
|
||||
runner_lock.needs_repaint.repaint_asap();
|
||||
}
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
// When input lost focus, focus on it again.
|
||||
// It is useful when user click somewhere outside canvas.
|
||||
runner_container.add_event_listener(
|
||||
&input,
|
||||
"focusout",
|
||||
move |_event: web_sys::MouseEvent, _| {
|
||||
// Delay 10 ms, and focus again.
|
||||
let func = js_sys::Function::new_no_args(&format!(
|
||||
"document.getElementById('{}').focus()",
|
||||
AGENT_ID
|
||||
));
|
||||
window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
|
||||
.unwrap();
|
||||
},
|
||||
)?;
|
||||
|
||||
body.append_child(&input)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Focus or blur text agent to toggle mobile keyboard.
|
||||
pub fn update_text_agent(runner: MutexGuard<'_, AppRunner>) -> Option<()> {
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::HtmlInputElement;
|
||||
let window = web_sys::window()?;
|
||||
let document = window.document()?;
|
||||
let input: HtmlInputElement = document.get_element_by_id(AGENT_ID)?.dyn_into().unwrap();
|
||||
let canvas_style = canvas_element(runner.canvas_id())?.style();
|
||||
|
||||
if runner.mutable_text_under_cursor {
|
||||
let is_already_editing = input.hidden();
|
||||
if is_already_editing {
|
||||
input.set_hidden(false);
|
||||
input.focus().ok()?;
|
||||
|
||||
// Move up canvas so that text edit is shown at ~30% of screen height.
|
||||
// Only on touch screens, when keyboard popups.
|
||||
if let Some(latest_touch_pos) = runner.input.latest_touch_pos {
|
||||
let window_height = window.inner_height().ok()?.as_f64()? as f32;
|
||||
let current_rel = latest_touch_pos.y / window_height;
|
||||
|
||||
// estimated amount of screen covered by keyboard
|
||||
let keyboard_fraction = 0.5;
|
||||
|
||||
if current_rel > keyboard_fraction {
|
||||
// below the keyboard
|
||||
|
||||
let target_rel = 0.3;
|
||||
|
||||
// Note: `delta` is negative, since we are moving the canvas UP
|
||||
let delta = target_rel - current_rel;
|
||||
|
||||
let delta = delta.max(-keyboard_fraction); // Don't move it crazy much
|
||||
|
||||
let new_pos_percent = format!("{}%", (delta * 100.0).round());
|
||||
|
||||
canvas_style.set_property("position", "absolute").ok()?;
|
||||
canvas_style.set_property("top", &new_pos_percent).ok()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Drop runner lock
|
||||
drop(runner);
|
||||
|
||||
// Holding the runner lock while calling input.blur() causes a panic.
|
||||
// This is most probably caused by the browser running the event handler
|
||||
// for the triggered blur event synchronously, meaning that the mutex
|
||||
// lock does not get dropped by the time another event handler is called.
|
||||
//
|
||||
// Why this didn't exist before #1290 is a mystery to me, but it exists now
|
||||
// and this apparently is the fix for it
|
||||
//
|
||||
// ¯\_(ツ)_/¯ - @DusterTheFirst
|
||||
input.blur().ok()?;
|
||||
|
||||
input.set_hidden(true);
|
||||
canvas_style.set_property("position", "absolute").ok()?;
|
||||
canvas_style.set_property("top", "0%").ok()?; // move back to normal position
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// If context is running under mobile device?
|
||||
fn is_mobile() -> Option<bool> {
|
||||
const MOBILE_DEVICE: [&str; 6] = ["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"];
|
||||
|
||||
let user_agent = web_sys::window()?.navigator().user_agent().ok()?;
|
||||
let is_mobile = MOBILE_DEVICE.iter().any(|&name| user_agent.contains(name));
|
||||
Some(is_mobile)
|
||||
}
|
||||
|
||||
// Move text agent to text cursor's position, on desktop/laptop,
|
||||
// candidate window moves following text element (agent),
|
||||
// so it appears that the IME candidate window moves with text cursor.
|
||||
// On mobile devices, there is no need to do that.
|
||||
pub fn move_text_cursor(cursor: Option<egui::Pos2>, canvas_id: &str) -> Option<()> {
|
||||
let style = text_agent().style();
|
||||
// Note: movint agent on mobile devices will lead to unpredictable scroll.
|
||||
if is_mobile() == Some(false) {
|
||||
cursor.as_ref().and_then(|&egui::Pos2 { x, y }| {
|
||||
let canvas = canvas_element(canvas_id)?;
|
||||
let bounding_rect = text_agent().get_bounding_client_rect();
|
||||
let y = (y + (canvas.scroll_top() + canvas.offset_top()) as f32)
|
||||
.min(canvas.client_height() as f32 - bounding_rect.height() as f32);
|
||||
let x = x + (canvas.scroll_left() + canvas.offset_left()) as f32;
|
||||
// Canvas is translated 50% horizontally in html.
|
||||
let x = (x - canvas.offset_width() as f32 / 2.0)
|
||||
.min(canvas.client_width() as f32 - bounding_rect.width() as f32);
|
||||
style.set_property("position", "absolute").ok()?;
|
||||
style.set_property("top", &format!("{}px", y)).ok()?;
|
||||
style.set_property("left", &format!("{}px", x)).ok()
|
||||
})
|
||||
} else {
|
||||
style.set_property("position", "absolute").ok()?;
|
||||
style.set_property("top", "0px").ok()?;
|
||||
style.set_property("left", "0px").ok()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user