Merge branch 'master' into web_handle

This commit is contained in:
Emil Ernerfeldt 2022-08-20 09:59:18 +02:00
commit d3e26cd00a
59 changed files with 1727 additions and 1094 deletions

View file

@ -58,8 +58,8 @@ jobs:
with: with:
profile: minimal profile: minimal
toolchain: 1.61.0 toolchain: 1.61.0
target: wasm32-unknown-unknown
override: true override: true
- run: rustup target add wasm32-unknown-unknown
- uses: actions-rs/cargo@v1 - uses: actions-rs/cargo@v1
with: with:
command: check command: check
@ -87,8 +87,8 @@ jobs:
with: with:
profile: minimal profile: minimal
toolchain: 1.61.0 toolchain: 1.61.0
target: wasm32-unknown-unknown
override: true override: true
- run: rustup target add wasm32-unknown-unknown
- name: check - name: check
run: cargo check -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown run: cargo check -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
@ -101,8 +101,8 @@ jobs:
with: with:
profile: minimal profile: minimal
toolchain: 1.61.0 toolchain: 1.61.0
target: wasm32-unknown-unknown
override: true override: true
- run: rustup target add wasm32-unknown-unknown
- uses: actions-rs/cargo@v1 - uses: actions-rs/cargo@v1
with: with:
command: check command: check
@ -136,7 +136,7 @@ jobs:
profile: minimal profile: minimal
toolchain: 1.61.0 toolchain: 1.61.0
override: true override: true
- run: rustup component add rustfmt components: rustfmt
- uses: actions-rs/cargo@v1 - uses: actions-rs/cargo@v1
with: with:
command: fmt command: fmt
@ -191,7 +191,22 @@ jobs:
with: with:
profile: minimal profile: minimal
toolchain: 1.61.0 toolchain: 1.61.0
target: wasm32-unknown-unknown
override: true override: true
- run: rustup target add wasm32-unknown-unknown
- run: ./sh/setup_web.sh - run: ./sh/setup_web.sh
- run: ./sh/wasm_bindgen_check.sh - run: ./sh/wasm_bindgen_check.sh
android:
name: android
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.61.0
target: aarch64-linux-android
override: true
- run: cargo check --features wgpu --target aarch64-linux-android
working-directory: eframe

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
/.*.json /.*.json
/.vscode /.vscode
/media/* /media/*
.DS_Store

View file

@ -18,6 +18,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w
* Added `Contex::request_repaint_after` ([#1694](https://github.com/emilk/egui/pull/1694)). * Added `Contex::request_repaint_after` ([#1694](https://github.com/emilk/egui/pull/1694)).
* `ctrl-h` now acts like backspace in `TextEdit` ([#1812](https://github.com/emilk/egui/pull/1812)). * `ctrl-h` now acts like backspace in `TextEdit` ([#1812](https://github.com/emilk/egui/pull/1812)).
* Added `RawInput::has_focus` which backends can set to indicate whether the UI as a whole has the keyboard focus ([#1859](https://github.com/emilk/egui/pull/1859)). * Added `RawInput::has_focus` which backends can set to indicate whether the UI as a whole has the keyboard focus ([#1859](https://github.com/emilk/egui/pull/1859)).
* Added `PointerState::button_double_clicked()` and `PointerState::button_triple_clicked()` ([#1906](https://github.com/emilk/egui/issues/1906)).
### Changed ### Changed
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)). * MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).

1185
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -12,19 +12,7 @@ members = [
"emath", "emath",
"epaint", "epaint",
"examples/confirm_exit", "examples/*",
"examples/custom_3d_glow",
"examples/custom_3d_three-d",
"examples/custom_font",
"examples/custom_font_style",
"examples/custom_window_frame",
"examples/download_image",
"examples/file_dialog",
"examples/hello_world",
"examples/puffin_profiler",
"examples/retained_image",
"examples/screenshot",
"examples/svg",
] ]
[profile.dev] [profile.dev]

View file

@ -58,6 +58,7 @@ allow = [
"MIT", # https://tldrlegal.com/license/mit-license "MIT", # https://tldrlegal.com/license/mit-license
"MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11 "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11
"OpenSSL", # https://www.openssl.org/source/license.html "OpenSSL", # https://www.openssl.org/source/license.html
"Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html
"Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib)
] ]

View file

@ -74,7 +74,7 @@ document-features = { version = "0.2", optional = true }
egui_glow = { version = "0.18.0", path = "../egui_glow", optional = true, default-features = false } egui_glow = { version = "0.18.0", path = "../egui_glow", optional = true, default-features = false }
egui-wgpu = { version = "0.18.0", path = "../egui-wgpu", optional = true, features = ["winit"] } egui-wgpu = { version = "0.18.0", path = "../egui-wgpu", optional = true, features = ["winit"] }
glow = { version = "0.11", optional = true } glow = { version = "0.11", optional = true }
ron = { version = "0.7", optional = true } ron = { version = "0.8", optional = true, features = ["integer128"] }
serde = { version = "1", optional = true, features = ["derive"] } serde = { version = "1", optional = true, features = ["derive"] }
wgpu = { version = "0.13", optional = true } wgpu = { version = "0.13", optional = true }
@ -83,8 +83,8 @@ wgpu = { version = "0.13", optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
dark-light = { version = "0.2.1", optional = true } dark-light = { version = "0.2.1", optional = true }
egui-winit = { version = "0.18.0", path = "../egui-winit", default-features = false, features = ["clipboard", "links"] } egui-winit = { version = "0.18.0", path = "../egui-winit", default-features = false, features = ["clipboard", "links"] }
glutin = { version = "0.28.0" } glutin = { version = "0.29.0" }
winit = "0.26.1" winit = "0.27.2"
# optional native: # optional native:
puffin = { version = "0.13", optional = true } puffin = { version = "0.13", optional = true }
@ -94,6 +94,7 @@ directories-next = { version = "2", optional = true }
# web: # web:
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
bytemuck = "1.7" bytemuck = "1.7"
getrandom = { version = "0.2", features = ["js"] } # used by ahash
js-sys = "0.3" js-sys = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
wasm-bindgen = "0.2" wasm-bindgen = "0.2"

View file

@ -30,13 +30,18 @@ pub struct CreationContext<'s> {
/// The [`glow::Context`] allows you to initialize OpenGL resources (e.g. shaders) that /// The [`glow::Context`] allows you to initialize OpenGL resources (e.g. shaders) that
/// you might want to use later from a [`egui::PaintCallback`]. /// 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")] #[cfg(feature = "glow")]
pub gl: Option<std::sync::Arc<glow::Context>>, pub gl: Option<std::sync::Arc<glow::Context>>,
/// Can be used to manage GPU resources for custom rendering with WGPU using /// The underlying WGPU render state.
/// [`egui::PaintCallback`]s. ///
/// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`].
///
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
pub render_state: Option<egui_wgpu::RenderState>, pub wgpu_render_state: Option<egui_wgpu::RenderState>,
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -281,6 +286,20 @@ pub struct NativeOptions {
/// ///
/// Default: `Theme::Dark`. /// Default: `Theme::Dark`.
pub default_theme: Theme, 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"))] #[cfg(not(target_arch = "wasm32"))]
@ -307,6 +326,7 @@ impl Default for NativeOptions {
renderer: Renderer::default(), renderer: Renderer::default(),
follow_system_theme: cfg!(target_os = "macos") || cfg!(target_os = "windows"), follow_system_theme: cfg!(target_os = "macos") || cfg!(target_os = "windows"),
default_theme: Theme::Dark, default_theme: Theme::Dark,
run_and_return: true,
} }
} }
} }
@ -507,10 +527,9 @@ pub struct Frame {
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
pub(crate) gl: Option<std::sync::Arc<glow::Context>>, pub(crate) gl: Option<std::sync::Arc<glow::Context>>,
/// Can be used to manage GPU resources for custom rendering with WGPU using /// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
/// [`egui::PaintCallback`]s.
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
pub render_state: Option<egui_wgpu::RenderState>, pub(crate) wgpu_render_state: Option<egui_wgpu::RenderState>,
} }
impl Frame { impl Frame {
@ -548,12 +567,22 @@ impl Frame {
/// ([`egui`] only collects [`egui::Shape`]s and then eframe paints them all in one go later on). /// ([`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, /// To get a [`glow`] context you need to compile with the `glow` feature flag,
/// and run eframe with the glow backend. /// and run eframe using [`Renderer::Glow`].
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
pub fn gl(&self) -> Option<&std::sync::Arc<glow::Context>> { pub fn gl(&self) -> Option<&std::sync::Arc<glow::Context>> {
self.gl.as_ref() 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). /// 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. /// The framework will not quit immediately, but at the end of the this frame.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -763,7 +792,10 @@ pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &st
/// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key. /// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key.
#[cfg(feature = "ron")] #[cfg(feature = "ron")]
pub fn set_value<T: serde::Serialize>(storage: &mut dyn Storage, key: &str, value: &T) { pub fn set_value<T: serde::Serialize>(storage: &mut dyn Storage, key: &str, value: &T) {
storage.set_string(key, ron::ser::to_string(value).unwrap()); 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 /// [`Storage`] key used for app

View file

@ -161,20 +161,20 @@ mod native;
/// ``` /// ```
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: AppCreator) -> ! { pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: AppCreator) {
let renderer = native_options.renderer; let renderer = native_options.renderer;
match renderer { match renderer {
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
Renderer::Glow => { Renderer::Glow => {
tracing::debug!("Using the glow renderer"); tracing::debug!("Using the glow renderer");
native::run::run_glow(app_name, &native_options, app_creator) native::run::run_glow(app_name, &native_options, app_creator);
} }
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
Renderer::Wgpu => { Renderer::Wgpu => {
tracing::debug!("Using the wgpu renderer"); tracing::debug!("Using the wgpu renderer");
native::run::run_wgpu(app_name, &native_options, app_creator) native::run::run_wgpu(app_name, &native_options, app_creator);
} }
} }
} }

View file

@ -196,7 +196,7 @@ impl EpiIntegration {
system_theme: Option<Theme>, system_theme: Option<Theme>,
storage: Option<Box<dyn epi::Storage>>, storage: Option<Box<dyn epi::Storage>>,
#[cfg(feature = "glow")] gl: Option<std::sync::Arc<glow::Context>>, #[cfg(feature = "glow")] gl: Option<std::sync::Arc<glow::Context>>,
#[cfg(feature = "wgpu")] render_state: Option<egui_wgpu::RenderState>, #[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
) -> Self { ) -> Self {
let egui_ctx = egui::Context::default(); let egui_ctx = egui::Context::default();
@ -214,7 +214,7 @@ impl EpiIntegration {
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
gl, gl,
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
render_state, wgpu_render_state,
}; };
let mut egui_winit = egui_winit::State::new(event_loop); let mut egui_winit = egui_winit::State::new(event_loop);

View file

@ -1,7 +1,16 @@
use super::epi_integration; //! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`].
use crate::epi; //! When making changes to one you often also want to apply it to the other.
use egui_winit::winit;
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; struct RequestRepaintEvent;
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
@ -9,7 +18,7 @@ struct RequestRepaintEvent;
fn create_display( fn create_display(
native_options: &NativeOptions, native_options: &NativeOptions,
window_builder: winit::window::WindowBuilder, window_builder: winit::window::WindowBuilder,
event_loop: &winit::event_loop::EventLoop<RequestRepaintEvent>, event_loop: &EventLoop<RequestRepaintEvent>,
) -> ( ) -> (
glutin::WindowedContext<glutin::PossiblyCurrent>, glutin::WindowedContext<glutin::PossiblyCurrent>,
glow::Context, glow::Context,
@ -47,28 +56,210 @@ fn create_display(
pub use epi::NativeOptions; 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 /// Run an egui app
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
pub fn run_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, app_name: &str,
native_options: &epi::NativeOptions, native_options: &epi::NativeOptions,
app_creator: epi::AppCreator, app_creator: epi::AppCreator,
) -> ! { ) -> Self {
let storage = epi_integration::create_storage(app_name); let storage = epi_integration::create_storage(app_name);
let window_settings = epi_integration::load_window_settings(storage.as_deref()); let window_settings = epi_integration::load_window_settings(storage.as_deref());
let event_loop = winit::event_loop::EventLoop::with_user_event();
let window_builder = let window_builder = epi_integration::window_builder(native_options, &window_settings)
epi_integration::window_builder(native_options, &window_settings).with_title(app_name); .with_title(app_name);
let (gl_window, gl) = create_display(native_options, window_builder, &event_loop); let (gl_window, gl) = create_display(native_options, window_builder, event_loop);
let gl = std::sync::Arc::new(gl); let gl = Arc::new(gl);
let mut painter = egui_glow::Painter::new(gl.clone(), None, "") let painter = egui_glow::Painter::new(gl.clone(), None, "")
.unwrap_or_else(|error| panic!("some OpenGL error occurred {}\n", error)); .unwrap_or_else(|error| panic!("some OpenGL error occurred {}\n", error));
let system_theme = native_options.system_theme(); let system_theme = native_options.system_theme();
let mut integration = epi_integration::EpiIntegration::new( let mut integration = epi_integration::EpiIntegration::new(
&event_loop, event_loop,
painter.max_texture_side(), painter.max_texture_side(),
gl_window.window(), gl_window.window(),
system_theme, system_theme,
@ -93,27 +284,63 @@ pub fn run_glow(
storage: integration.frame.storage(), storage: integration.frame.storage(),
gl: Some(gl.clone()), gl: Some(gl.clone()),
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
render_state: None, wgpu_render_state: None,
}); });
if app.warm_up_enabled() { if app.warm_up_enabled() {
integration.warm_up(app.as_mut(), gl_window.window()); integration.warm_up(app.as_mut(), gl_window.window());
} }
let mut is_focused = true; Self {
gl_window,
gl,
painter,
integration,
app,
is_focused: true,
}
}
}
event_loop.run(move |event, _, control_flow| { impl WinitApp for GlowWinitApp {
let window = gl_window.window(); fn is_focused(&self) -> bool {
self.is_focused
}
let mut redraw = || { 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")] #[cfg(feature = "puffin")]
puffin::GlobalProfiler::lock().new_frame(); puffin::GlobalProfiler::lock().new_frame();
crate::profile_scope!("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(); let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
egui_glow::painter::clear( egui_glow::painter::clear(
&gl, gl,
screen_size_in_pixels, screen_size_in_pixels,
app.clear_color(&integration.egui_ctx.style().visuals), app.clear_color(&integration.egui_ctx.style().visuals),
); );
@ -146,11 +373,10 @@ pub fn run_glow(
gl_window.swap_buffers().unwrap(); gl_window.swap_buffers().unwrap();
} }
*control_flow = if integration.should_quit() { let control_flow = if integration.should_quit() {
winit::event_loop::ControlFlow::Exit EventResult::Exit
} else if repaint_after.is_zero() { } else if repaint_after.is_zero() {
window.request_redraw(); EventResult::RepaintAsap
winit::event_loop::ControlFlow::Poll
} else if let Some(repaint_after_instant) = } else if let Some(repaint_after_instant) =
std::time::Instant::now().checked_add(repaint_after) std::time::Instant::now().checked_add(repaint_after)
{ {
@ -159,14 +385,14 @@ pub fn run_glow(
// technically, this might lead to some weird corner cases where the user *WANTS* // 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 // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own
// egui backend impl i guess. // egui backend impl i guess.
winit::event_loop::ControlFlow::WaitUntil(repaint_after_instant) EventResult::RepaintAt(repaint_after_instant)
} else { } else {
winit::event_loop::ControlFlow::Wait EventResult::Wait
}; };
integration.maybe_autosave(app.as_mut(), window); integration.maybe_autosave(app.as_mut(), window);
if !is_focused { if !self.is_focused {
// On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325 // 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 // 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. // But we know if we are focused (in foreground). When minimized, we are not focused.
@ -175,78 +401,106 @@ pub fn run_glow(
crate::profile_scope!("bg_sleep"); crate::profile_scope!("bg_sleep");
std::thread::sleep(std::time::Duration::from_millis(10)); std::thread::sleep(std::time::Duration::from_millis(10));
} }
};
match event {
// 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) => redraw(),
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => redraw(),
control_flow
}
fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult {
match event {
winit::event::Event::WindowEvent { event, .. } => { winit::event::Event::WindowEvent { event, .. } => {
match &event { match &event {
winit::event::WindowEvent::Focused(new_focused) => { winit::event::WindowEvent::Focused(new_focused) => {
is_focused = *new_focused; self.is_focused = *new_focused;
} }
winit::event::WindowEvent::Resized(physical_size) => { winit::event::WindowEvent::Resized(physical_size) => {
// Resize with 0 width and height is used by winit to signal a minimize event on Windows. // 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 // See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows. // This solves an issue where the app would panic when minimizing on Windows.
if physical_size.width > 0 && physical_size.height > 0 { if physical_size.width > 0 && physical_size.height > 0 {
gl_window.resize(*physical_size); self.gl_window.resize(*physical_size);
} }
} }
winit::event::WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { winit::event::WindowEvent::ScaleFactorChanged {
gl_window.resize(**new_inner_size); new_inner_size, ..
} => {
self.gl_window.resize(**new_inner_size);
} }
winit::event::WindowEvent::CloseRequested if integration.should_quit() => { winit::event::WindowEvent::CloseRequested
*control_flow = winit::event_loop::ControlFlow::Exit; if self.integration.should_quit() =>
{
return EventResult::Exit
} }
_ => {} _ => {}
} }
integration.on_event(app.as_mut(), &event); self.integration.on_event(self.app.as_mut(), &event);
if integration.should_quit() {
*control_flow = winit::event_loop::ControlFlow::Exit; if self.integration.should_quit() {
EventResult::Exit
} else {
// TODO(emilk): ask egui if the event warrants a repaint
EventResult::RepaintAsap
} }
window.request_redraw(); // TODO(emilk): ask egui if the events warrants a repaint instead
} }
winit::event::Event::LoopDestroyed => { _ => EventResult::Wait,
integration.save(&mut *app, window);
app.on_exit(Some(&gl));
painter.destroy();
} }
winit::event::Event::UserEvent(RequestRepaintEvent) => window.request_redraw(),
winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached {
..
}) => {
window.request_redraw();
} }
_ => {}
}
});
} }
// TODO(emilk): merge with with the clone above pub fn run_glow(
/// Run an egui app
#[cfg(feature = "wgpu")]
pub fn run_wgpu(
app_name: &str, app_name: &str,
native_options: &epi::NativeOptions, native_options: &epi::NativeOptions,
app_creator: epi::AppCreator, 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 storage = epi_integration::create_storage(app_name);
let window_settings = epi_integration::load_window_settings(storage.as_deref()); let window_settings = epi_integration::load_window_settings(storage.as_deref());
let event_loop = winit::event_loop::EventLoop::with_user_event();
let window = epi_integration::window_builder(native_options, &window_settings) let window = epi_integration::window_builder(native_options, &window_settings)
.with_title(app_name) .with_title(app_name)
.build(&event_loop) .build(event_loop)
.unwrap(); .unwrap();
// SAFETY: `window` must outlive `painter`. // SAFETY: `window` must outlive `painter`.
#[allow(unsafe_code)] #[allow(unsafe_code, unused_mut, unused_unsafe)]
let mut painter = unsafe { let painter = unsafe {
let mut painter = egui_wgpu::winit::Painter::new( let mut painter = egui_wgpu::winit::Painter::new(
wgpu::Backends::PRIMARY | wgpu::Backends::GL, wgpu::Backends::PRIMARY | wgpu::Backends::GL,
wgpu::PowerPreference::HighPerformance, wgpu::PowerPreference::HighPerformance,
@ -263,18 +517,18 @@ pub fn run_wgpu(
painter painter
}; };
let render_state = painter.get_render_state().expect("Uninitialized"); let wgpu_render_state = painter.render_state();
let system_theme = native_options.system_theme(); let system_theme = native_options.system_theme();
let mut integration = epi_integration::EpiIntegration::new( let mut integration = epi_integration::EpiIntegration::new(
&event_loop, event_loop,
painter.max_texture_side().unwrap_or(2048), painter.max_texture_side().unwrap_or(2048),
&window, &window,
system_theme, system_theme,
storage, storage,
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
None, None,
Some(render_state.clone()), wgpu_render_state.clone(),
); );
let theme = system_theme.unwrap_or(native_options.default_theme); let theme = system_theme.unwrap_or(native_options.default_theme);
integration.egui_ctx.set_visuals(theme.egui_visuals()); integration.egui_ctx.set_visuals(theme.egui_visuals());
@ -292,23 +546,61 @@ pub fn run_wgpu(
storage: integration.frame.storage(), storage: integration.frame.storage(),
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
gl: None, gl: None,
render_state: Some(render_state), wgpu_render_state,
}); });
if app.warm_up_enabled() { if app.warm_up_enabled() {
integration.warm_up(app.as_mut(), &window); integration.warm_up(app.as_mut(), &window);
} }
let mut is_focused = true; Self {
window,
painter,
integration,
app,
is_focused: true,
}
}
}
event_loop.run(move |event, _, control_flow| { impl WinitApp for WgpuWinitApp {
let window = &window; fn is_focused(&self) -> bool {
self.is_focused
}
let mut redraw = || { 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")] #[cfg(feature = "puffin")]
puffin::GlobalProfiler::lock().new_frame(); puffin::GlobalProfiler::lock().new_frame();
crate::profile_scope!("frame"); crate::profile_scope!("frame");
let Self {
window,
app,
integration,
painter,
..
} = self;
let egui::FullOutput { let egui::FullOutput {
platform_output, platform_output,
repaint_after, repaint_after,
@ -330,11 +622,12 @@ pub fn run_wgpu(
&textures_delta, &textures_delta,
); );
*control_flow = if integration.should_quit() { integration.post_rendering(app.as_mut(), window);
winit::event_loop::ControlFlow::Exit
let control_flow = if integration.should_quit() {
EventResult::Exit
} else if repaint_after.is_zero() { } else if repaint_after.is_zero() {
window.request_redraw(); EventResult::RepaintAsap
winit::event_loop::ControlFlow::Poll
} else if let Some(repaint_after_instant) = } else if let Some(repaint_after_instant) =
std::time::Instant::now().checked_add(repaint_after) std::time::Instant::now().checked_add(repaint_after)
{ {
@ -343,14 +636,14 @@ pub fn run_wgpu(
// technically, this might lead to some weird corner cases where the user *WANTS* // 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 // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own
// egui backend impl i guess. // egui backend impl i guess.
winit::event_loop::ControlFlow::WaitUntil(repaint_after_instant) EventResult::RepaintAt(repaint_after_instant)
} else { } else {
winit::event_loop::ControlFlow::Wait EventResult::Wait
}; };
integration.maybe_autosave(app.as_mut(), window); integration.maybe_autosave(app.as_mut(), window);
if !is_focused { if !self.is_focused {
// On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325 // 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 // 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. // But we know if we are focused (in foreground). When minimized, we are not focused.
@ -359,70 +652,84 @@ pub fn run_wgpu(
crate::profile_scope!("bg_sleep"); crate::profile_scope!("bg_sleep");
std::thread::sleep(std::time::Duration::from_millis(10)); std::thread::sleep(std::time::Duration::from_millis(10));
} }
};
control_flow
}
fn on_event(&mut self, event: winit::event::Event<'_, RequestRepaintEvent>) -> EventResult {
match event { match event {
// 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) => redraw(),
winit::event::Event::RedrawRequested(_) if !cfg!(windows) => redraw(),
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
winit::event::Event::Resumed => unsafe { winit::event::Event::Resumed => unsafe {
painter.set_window(Some(&window)); self.painter.set_window(Some(&self.window));
EventResult::RepaintAsap
}, },
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
winit::event::Event::Paused => unsafe { winit::event::Event::Suspended => unsafe {
painter.set_window(None); self.painter.set_window(None);
EventResult::Wait
}, },
winit::event::Event::WindowEvent { event, .. } => { winit::event::Event::WindowEvent { event, .. } => {
match &event { match &event {
winit::event::WindowEvent::Focused(new_focused) => { winit::event::WindowEvent::Focused(new_focused) => {
is_focused = *new_focused; self.is_focused = *new_focused;
} }
winit::event::WindowEvent::Resized(physical_size) => { winit::event::WindowEvent::Resized(physical_size) => {
// Resize with 0 width and height is used by winit to signal a minimize event on Windows. // 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 // See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows. // This solves an issue where the app would panic when minimizing on Windows.
if physical_size.width > 0 && physical_size.height > 0 { if physical_size.width > 0 && physical_size.height > 0 {
painter.on_window_resized(physical_size.width, physical_size.height); self.painter
.on_window_resized(physical_size.width, physical_size.height);
} }
} }
winit::event::WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { winit::event::WindowEvent::ScaleFactorChanged {
painter.on_window_resized(new_inner_size.width, new_inner_size.height); new_inner_size, ..
} => {
self.painter
.on_window_resized(new_inner_size.width, new_inner_size.height);
} }
winit::event::WindowEvent::CloseRequested if integration.should_quit() => { winit::event::WindowEvent::CloseRequested
*control_flow = winit::event_loop::ControlFlow::Exit; if self.integration.should_quit() =>
{
return EventResult::Exit
} }
_ => {} _ => {}
}; };
integration.on_event(app.as_mut(), &event); self.integration.on_event(self.app.as_mut(), &event);
if integration.should_quit() { if self.integration.should_quit() {
*control_flow = winit::event_loop::ControlFlow::Exit; EventResult::Exit
} else {
// TODO(emilk): ask egui if the event warrants a repaint
EventResult::RepaintAsap
}
}
_ => EventResult::Wait,
}
} }
window.request_redraw(); // TODO(emilk): ask egui if the events warrants a repaint instead
} }
winit::event::Event::LoopDestroyed => {
integration.save(&mut *app, window);
#[cfg(feature = "glow")] pub fn run_wgpu(
app.on_exit(None); app_name: &str,
native_options: &epi::NativeOptions,
#[cfg(not(feature = "glow"))] app_creator: epi::AppCreator,
app.on_exit(); ) {
if native_options.run_and_return {
painter.destroy(); with_event_loop(|event_loop| {
} let wgpu_eframe =
winit::event::Event::UserEvent(RequestRepaintEvent) => window.request_redraw(), WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached { run_and_return(event_loop, wgpu_eframe);
..
}) => {
window.request_redraw();
}
_ => (),
}
}); });
} 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;

View file

@ -219,7 +219,7 @@ impl AppRunner {
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
gl: Some(painter.painter.gl().clone()), gl: Some(painter.painter.gl().clone()),
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
render_state: None, wgpu_render_state: None,
}); });
let frame = epi::Frame { let frame = epi::Frame {
@ -229,7 +229,7 @@ impl AppRunner {
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
gl: Some(painter.gl().clone()), gl: Some(painter.gl().clone()),
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
render_state: None, wgpu_render_state: None,
}; };
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default(); let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();

View file

@ -47,4 +47,4 @@ wgpu = "0.13"
document-features = { version = "0.2", optional = true } document-features = { version = "0.2", optional = true }
pollster = { version = "0.2", optional = true } pollster = { version = "0.2", optional = true }
winit = { version = "0.26", optional = true } winit = { version = "0.27.2", optional = true }

View file

@ -569,7 +569,7 @@ impl RenderPass {
/// This could be used by custom paint hooks to render images that have been added through with /// This could be used by custom paint hooks to render images that have been added through with
/// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html) /// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html)
/// or [`egui::Context::load_texture`]. /// or [`egui::Context::load_texture`].
pub fn get_texture( pub fn texture(
&self, &self,
id: &egui::TextureId, id: &egui::TextureId,
) -> Option<&(Option<wgpu::Texture>, wgpu::BindGroup)> { ) -> Option<&(Option<wgpu::Texture>, wgpu::BindGroup)> {

View file

@ -2,7 +2,7 @@ use std::sync::Arc;
use egui::mutex::RwLock; use egui::mutex::RwLock;
use tracing::error; use tracing::error;
use wgpu::{Adapter, Instance, Surface, TextureFormat}; use wgpu::{Adapter, Instance, Surface};
use crate::renderer; use crate::renderer;
@ -12,7 +12,7 @@ use crate::renderer;
pub struct RenderState { pub struct RenderState {
pub device: Arc<wgpu::Device>, pub device: Arc<wgpu::Device>,
pub queue: Arc<wgpu::Queue>, pub queue: Arc<wgpu::Queue>,
pub target_format: TextureFormat, pub target_format: wgpu::TextureFormat,
pub egui_rpass: Arc<RwLock<renderer::RenderPass>>, pub egui_rpass: Arc<RwLock<renderer::RenderPass>>,
} }
@ -75,14 +75,14 @@ impl<'a> Painter<'a> {
/// Get the [`RenderState`]. /// Get the [`RenderState`].
/// ///
/// Will return [`None`] if the render state has not been initialized yet. /// Will return [`None`] if the render state has not been initialized yet.
pub fn get_render_state(&self) -> Option<RenderState> { pub fn render_state(&self) -> Option<RenderState> {
self.render_state.as_ref().cloned() self.render_state.as_ref().cloned()
} }
async fn init_render_state( async fn init_render_state(
&self, &self,
adapter: &Adapter, adapter: &Adapter,
target_format: TextureFormat, target_format: wgpu::TextureFormat,
) -> RenderState { ) -> RenderState {
let (device, queue) = let (device, queue) =
pollster::block_on(adapter.request_device(&self.device_descriptor, None)).unwrap(); pollster::block_on(adapter.request_device(&self.device_descriptor, None)).unwrap();

View file

@ -46,10 +46,9 @@ egui = { version = "0.18.0", path = "../egui", default-features = false, feature
] } ] }
instant = { version = "0.1", features = ["wasm-bindgen"] } # We use instant so we can (maybe) compile for web instant = { version = "0.1", features = ["wasm-bindgen"] } # We use instant so we can (maybe) compile for web
tracing = { version = "0.1", default-features = false, features = ["std"] } tracing = { version = "0.1", default-features = false, features = ["std"] }
winit = "0.26.1" winit = "0.27.2"
#! ### Optional dependencies #! ### Optional dependencies
arboard = { version = "2.1", optional = true, default-features = false }
## Enable this when generating docs. ## Enable this when generating docs.
document-features = { version = "0.2", optional = true } document-features = { version = "0.2", optional = true }
@ -64,3 +63,6 @@ webbrowser = { version = "0.7", optional = true }
[target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies] [target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies]
smithay-clipboard = { version = "0.6.3", optional = true } smithay-clipboard = { version = "0.6.3", optional = true }
[target.'cfg(not(target_os = "android"))'.dependencies]
arboard = { version = "2.1", optional = true, default-features = false }

View file

@ -5,7 +5,7 @@ use std::os::raw::c_void;
/// If the "clipboard" feature is off, or we cannot connect to the OS clipboard, /// If the "clipboard" feature is off, or we cannot connect to the OS clipboard,
/// then a fallback clipboard that just works works within the same app is used instead. /// then a fallback clipboard that just works works within the same app is used instead.
pub struct Clipboard { pub struct Clipboard {
#[cfg(feature = "arboard")] #[cfg(all(feature = "arboard", not(target_os = "android")))]
arboard: Option<arboard::Clipboard>, arboard: Option<arboard::Clipboard>,
#[cfg(all( #[cfg(all(
@ -28,7 +28,7 @@ impl Clipboard {
#[allow(unused_variables)] #[allow(unused_variables)]
pub fn new(#[allow(unused_variables)] wayland_display: Option<*mut c_void>) -> Self { pub fn new(#[allow(unused_variables)] wayland_display: Option<*mut c_void>) -> Self {
Self { Self {
#[cfg(feature = "arboard")] #[cfg(all(feature = "arboard", not(target_os = "android")))]
arboard: init_arboard(), arboard: init_arboard(),
#[cfg(all( #[cfg(all(
any( any(
@ -66,7 +66,7 @@ impl Clipboard {
}; };
} }
#[cfg(feature = "arboard")] #[cfg(all(feature = "arboard", not(target_os = "android")))]
if let Some(clipboard) = &mut self.arboard { if let Some(clipboard) = &mut self.arboard {
return match clipboard.get_text() { return match clipboard.get_text() {
Ok(text) => Some(text), Ok(text) => Some(text),
@ -96,7 +96,7 @@ impl Clipboard {
return; return;
} }
#[cfg(feature = "arboard")] #[cfg(all(feature = "arboard", not(target_os = "android")))]
if let Some(clipboard) = &mut self.arboard { if let Some(clipboard) = &mut self.arboard {
if let Err(err) = clipboard.set_text(text) { if let Err(err) = clipboard.set_text(text) {
tracing::error!("Copy/Cut error: {}", err); tracing::error!("Copy/Cut error: {}", err);
@ -108,7 +108,7 @@ impl Clipboard {
} }
} }
#[cfg(feature = "arboard")] #[cfg(all(feature = "arboard", not(target_os = "android")))]
fn init_arboard() -> Option<arboard::Clipboard> { fn init_arboard() -> Option<arboard::Clipboard> {
match arboard::Clipboard::new() { match arboard::Clipboard::new() {
Ok(clipboard) => Some(clipboard), Ok(clipboard) => Some(clipboard),

View file

@ -66,7 +66,7 @@ pub struct State {
impl State { impl State {
pub fn new<T>(event_loop: &EventLoopWindowTarget<T>) -> Self { pub fn new<T>(event_loop: &EventLoopWindowTarget<T>) -> Self {
Self::new_with_wayland_display(get_wayland_display(event_loop)) Self::new_with_wayland_display(wayland_display(event_loop))
} }
pub fn new_with_wayland_display(wayland_display: Option<*mut c_void>) -> Self { pub fn new_with_wayland_display(wayland_display: Option<*mut c_void>) -> Self {
@ -400,7 +400,7 @@ impl State {
} }
fn on_mouse_wheel(&mut self, delta: winit::event::MouseScrollDelta) { fn on_mouse_wheel(&mut self, delta: winit::event::MouseScrollDelta) {
let mut delta = match delta { let delta = match delta {
winit::event::MouseScrollDelta::LineDelta(x, y) => { winit::event::MouseScrollDelta::LineDelta(x, y) => {
let points_per_scroll_line = 50.0; // Scroll speed decided by consensus: https://github.com/emilk/egui/issues/461 let points_per_scroll_line = 50.0; // Scroll speed decided by consensus: https://github.com/emilk/egui/issues/461
egui::vec2(x, y) * points_per_scroll_line egui::vec2(x, y) * points_per_scroll_line
@ -410,8 +410,6 @@ impl State {
} }
}; };
delta.x *= -1.0; // Winit has inverted hscroll. Remove this line when we update winit after https://github.com/rust-windowing/winit/pull/2105 is merged and released
if self.egui_input.modifiers.ctrl || self.egui_input.modifiers.command { if self.egui_input.modifiers.ctrl || self.egui_input.modifiers.command {
// Treat as zoom instead: // Treat as zoom instead:
let factor = (delta.y / 200.0).exp(); let factor = (delta.y / 200.0).exp();
@ -712,7 +710,7 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option<winit::window::Curs
} }
/// Returns a Wayland display handle if the target is running Wayland /// Returns a Wayland display handle if the target is running Wayland
fn get_wayland_display<T>(_event_loop: &EventLoopWindowTarget<T>) -> Option<*mut c_void> { fn wayland_display<T>(_event_loop: &EventLoopWindowTarget<T>) -> Option<*mut c_void> {
#[cfg(any( #[cfg(any(
target_os = "linux", target_os = "linux",
target_os = "dragonfly", target_os = "dragonfly",

View file

@ -4,7 +4,7 @@
pub struct WindowSettings { pub struct WindowSettings {
/// Position of window in physical pixels. This is either /// Position of window in physical pixels. This is either
/// the inner or outer position depending on the platform. /// the inner or outer position depending on the platform.
/// See [`winit::window::WindowAttributes`] for details. /// See [`winit::window::WindowBuilder::with_position`] for details.
position: Option<egui::Pos2>, position: Option<egui::Pos2>,
fullscreen: bool, fullscreen: bool,

View file

@ -57,14 +57,14 @@ serde = ["dep:serde", "epaint/serde"]
[dependencies] [dependencies]
epaint = { version = "0.18.1", path = "../epaint", default-features = false } epaint = { version = "0.18.1", path = "../epaint", default-features = false }
ahash = "0.7" ahash = "0.8"
nohash-hasher = "0.2" nohash-hasher = "0.2"
#! ### Optional dependencies #! ### Optional dependencies
## Enable this when generating docs. ## Enable this when generating docs.
document-features = { version = "0.2", optional = true } document-features = { version = "0.2", optional = true }
ron = { version = "0.7", optional = true } ron = { version = "0.8", optional = true }
serde = { version = "1", optional = true, features = ["derive", "rc"] } serde = { version = "1", optional = true, features = ["derive", "rc"] }
# egui doesn't log much, but when it does, it uses [`tracing`](https://docs.rs/tracing). # egui doesn't log much, but when it does, it uses [`tracing`](https://docs.rs/tracing).

View file

@ -46,7 +46,7 @@ impl Id {
/// Generate a new [`Id`] by hashing some source (e.g. a string or integer). /// Generate a new [`Id`] by hashing some source (e.g. a string or integer).
pub fn new(source: impl std::hash::Hash) -> Id { pub fn new(source: impl std::hash::Hash) -> Id {
use std::hash::Hasher; use std::hash::Hasher;
let mut hasher = epaint::ahash::AHasher::new_with_keys(123, 456); let mut hasher = epaint::ahash::AHasher::default();
source.hash(&mut hasher); source.hash(&mut hasher);
Id(hasher.finish()) Id(hasher.finish())
} }
@ -54,7 +54,7 @@ impl Id {
/// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument. /// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument.
pub fn with(self, child: impl std::hash::Hash) -> Id { pub fn with(self, child: impl std::hash::Hash) -> Id {
use std::hash::Hasher; use std::hash::Hasher;
let mut hasher = epaint::ahash::AHasher::new_with_keys(123, 456); let mut hasher = epaint::ahash::AHasher::default();
hasher.write_u64(self.0); hasher.write_u64(self.0);
child.hash(&mut hasher); child.hash(&mut hasher);
Id(hasher.finish()) Id(hasher.finish())

View file

@ -750,6 +750,20 @@ impl PointerState {
.any(|event| matches!(event, &PointerEvent::Pressed { button: b, .. } if button == b)) .any(|event| matches!(event, &PointerEvent::Pressed { button: b, .. } if button == b))
} }
/// Was the button given double clicked this frame?
pub fn button_double_clicked(&self, button: PointerButton) -> bool {
self.pointer_events
.iter()
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_double()))
}
/// Was the button given triple clicked this frame?
pub fn button_triple_clicked(&self, button: PointerButton) -> bool {
self.pointer_events
.iter()
.any(|event| matches!(&event, PointerEvent::Released(Some(click)) if click.button == button && click.is_triple()))
}
/// Was the primary button clicked this frame? /// Was the primary button clicked this frame?
pub fn primary_clicked(&self) -> bool { pub fn primary_clicked(&self) -> bool {
self.button_clicked(PointerButton::Primary) self.button_clicked(PointerButton::Primary)

View file

@ -252,7 +252,13 @@ impl Layout {
Self { main_wrap, ..self } Self { main_wrap, ..self }
} }
/// The aligmnet to use on the cross axis. /// The alignment to use on the main axis.
#[inline(always)]
pub fn with_main_align(self, main_align: Align) -> Self {
Self { main_align, ..self }
}
/// The alignment to use on the cross axis.
/// ///
/// The "cross" axis is the one orthogonal to the main axis. /// The "cross" axis is the one orthogonal to the main axis.
/// For instance: in left-to-right layout, the main axis is horizontal and the cross axis is vertical. /// For instance: in left-to-right layout, the main axis is horizontal and the cross axis is vertical.

View file

@ -1,5 +1,3 @@
use epaint::ahash::AHashSet;
use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style}; use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -490,15 +488,15 @@ pub struct Areas {
areas: IdMap<area::State>, areas: IdMap<area::State>,
/// Back-to-front. Top is last. /// Back-to-front. Top is last.
order: Vec<LayerId>, order: Vec<LayerId>,
visible_last_frame: AHashSet<LayerId>, visible_last_frame: ahash::HashSet<LayerId>,
visible_current_frame: AHashSet<LayerId>, visible_current_frame: ahash::HashSet<LayerId>,
/// When an area want to be on top, it is put in here. /// When an area want to be on top, it is put in here.
/// At the end of the frame, this is used to reorder the layers. /// At the end of the frame, this is used to reorder the layers.
/// This means if several layers want to be on top, they will keep their relative order. /// This means if several layers want to be on top, they will keep their relative order.
/// So if you close three windows and then reopen them all in one frame, /// So if you close three windows and then reopen them all in one frame,
/// they will all be sent to the top, but keep their previous internal order. /// they will all be sent to the top, but keep their previous internal order.
wants_to_be_on_top: AHashSet<LayerId>, wants_to_be_on_top: ahash::HashSet<LayerId>,
} }
impl Areas { impl Areas {
@ -550,7 +548,7 @@ impl Areas {
self.visible_last_frame.contains(layer_id) || self.visible_current_frame.contains(layer_id) self.visible_last_frame.contains(layer_id) || self.visible_current_frame.contains(layer_id)
} }
pub fn visible_layer_ids(&self) -> AHashSet<LayerId> { pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
self.visible_last_frame self.visible_last_frame
.iter() .iter()
.cloned() .cloned()

View file

@ -532,7 +532,7 @@ impl MenuState {
id: Id, id: Id,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> { ) -> Option<R> {
let (sub_response, response) = self.get_submenu(id).map(|sub| { let (sub_response, response) = self.submenu(id).map(|sub| {
let inner_response = Self::show(ctx, sub, id, add_contents); let inner_response = Self::show(ctx, sub, id, add_contents);
(sub.read().response, inner_response.inner) (sub.read().response, inner_response.inner)
})?; })?;
@ -582,7 +582,7 @@ impl MenuState {
if pointer.is_still() { if pointer.is_still() {
return false; return false;
} }
if let Some(sub_menu) = self.get_current_submenu() { if let Some(sub_menu) = self.current_submenu() {
if let Some(pos) = pointer.hover_pos() { if let Some(pos) = pointer.hover_pos() {
return Self::points_at_left_of_rect(pos, pointer.velocity(), sub_menu.read().rect); return Self::points_at_left_of_rect(pos, pointer.velocity(), sub_menu.read().rect);
} }
@ -592,7 +592,7 @@ impl MenuState {
/// Check if pointer is hovering current submenu. /// Check if pointer is hovering current submenu.
fn hovering_current_submenu(&self, pointer: &PointerState) -> bool { fn hovering_current_submenu(&self, pointer: &PointerState) -> bool {
if let Some(sub_menu) = self.get_current_submenu() { if let Some(sub_menu) = self.current_submenu() {
if let Some(pos) = pointer.hover_pos() { if let Some(pos) = pointer.hover_pos() {
return sub_menu.read().area_contains(pos); return sub_menu.read().area_contains(pos);
} }
@ -608,18 +608,18 @@ impl MenuState {
} }
fn is_open(&self, id: Id) -> bool { fn is_open(&self, id: Id) -> bool {
self.get_sub_id() == Some(id) self.sub_id() == Some(id)
} }
fn get_sub_id(&self) -> Option<Id> { fn sub_id(&self) -> Option<Id> {
self.sub_menu.as_ref().map(|(id, _)| *id) self.sub_menu.as_ref().map(|(id, _)| *id)
} }
fn get_current_submenu(&self) -> Option<&Arc<RwLock<MenuState>>> { fn current_submenu(&self) -> Option<&Arc<RwLock<MenuState>>> {
self.sub_menu.as_ref().map(|(_, sub)| sub) self.sub_menu.as_ref().map(|(_, sub)| sub)
} }
fn get_submenu(&mut self, id: Id) -> Option<&Arc<RwLock<MenuState>>> { fn submenu(&mut self, id: Id) -> Option<&Arc<RwLock<MenuState>>> {
self.sub_menu self.sub_menu
.as_ref() .as_ref()
.and_then(|(k, sub)| if id == *k { Some(sub) } else { None }) .and_then(|(k, sub)| if id == *k { Some(sub) } else { None })

View file

@ -99,7 +99,7 @@ impl Ui {
crate::egui_assert!(!max_rect.any_nan()); crate::egui_assert!(!max_rect.any_nan());
let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value(); let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value();
self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1); self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1);
let menu_state = self.get_menu_state(); let menu_state = self.menu_state();
Ui { Ui {
id: self.id.with(id_source), id: self.id.with(id_source),
next_auto_id_source, next_auto_id_source,
@ -2096,7 +2096,7 @@ impl Ui {
self.menu_state = None; self.menu_state = None;
} }
pub(crate) fn get_menu_state(&self) -> Option<Arc<RwLock<MenuState>>> { pub(crate) fn menu_state(&self) -> Option<Arc<RwLock<MenuState>>> {
self.menu_state.clone() self.menu_state.clone()
} }

View file

@ -119,7 +119,7 @@ impl<Value: 'static + Send + Sync, Computer: 'static + Send + Sync> CacheTrait
/// ``` /// ```
#[derive(Default)] #[derive(Default)]
pub struct CacheStorage { pub struct CacheStorage {
caches: ahash::AHashMap<std::any::TypeId, Box<dyn CacheTrait>>, caches: ahash::HashMap<std::any::TypeId, Box<dyn CacheTrait>>,
} }
impl CacheStorage { impl CacheStorage {

View file

@ -32,7 +32,7 @@ pub(super) struct PlotConfig<'a> {
/// Trait shared by things that can be drawn in the plot. /// Trait shared by things that can be drawn in the plot.
pub(super) trait PlotItem { pub(super) trait PlotItem {
fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>); fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>);
fn initialize(&mut self, x_range: RangeInclusive<f64>); fn initialize(&mut self, x_range: RangeInclusive<f64>);
@ -46,7 +46,7 @@ pub(super) trait PlotItem {
fn geometry(&self) -> PlotGeometry<'_>; fn geometry(&self) -> PlotGeometry<'_>;
fn get_bounds(&self) -> PlotBounds; fn bounds(&self) -> PlotBounds;
fn find_closest(&self, point: Pos2, transform: &ScreenTransform) -> Option<ClosestElem> { fn find_closest(&self, point: Pos2, transform: &ScreenTransform) -> Option<ClosestElem> {
match self.geometry() { match self.geometry() {
@ -167,7 +167,7 @@ impl HLine {
} }
impl PlotItem for HLine { impl PlotItem for HLine {
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) { fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
let HLine { let HLine {
y, y,
stroke, stroke,
@ -204,7 +204,7 @@ impl PlotItem for HLine {
PlotGeometry::None PlotGeometry::None
} }
fn get_bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
let mut bounds = PlotBounds::NOTHING; let mut bounds = PlotBounds::NOTHING;
bounds.min[1] = self.y; bounds.min[1] = self.y;
bounds.max[1] = self.y; bounds.max[1] = self.y;
@ -277,7 +277,7 @@ impl VLine {
} }
impl PlotItem for VLine { impl PlotItem for VLine {
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) { fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
let VLine { let VLine {
x, x,
stroke, stroke,
@ -314,7 +314,7 @@ impl PlotItem for VLine {
PlotGeometry::None PlotGeometry::None
} }
fn get_bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
let mut bounds = PlotBounds::NOTHING; let mut bounds = PlotBounds::NOTHING;
bounds.min[0] = self.x; bounds.min[0] = self.x;
bounds.max[0] = self.x; bounds.max[0] = self.x;
@ -401,7 +401,7 @@ fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option<f32> {
} }
impl PlotItem for Line { impl PlotItem for Line {
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) { fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
let Self { let Self {
series, series,
stroke, stroke,
@ -484,8 +484,8 @@ impl PlotItem for Line {
PlotGeometry::Points(self.series.points()) PlotGeometry::Points(self.series.points())
} }
fn get_bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
self.series.get_bounds() self.series.bounds()
} }
} }
@ -562,7 +562,7 @@ impl Polygon {
} }
impl PlotItem for Polygon { impl PlotItem for Polygon {
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) { fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
let Self { let Self {
series, series,
stroke, stroke,
@ -614,8 +614,8 @@ impl PlotItem for Polygon {
PlotGeometry::Points(self.series.points()) PlotGeometry::Points(self.series.points())
} }
fn get_bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
self.series.get_bounds() self.series.bounds()
} }
} }
@ -674,7 +674,7 @@ impl Text {
} }
impl PlotItem for Text { impl PlotItem for Text {
fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) { fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
let color = if self.color == Color32::TRANSPARENT { let color = if self.color == Color32::TRANSPARENT {
ui.style().visuals.text_color() ui.style().visuals.text_color()
} else { } else {
@ -728,7 +728,7 @@ impl PlotItem for Text {
PlotGeometry::None PlotGeometry::None
} }
fn get_bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
let mut bounds = PlotBounds::NOTHING; let mut bounds = PlotBounds::NOTHING;
bounds.extend_with(&self.position); bounds.extend_with(&self.position);
bounds bounds
@ -814,7 +814,7 @@ impl Points {
} }
impl PlotItem for Points { impl PlotItem for Points {
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) { fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
let sqrt_3 = 3_f32.sqrt(); let sqrt_3 = 3_f32.sqrt();
let frac_sqrt_3_2 = 3_f32.sqrt() / 2.0; let frac_sqrt_3_2 = 3_f32.sqrt() / 2.0;
let frac_1_sqrt_2 = 1.0 / 2_f32.sqrt(); let frac_1_sqrt_2 = 1.0 / 2_f32.sqrt();
@ -965,8 +965,8 @@ impl PlotItem for Points {
PlotGeometry::Points(self.series.points()) PlotGeometry::Points(self.series.points())
} }
fn get_bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
self.series.get_bounds() self.series.bounds()
} }
} }
@ -1016,7 +1016,7 @@ impl Arrows {
} }
impl PlotItem for Arrows { impl PlotItem for Arrows {
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) { fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
use crate::emath::*; use crate::emath::*;
let Self { let Self {
origins, origins,
@ -1080,8 +1080,8 @@ impl PlotItem for Arrows {
PlotGeometry::Points(self.origins.points()) PlotGeometry::Points(self.origins.points())
} }
fn get_bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
self.origins.get_bounds() self.origins.bounds()
} }
} }
@ -1155,7 +1155,7 @@ impl PlotImage {
} }
impl PlotItem for PlotImage { impl PlotItem for PlotImage {
fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) { fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
let Self { let Self {
position, position,
texture_id, texture_id,
@ -1215,7 +1215,7 @@ impl PlotItem for PlotImage {
PlotGeometry::None PlotGeometry::None
} }
fn get_bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
let mut bounds = PlotBounds::NOTHING; let mut bounds = PlotBounds::NOTHING;
let left_top = PlotPoint::new( let left_top = PlotPoint::new(
self.position.x as f32 - self.size.x / 2.0, self.position.x as f32 - self.size.x / 2.0,
@ -1346,7 +1346,7 @@ impl BarChart {
} }
impl PlotItem for BarChart { impl PlotItem for BarChart {
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) { fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
for b in &self.bars { for b in &self.bars {
b.add_shapes(transform, self.highlight, shapes); b.add_shapes(transform, self.highlight, shapes);
} }
@ -1376,7 +1376,7 @@ impl PlotItem for BarChart {
PlotGeometry::Rects PlotGeometry::Rects
} }
fn get_bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
let mut bounds = PlotBounds::NOTHING; let mut bounds = PlotBounds::NOTHING;
for b in &self.bars { for b in &self.bars {
bounds.merge(&b.bounds()); bounds.merge(&b.bounds());
@ -1488,7 +1488,7 @@ impl BoxPlot {
} }
impl PlotItem for BoxPlot { impl PlotItem for BoxPlot {
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) { fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
for b in &self.boxes { for b in &self.boxes {
b.add_shapes(transform, self.highlight, shapes); b.add_shapes(transform, self.highlight, shapes);
} }
@ -1518,7 +1518,7 @@ impl PlotItem for BoxPlot {
PlotGeometry::Rects PlotGeometry::Rects
} }
fn get_bounds(&self) -> PlotBounds { fn bounds(&self) -> PlotBounds {
let mut bounds = PlotBounds::NOTHING; let mut bounds = PlotBounds::NOTHING;
for b in &self.boxes { for b in &self.boxes {
bounds.merge(&b.bounds()); bounds.merge(&b.bounds());

View file

@ -302,7 +302,7 @@ impl PlotPoints {
(start < end).then(|| start..=end) (start < end).then(|| start..=end)
} }
pub(super) fn get_bounds(&self) -> PlotBounds { pub(super) fn bounds(&self) -> PlotBounds {
match self { match self {
PlotPoints::Owned(points) => { PlotPoints::Owned(points) => {
let mut bounds = PlotBounds::NOTHING; let mut bounds = PlotBounds::NOTHING;

View file

@ -1,7 +1,5 @@
use std::{collections::BTreeMap, string::String}; use std::{collections::BTreeMap, string::String};
use epaint::ahash::AHashSet;
use crate::*; use crate::*;
use super::items::PlotItem; use super::items::PlotItem;
@ -168,7 +166,7 @@ impl LegendWidget {
rect: Rect, rect: Rect,
config: Legend, config: Legend,
items: &[Box<dyn PlotItem>], items: &[Box<dyn PlotItem>],
hidden_items: &AHashSet<String>, hidden_items: &ahash::HashSet<String>,
) -> Option<Self> { ) -> Option<Self> {
// Collect the legend entries. If multiple items have the same name, they share a // Collect the legend entries. If multiple items have the same name, they share a
// checkbox. If their colors don't match, we pick a neutral color for the checkbox. // checkbox. If their colors don't match, we pick a neutral color for the checkbox.
@ -199,7 +197,7 @@ impl LegendWidget {
} }
// Get the names of the hidden items. // Get the names of the hidden items.
pub fn get_hidden_items(&self) -> AHashSet<String> { pub fn hidden_items(&self) -> ahash::HashSet<String> {
self.entries self.entries
.iter() .iter()
.filter(|(_, entry)| !entry.checked) .filter(|(_, entry)| !entry.checked)
@ -208,7 +206,7 @@ impl LegendWidget {
} }
// Get the name of the hovered items. // Get the name of the hovered items.
pub fn get_hovered_entry_name(&self) -> Option<String> { pub fn hovered_entry_name(&self) -> Option<String> {
self.entries self.entries
.iter() .iter()
.find(|(_, entry)| entry.hovered) .find(|(_, entry)| entry.hovered)

View file

@ -3,7 +3,6 @@
use std::{cell::Cell, ops::RangeInclusive, rc::Rc}; use std::{cell::Cell, ops::RangeInclusive, rc::Rc};
use crate::*; use crate::*;
use epaint::ahash::AHashSet;
use epaint::color::Hsva; use epaint::color::Hsva;
use epaint::util::FloatOrd; use epaint::util::FloatOrd;
@ -96,7 +95,7 @@ impl From<bool> for AutoBounds {
struct PlotMemory { struct PlotMemory {
auto_bounds: AutoBounds, auto_bounds: AutoBounds,
hovered_entry: Option<String>, hovered_entry: Option<String>,
hidden_items: AHashSet<String>, hidden_items: ahash::HashSet<String>,
min_auto_bounds: PlotBounds, min_auto_bounds: PlotBounds,
last_screen_transform: ScreenTransform, last_screen_transform: ScreenTransform,
/// Allows to remember the first click position when performing a boxed zoom /// Allows to remember the first click position when performing a boxed zoom
@ -430,7 +429,7 @@ impl Plot {
/// ///
/// The function has this signature: /// The function has this signature:
/// ```ignore /// ```ignore
/// fn get_step_sizes(input: GridInput) -> Vec<GridMark>; /// fn step_sizes(input: GridInput) -> Vec<GridMark>;
/// ``` /// ```
/// ///
/// This function should return all marks along the visible range of the X axis. /// This function should return all marks along the visible range of the X axis.
@ -697,7 +696,7 @@ impl Plot {
} }
for item in &items { for item in &items {
let item_bounds = item.get_bounds(); let item_bounds = item.bounds();
if auto_bounds.x { if auto_bounds.x {
bounds.merge_x(&item_bounds); bounds.merge_x(&item_bounds);
@ -829,8 +828,8 @@ impl Plot {
if let Some(mut legend) = legend { if let Some(mut legend) = legend {
ui.add(&mut legend); ui.add(&mut legend);
hidden_items = legend.get_hidden_items(); hidden_items = legend.hidden_items();
hovered_entry = legend.get_hovered_entry_name(); hovered_entry = legend.hovered_entry_name();
} }
if let Some(group) = linked_axes.as_ref() { if let Some(group) = linked_axes.as_ref() {
@ -1073,7 +1072,7 @@ pub struct GridMark {
/// 10 is a typical value, others are possible though. /// 10 is a typical value, others are possible though.
pub fn log_grid_spacer(log_base: i64) -> GridSpacer { pub fn log_grid_spacer(log_base: i64) -> GridSpacer {
let log_base = log_base as f64; let log_base = log_base as f64;
let get_step_sizes = move |input: GridInput| -> Vec<GridMark> { let step_sizes = move |input: GridInput| -> Vec<GridMark> {
// The distance between two of the thinnest grid lines is "rounded" up // The distance between two of the thinnest grid lines is "rounded" up
// to the next-bigger power of base // to the next-bigger power of base
let smallest_visible_unit = next_power(input.base_step_size, log_base); let smallest_visible_unit = next_power(input.base_step_size, log_base);
@ -1087,7 +1086,7 @@ pub fn log_grid_spacer(log_base: i64) -> GridSpacer {
generate_marks(step_sizes, input.bounds) generate_marks(step_sizes, input.bounds)
}; };
Box::new(get_step_sizes) Box::new(step_sizes)
} }
/// Splits the grid into uniform-sized spacings (e.g. 100, 25, 1). /// Splits the grid into uniform-sized spacings (e.g. 100, 25, 1).
@ -1136,7 +1135,7 @@ impl PreparedPlot {
let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default()); let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default());
plot_ui.set_clip_rect(*transform.frame()); plot_ui.set_clip_rect(*transform.frame());
for item in &self.items { for item in &self.items {
item.get_shapes(&mut plot_ui, transform, &mut shapes); item.shapes(&mut plot_ui, transform, &mut shapes);
} }
if let Some(pointer) = response.hover_pos() { if let Some(pointer) = response.hover_pos() {

View file

@ -16,9 +16,9 @@ impl Custom3d {
pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Self { pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Self {
// Get the WGPU render state from the eframe creation context. This can also be retrieved // Get the WGPU render state from the eframe creation context. This can also be retrieved
// from `eframe::Frame` when you don't have a `CreationContext` available. // from `eframe::Frame` when you don't have a `CreationContext` available.
let render_state = cc.render_state.as_ref().expect("WGPU enabled"); let wgpu_render_state = cc.wgpu_render_state.as_ref().expect("WGPU enabled");
let device = &render_state.device; let device = &wgpu_render_state.device;
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None, label: None,
@ -56,7 +56,7 @@ impl Custom3d {
fragment: Some(wgpu::FragmentState { fragment: Some(wgpu::FragmentState {
module: &shader, module: &shader,
entry_point: "fs_main", entry_point: "fs_main",
targets: &[Some(render_state.target_format.into())], targets: &[Some(wgpu_render_state.target_format.into())],
}), }),
primitive: wgpu::PrimitiveState::default(), primitive: wgpu::PrimitiveState::default(),
depth_stencil: None, depth_stencil: None,
@ -84,7 +84,7 @@ impl Custom3d {
// Because the graphics pipeline must have the same lifetime as the egui render pass, // Because the graphics pipeline must have the same lifetime as the egui render pass,
// instead of storing the pipeline in our `Custom3D` struct, we insert it into the // instead of storing the pipeline in our `Custom3D` struct, we insert it into the
// `paint_callback_resources` type map, which is stored alongside the render pass. // `paint_callback_resources` type map, which is stored alongside the render pass.
render_state wgpu_render_state
.egui_rpass .egui_rpass
.write() .write()
.paint_callback_resources .paint_callback_resources

View file

@ -389,19 +389,19 @@ impl CustomAxisDemo {
const MINS_PER_DAY: f64 = CustomAxisDemo::MINS_PER_DAY; const MINS_PER_DAY: f64 = CustomAxisDemo::MINS_PER_DAY;
const MINS_PER_H: f64 = CustomAxisDemo::MINS_PER_H; const MINS_PER_H: f64 = CustomAxisDemo::MINS_PER_H;
fn get_day(x: f64) -> f64 { fn day(x: f64) -> f64 {
(x / MINS_PER_DAY).floor() (x / MINS_PER_DAY).floor()
} }
fn get_hour(x: f64) -> f64 { fn hour(x: f64) -> f64 {
(x.rem_euclid(MINS_PER_DAY) / MINS_PER_H).floor() (x.rem_euclid(MINS_PER_DAY) / MINS_PER_H).floor()
} }
fn get_minute(x: f64) -> f64 { fn minute(x: f64) -> f64 {
x.rem_euclid(MINS_PER_H).floor() x.rem_euclid(MINS_PER_H).floor()
} }
fn get_percent(y: f64) -> f64 { fn percent(y: f64) -> f64 {
100.0 * y 100.0 * y
} }
@ -411,17 +411,17 @@ impl CustomAxisDemo {
String::new() String::new()
} else if is_approx_integer(x / MINS_PER_DAY) { } else if is_approx_integer(x / MINS_PER_DAY) {
// Days // Days
format!("Day {}", get_day(x)) format!("Day {}", day(x))
} else { } else {
// Hours and minutes // Hours and minutes
format!("{h}:{m:02}", h = get_hour(x), m = get_minute(x)) format!("{h}:{m:02}", h = hour(x), m = minute(x))
} }
}; };
let y_fmt = |y, _range: &RangeInclusive<f64>| { let y_fmt = |y, _range: &RangeInclusive<f64>| {
// Display only integer percentages // Display only integer percentages
if !is_approx_zero(y) && is_approx_integer(100.0 * y) { if !is_approx_zero(y) && is_approx_integer(100.0 * y) {
format!("{:.0}%", get_percent(y)) format!("{:.0}%", percent(y))
} else { } else {
String::new() String::new()
} }
@ -430,10 +430,10 @@ impl CustomAxisDemo {
let label_fmt = |_s: &str, val: &PlotPoint| { let label_fmt = |_s: &str, val: &PlotPoint| {
format!( format!(
"Day {d}, {h}:{m:02}\n{p:.2}%", "Day {d}, {h}:{m:02}\n{p:.2}%",
d = get_day(val.x), d = day(val.x),
h = get_hour(val.x), h = hour(val.x),
m = get_minute(val.x), m = minute(val.x),
p = get_percent(val.y) p = percent(val.y)
) )
}; };

View file

@ -60,7 +60,7 @@ image = { version = "0.24", optional = true, default-features = false }
# svg feature # svg feature
resvg = { version = "0.23", optional = true } resvg = { version = "0.23", optional = true }
tiny-skia = { version = "0.6", optional = true } tiny-skia = { version = "0.6", optional = true } # must be updated in lock-step with resvg
usvg = { version = "0.23", optional = true } usvg = { version = "0.23", optional = true }
# feature "serde": # feature "serde":

View file

@ -44,9 +44,9 @@ egui = { version = "0.18.0", path = "../egui", default-features = false, feature
] } ] }
egui-winit = { version = "0.18.0", path = "../egui-winit", default-features = false } egui-winit = { version = "0.18.0", path = "../egui-winit", default-features = false }
ahash = "0.7" ahash = "0.8"
bytemuck = "1.7" bytemuck = "1.7"
glium = "0.31" glium = "0.32"
#! ### Optional dependencies #! ### Optional dependencies
## Enable this when generating docs. ## Enable this when generating docs.

View file

@ -3,7 +3,7 @@
use glium::glutin; use glium::glutin;
fn main() { fn main() {
let event_loop = glutin::event_loop::EventLoop::with_user_event(); let event_loop = glutin::event_loop::EventLoopBuilder::with_user_event().build();
let display = create_display(&event_loop); let display = create_display(&event_loop);
let mut egui_glium = egui_glium::EguiGlium::new(&display, &event_loop); let mut egui_glium = egui_glium::EguiGlium::new(&display, &event_loop);

View file

@ -5,7 +5,7 @@
use glium::glutin; use glium::glutin;
fn main() { fn main() {
let event_loop = glutin::event_loop::EventLoop::with_user_event(); let event_loop = glutin::event_loop::EventLoopBuilder::with_user_event().build();
let display = create_display(&event_loop); let display = create_display(&event_loop);
let mut egui_glium = egui_glium::EguiGlium::new(&display, &event_loop); let mut egui_glium = egui_glium::EguiGlium::new(&display, &event_loop);

View file

@ -4,7 +4,6 @@
use egui::epaint::Primitive; use egui::epaint::Primitive;
use { use {
ahash::AHashMap,
egui::{emath::Rect, epaint::Mesh}, egui::{emath::Rect, epaint::Mesh},
glium::{ glium::{
implement_vertex, implement_vertex,
@ -21,7 +20,7 @@ pub struct Painter {
max_texture_side: usize, max_texture_side: usize,
program: glium::Program, program: glium::Program,
textures: AHashMap<egui::TextureId, Rc<SrgbTexture2d>>, textures: ahash::HashMap<egui::TextureId, Rc<SrgbTexture2d>>,
/// [`egui::TextureId::User`] index /// [`egui::TextureId::User`] index
next_native_tex_id: u64, next_native_tex_id: u64,
@ -145,7 +144,7 @@ impl Painter {
let width_in_points = width_in_pixels as f32 / pixels_per_point; let width_in_points = width_in_pixels as f32 / pixels_per_point;
let height_in_points = height_in_pixels as f32 / pixels_per_point; let height_in_points = height_in_pixels as f32 / pixels_per_point;
if let Some(texture) = self.get_texture(mesh.texture_id) { if let Some(texture) = self.texture(mesh.texture_id) {
// The texture coordinates for text are so that both nearest and linear should work with the egui font texture. // The texture coordinates for text are so that both nearest and linear should work with the egui font texture.
// For user textures linear sampling is more likely to be the right choice. // For user textures linear sampling is more likely to be the right choice.
let filter = MagnifySamplerFilter::Linear; let filter = MagnifySamplerFilter::Linear;
@ -274,7 +273,7 @@ impl Painter {
self.textures.remove(&tex_id); self.textures.remove(&tex_id);
} }
fn get_texture(&self, texture_id: egui::TextureId) -> Option<&SrgbTexture2d> { fn texture(&self, texture_id: egui::TextureId) -> Option<&SrgbTexture2d> {
self.textures.get(&texture_id).map(|rc| rc.as_ref()) self.textures.get(&texture_id).map(|rc| rc.as_ref())
} }

View file

@ -72,7 +72,7 @@ wasm-bindgen = { version = "0.2" }
[dev-dependencies] [dev-dependencies]
glutin = "0.28.0" # examples/pure_glow glutin = "0.29.0" # examples/pure_glow
[[example]] [[example]]

View file

@ -6,7 +6,7 @@
fn main() { fn main() {
let mut clear_color = [0.1, 0.1, 0.1]; let mut clear_color = [0.1, 0.1, 0.1];
let event_loop = glutin::event_loop::EventLoop::with_user_event(); let event_loop = glutin::event_loop::EventLoopBuilder::with_user_event().build();
let (gl_window, gl) = create_display(&event_loop); let (gl_window, gl) = create_display(&event_loop);
let gl = std::sync::Arc::new(gl); let gl = std::sync::Arc::new(gl);

View file

@ -452,7 +452,7 @@ impl Painter {
#[inline(never)] // Easier profiling #[inline(never)] // Easier profiling
fn paint_mesh(&mut self, mesh: &Mesh) { fn paint_mesh(&mut self, mesh: &Mesh) {
debug_assert!(mesh.is_valid()); debug_assert!(mesh.is_valid());
if let Some(texture) = self.get_texture(mesh.texture_id) { if let Some(texture) = self.texture(mesh.texture_id) {
unsafe { unsafe {
self.gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo)); self.gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo));
self.gl.buffer_data_u8_slice( self.gl.buffer_data_u8_slice(
@ -634,10 +634,15 @@ impl Painter {
} }
/// Get the [`glow::Texture`] bound to a [`egui::TextureId`]. /// Get the [`glow::Texture`] bound to a [`egui::TextureId`].
pub fn get_texture(&self, texture_id: egui::TextureId) -> Option<glow::Texture> { pub fn texture(&self, texture_id: egui::TextureId) -> Option<glow::Texture> {
self.textures.get(&texture_id).copied() self.textures.get(&texture_id).copied()
} }
#[deprecated = "renamed 'texture'"]
pub fn get_texture(&self, texture_id: egui::TextureId) -> Option<glow::Texture> {
self.texture(texture_id)
}
#[allow(clippy::needless_pass_by_value)] // False positive #[allow(clippy::needless_pass_by_value)] // False positive
pub fn register_native_texture(&mut self, native: glow::Texture) -> egui::TextureId { pub fn register_native_texture(&mut self, native: glow::Texture) -> egui::TextureId {
self.assert_not_destroyed(); self.assert_not_destroyed();

View file

@ -56,7 +56,7 @@ serde = ["dep:serde", "ahash/serde", "emath/serde"]
emath = { version = "0.18.0", path = "../emath" } emath = { version = "0.18.0", path = "../emath" }
ab_glyph = "0.2.11" ab_glyph = "0.2.11"
ahash = { version = "0.7", default-features = false, features = ["std"] } ahash = { version = "0.8", default-features = false, features = ["std"] }
nohash-hasher = "0.2" nohash-hasher = "0.2"
#! ### Optional dependencies #! ### Optional dependencies

View file

@ -13,6 +13,11 @@ pub use crate::{CubicBezierShape, QuadraticBezierShape};
/// A paint primitive such as a circle or a piece of text. /// A paint primitive such as a circle or a piece of text.
/// Coordinates are all screen space points (not physical pixels). /// Coordinates are all screen space points (not physical pixels).
///
/// You should generally recreate your [`Shape`]s each frame,
/// but storing them should also be fine with one exception:
/// [`Shape::Text`] depends on the current `pixels_per_point` (dpi scale)
/// and so must be recreated every time `pixels_per_point` changes.
#[must_use = "Add a Shape to a Painter"] #[must_use = "Add a Shape to a Painter"]
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Shape { pub enum Shape {
@ -37,6 +42,8 @@ pub enum Shape {
Rect(RectShape), Rect(RectShape),
/// Text. /// Text.
///
/// This needs to be recreated if `pixels_per_point` (dpi scale) changes.
Text(TextShape), Text(TextShape),
/// A general triangle mesh. /// A general triangle mesh.
@ -604,6 +611,8 @@ impl Rounding {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// How to paint some text on screen. /// How to paint some text on screen.
///
/// This needs to be recreated if `pixels_per_point` (dpi scale) changes.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct TextShape { pub struct TextShape {

View file

@ -1334,6 +1334,11 @@ impl Tessellator {
return; return;
} }
if galley.pixels_per_point != self.pixels_per_point {
eprintln!("epaint: WARNING: pixels_per_point (dpi scale) have changed between text layout and tessellation. \
You must recreate your text shapes if pixels_per_point changes.");
}
out.vertices.reserve(galley.num_vertices); out.vertices.reserve(galley.num_vertices);
out.indices.reserve(galley.num_indices); out.indices.reserve(galley.num_indices);

View file

@ -2,7 +2,6 @@ use crate::{
mutex::{Mutex, RwLock}, mutex::{Mutex, RwLock},
TextureAtlas, TextureAtlas,
}; };
use ahash::AHashMap;
use emath::{vec2, Vec2}; use emath::{vec2, Vec2};
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::sync::Arc; use std::sync::Arc;
@ -66,7 +65,7 @@ pub struct FontImpl {
// move each character by this much (hack) // move each character by this much (hack)
y_offset: f32, y_offset: f32,
pixels_per_point: f32, pixels_per_point: f32,
glyph_info_cache: RwLock<AHashMap<char, GlyphInfo>>, // TODO(emilk): standard Mutex glyph_info_cache: RwLock<ahash::HashMap<char, GlyphInfo>>, // TODO(emilk): standard Mutex
atlas: Arc<Mutex<TextureAtlas>>, atlas: Arc<Mutex<TextureAtlas>>,
} }
@ -221,7 +220,7 @@ pub struct Font {
replacement_glyph: (FontIndex, GlyphInfo), replacement_glyph: (FontIndex, GlyphInfo),
pixels_per_point: f32, pixels_per_point: f32,
row_height: f32, row_height: f32,
glyph_info_cache: AHashMap<char, (FontIndex, GlyphInfo)>, glyph_info_cache: ahash::HashMap<char, (FontIndex, GlyphInfo)>,
} }
impl Font { impl Font {

View file

@ -535,7 +535,7 @@ pub struct FontsImpl {
definitions: FontDefinitions, definitions: FontDefinitions,
atlas: Arc<Mutex<TextureAtlas>>, atlas: Arc<Mutex<TextureAtlas>>,
font_impl_cache: FontImplCache, font_impl_cache: FontImplCache,
sized_family: ahash::AHashMap<(u32, FontFamily), Font>, sized_family: ahash::HashMap<(u32, FontFamily), Font>,
} }
impl FontsImpl { impl FontsImpl {
@ -673,7 +673,7 @@ struct FontImplCache {
ab_glyph_fonts: BTreeMap<String, (FontTweak, ab_glyph::FontArc)>, ab_glyph_fonts: BTreeMap<String, (FontTweak, ab_glyph::FontArc)>,
/// Map font pixel sizes and names to the cached [`FontImpl`]. /// Map font pixel sizes and names to the cached [`FontImpl`].
cache: ahash::AHashMap<(u32, String), Arc<FontImpl>>, cache: ahash::HashMap<(u32, String), Arc<FontImpl>>,
} }
impl FontImplCache { impl FontImplCache {

View file

@ -478,6 +478,7 @@ fn galley_from_rows(point_scale: PointScale, job: Arc<LayoutJob>, mut rows: Vec<
mesh_bounds, mesh_bounds,
num_vertices, num_vertices,
num_indices, num_indices,
pixels_per_point: point_scale.pixels_per_point,
} }
} }

View file

@ -309,6 +309,8 @@ impl Default for TextWrapping {
/// Text that has been layed out, ready for painting. /// Text that has been layed out, ready for painting.
/// ///
/// You can create a [`Galley`] using [`crate::Fonts::layout_job`]; /// You can create a [`Galley`] using [`crate::Fonts::layout_job`];
///
/// This needs to be recreated if `pixels_per_point` (dpi scale) changes.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Galley { pub struct Galley {
@ -341,6 +343,12 @@ pub struct Galley {
/// Total number of indices in all the row meshes. /// Total number of indices in all the row meshes.
pub num_indices: usize, pub num_indices: usize,
/// The number of physical pixels for each logical point.
/// Since this affects the layout, we keep track of it
/// so that we can warn if this has changed once we get to
/// tessellation.
pub pixels_per_point: f32,
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]

View file

@ -1,5 +1,4 @@
use crate::{ImageData, ImageDelta, TextureId}; use crate::{ImageData, ImageDelta, TextureId};
use ahash::AHashMap;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -11,7 +10,7 @@ pub struct TextureManager {
/// We allocate texture id:s linearly. /// We allocate texture id:s linearly.
next_id: u64, next_id: u64,
/// Information about currently allocated textures. /// Information about currently allocated textures.
metas: AHashMap<TextureId, TextureMeta>, metas: ahash::HashMap<TextureId, TextureMeta>,
delta: TexturesDelta, delta: TexturesDelta,
} }

View file

@ -6,7 +6,7 @@ pub use ordered_float::*;
#[inline] #[inline]
pub fn hash(value: impl std::hash::Hash) -> u64 { pub fn hash(value: impl std::hash::Hash) -> u64 {
use std::hash::Hasher as _; use std::hash::Hasher as _;
let mut hasher = ahash::AHasher::new_with_keys(123, 456); let mut hasher = ahash::AHasher::default();
value.hash(&mut hasher); value.hash(&mut hasher);
hasher.finish() hasher.finish()
} }

View file

@ -7,9 +7,16 @@ edition = "2021"
rust-version = "1.61" rust-version = "1.61"
publish = false publish = false
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
eframe = { path = "../../eframe", features = ["glow"] } eframe = { path = "../../eframe", features = ["glow"] }
egui_glow = { path = "../../egui_glow" } egui_glow = { path = "../../egui_glow" }
glow = "0.11" glow = "0.11"
three-d = { version = "0.12", default-features = false } # 2022-05-22 three-d = { version = "0.13", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies] # Web dependencies
wasm-bindgen = "0.2" # Core bindings
wasm-bindgen-futures = "0.4" # Core bindings
console_error_panic_hook = "0.1" # For logging

View file

@ -14,3 +14,7 @@ If you are content of having egui sit on top of a 3D background, take a look at:
```sh ```sh
cargo run -p custom_3d_three-d cargo run -p custom_3d_three-d
``` ```
```
wasm-pack build examples/custom_3d_three-d --target web
```

View file

@ -0,0 +1,38 @@
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
</head>
<body>
<canvas id="my" style="position: absolute;top:0;bottom: 0;left: 0;right: 0;margin:auto;"></canvas>
<!-- Note the usage of `type=module` here as this is an ES6 module -->
<script type="module">
// Use ES module import syntax to import functionality from the module
// that we have compiled.
//
// Note that the `default` import is an initialization function which
// will "boot" the module and make it ready to use. Currently browsers
// don't support natively imported WebAssembly as an ES module, but
// eventually the manual initialization won't be required!
import init from './pkg/custom_3d_three_d.js';
async function run() {
// First up we need to actually load the wasm file, so we use the
// default export to inform it where the wasm file is located on the
// server, and then we wait on the returned promise to wait for the
// wasm to be loaded.
// It may look like this: `await init('./pkg/without_a_bundler_bg.wasm');`,
// but there is also a handy default inside `init` function, which uses
// `import.meta` to locate the wasm file relatively to js file
//
// Note that instead of a string here you can also pass in an instance
// of `WebAssembly.Module` which allows you to compile your own module.
// Also note that the promise, when resolved, yields the wasm module's
// exports which is the same as importing the `*_bg` module in other
// modes
await init('./pkg/custom_3d_three_d_bg.wasm');
}
run();
</script>
</body>
</html>

View file

@ -0,0 +1,19 @@
mod main;
// Entry point for wasm
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(start)]
pub async fn start() -> Result<(), JsValue> {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
let web_options = eframe::WebOptions::default();
eframe::start_web(
"my",
web_options,
Box::new(|cc| Box::new(main::MyApp::new(cc))),
)?;
Ok(())
}

View file

@ -1,7 +1,9 @@
#![allow(dead_code)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui; use eframe::egui;
#[cfg(not(target_arch = "wasm32"))]
fn main() { fn main() {
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(550.0, 610.0)), initial_window_size: Some(egui::vec2(550.0, 610.0)),
@ -17,12 +19,12 @@ fn main() {
); );
} }
struct MyApp { pub struct MyApp {
angle: f32, angle: f32,
} }
impl MyApp { impl MyApp {
fn new(_cc: &eframe::CreationContext<'_>) -> Self { pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
Self { angle: 0.2 } Self { angle: 0.2 }
} }
} }
@ -41,7 +43,28 @@ impl eframe::App for MyApp {
egui::ScrollArea::both().show(ui, |ui| { egui::ScrollArea::both().show(ui, |ui| {
egui::Frame::canvas(ui.style()).show(ui, |ui| { egui::Frame::canvas(ui.style()).show(ui, |ui| {
self.custom_painting(ui); let (rect, response) =
ui.allocate_exact_size(egui::Vec2::splat(512.0), egui::Sense::drag());
self.angle += response.drag_delta().x * 0.01;
// Clone locals so we can move them into the paint callback:
let angle = self.angle;
let callback = egui::PaintCallback {
rect,
callback: std::sync::Arc::new(egui_glow::CallbackFn::new(
move |info, painter| {
with_three_d(painter.gl(), |three_d| {
three_d.frame(
FrameInput::new(&three_d.context, &info, painter),
angle,
);
});
},
)),
};
ui.painter().add(callback);
}); });
ui.label("Drag to rotate!"); ui.label("Drag to rotate!");
}); });
@ -54,65 +77,68 @@ impl eframe::App for MyApp {
} }
} }
impl MyApp { /// We get a [`glow::Context`] from `eframe` and we want to construct a [`ThreeDApp`].
fn custom_painting(&mut self, ui: &mut egui::Ui) {
let (rect, response) =
ui.allocate_exact_size(egui::Vec2::splat(512.0), egui::Sense::drag());
self.angle += response.drag_delta().x * 0.01;
// Clone locals so we can move them into the paint callback:
let angle = self.angle;
let callback = egui::PaintCallback {
rect,
callback: std::sync::Arc::new(egui_glow::CallbackFn::new(move |info, painter| {
with_three_d_context(painter.gl(), |three_d| {
paint_with_three_d(three_d, &info, angle);
});
})),
};
ui.painter().add(callback);
}
}
/// We get a [`glow::Context`] from `eframe`, but we want a [`three_d::Context`].
/// ///
/// Sadly we can't just create a [`three_d::Context`] in [`MyApp::new`] and pass it /// Sadly we can't just create a [`ThreeDApp`] in [`MyApp::new`] and pass it
/// to the [`egui::PaintCallback`] because [`three_d::Context`] isn't `Send+Sync`, which /// to the [`egui::PaintCallback`] because [`glow::Context`] isn't `Send+Sync` on web, which
/// [`egui::PaintCallback`] is. /// [`egui::PaintCallback`] needs. If you do not target web, then you can construct the [`ThreeDApp`] in [`MyApp::new`].
fn with_three_d_context<R>( fn with_three_d<R>(gl: &std::sync::Arc<glow::Context>, f: impl FnOnce(&mut ThreeDApp) -> R) -> R {
gl: &std::sync::Arc<glow::Context>,
f: impl FnOnce(&three_d::Context) -> R,
) -> R {
use std::cell::RefCell; use std::cell::RefCell;
thread_local! { thread_local! {
pub static THREE_D: RefCell<Option<three_d::Context>> = RefCell::new(None); pub static THREE_D: RefCell<Option<ThreeDApp>> = RefCell::new(None);
}
// If you are using the depth buffer you need to do this:
#[allow(unsafe_code)]
unsafe {
use glow::HasContext as _;
gl.enable(glow::DEPTH_TEST);
if !cfg!(target_arch = "wasm32") {
gl.disable(glow::FRAMEBUFFER_SRGB);
}
gl.clear(glow::DEPTH_BUFFER_BIT);
} }
THREE_D.with(|three_d| { THREE_D.with(|three_d| {
let mut three_d = three_d.borrow_mut(); let mut three_d = three_d.borrow_mut();
let three_d = let three_d = three_d.get_or_insert_with(|| ThreeDApp::new(gl.clone()));
three_d.get_or_insert_with(|| three_d::Context::from_gl_context(gl.clone()).unwrap());
f(three_d) f(three_d)
}) })
} }
fn paint_with_three_d(three_d: &three_d::Context, info: &egui::PaintCallbackInfo, angle: f32) { ///
// Based on https://github.com/asny/three-d/blob/master/examples/triangle/src/main.rs /// Translates from egui input to three-d input
///
pub struct FrameInput<'a> {
screen: three_d::RenderTarget<'a>,
viewport: three_d::Viewport,
scissor_box: three_d::ScissorBox,
}
impl FrameInput<'_> {
pub fn new(
context: &three_d::Context,
info: &egui::PaintCallbackInfo,
painter: &egui_glow::Painter,
) -> Self {
use three_d::*; use three_d::*;
// Disable sRGB textures for three-d
#[cfg(not(target_arch = "wasm32"))]
#[allow(unsafe_code)]
unsafe {
use glow::HasContext as _;
context.disable(glow::FRAMEBUFFER_SRGB);
}
// Constructs a screen render target to render the final image to
let screen = painter.intermediate_fbo().map_or_else(
|| {
RenderTarget::screen(
context,
info.viewport.width() as u32,
info.viewport.height() as u32,
)
},
|fbo| {
RenderTarget::from_framebuffer(
context,
info.viewport.width() as u32,
info.viewport.height() as u32,
fbo,
)
},
);
// Set where to paint // Set where to paint
let viewport = info.viewport_in_pixels(); let viewport = info.viewport_in_pixels();
let viewport = Viewport { let viewport = Viewport {
@ -124,24 +150,44 @@ fn paint_with_three_d(three_d: &three_d::Context, info: &egui::PaintCallbackInfo
// Respect the egui clip region (e.g. if we are inside an `egui::ScrollArea`). // Respect the egui clip region (e.g. if we are inside an `egui::ScrollArea`).
let clip_rect = info.clip_rect_in_pixels(); let clip_rect = info.clip_rect_in_pixels();
three_d.set_scissor(ScissorBox { let scissor_box = ScissorBox {
x: clip_rect.left_px.round() as _, x: clip_rect.left_px.round() as _,
y: clip_rect.from_bottom_px.round() as _, y: clip_rect.from_bottom_px.round() as _,
width: clip_rect.width_px.round() as _, width: clip_rect.width_px.round() as _,
height: clip_rect.height_px.round() as _, height: clip_rect.height_px.round() as _,
}); };
Self {
let camera = Camera::new_perspective( screen,
three_d, scissor_box,
viewport, viewport,
}
}
}
///
/// Based on the `three-d` [Triangle example](https://github.com/asny/three-d/blob/master/examples/triangle/src/main.rs).
/// This is where you'll need to customize
///
use three_d::*;
pub struct ThreeDApp {
context: Context,
camera: Camera,
model: Gm<Mesh, ColorMaterial>,
}
impl ThreeDApp {
pub fn new(gl: std::sync::Arc<glow::Context>) -> Self {
let context = Context::from_gl_context(gl).unwrap();
// Create a camera
let camera = Camera::new_perspective(
Viewport::new_at_origo(1, 1),
vec3(0.0, 0.0, 2.0), vec3(0.0, 0.0, 2.0),
vec3(0.0, 0.0, 0.0), vec3(0.0, 0.0, 0.0),
vec3(0.0, 1.0, 0.0), vec3(0.0, 1.0, 0.0),
degrees(45.0), degrees(45.0),
0.1, 0.1,
10.0, 10.0,
) );
.unwrap();
// Create a CPU-side mesh consisting of a single colored triangle // Create a CPU-side mesh consisting of a single colored triangle
let positions = vec![ let positions = vec![
@ -160,14 +206,31 @@ fn paint_with_three_d(three_d: &three_d::Context, info: &egui::PaintCallbackInfo
..Default::default() ..Default::default()
}; };
let mut model = Gm::new( // Construct a model, with a default color material, thereby transferring the mesh data to the GPU
Mesh::new(three_d, &cpu_mesh).unwrap(), let model = Gm::new(Mesh::new(&context, &cpu_mesh), ColorMaterial::default());
ColorMaterial::default(), Self {
); context,
camera,
model,
}
}
pub fn frame(&mut self, frame_input: FrameInput<'_>, angle: f32) -> Option<glow::Framebuffer> {
// Ensure the viewport matches the current window viewport which changes if the window is resized
self.camera.set_viewport(frame_input.viewport);
// Set the current transformation of the triangle // Set the current transformation of the triangle
model.set_transformation(Mat4::from_angle_y(radians(angle))); self.model
.set_transformation(Mat4::from_angle_y(radians(angle)));
// Get the screen render target to be able to render something on the screen
frame_input
.screen
// Clear the color and depth of the screen render target
.clear_partially(frame_input.scissor_box, ClearState::depth(1.0))
// Render the triangle with the color material which uses the per vertex colors defined at construction // Render the triangle with the color material which uses the per vertex colors defined at construction
model.render(&camera, &[]).unwrap(); .render_partially(frame_input.scissor_box, &self.camera, &[&self.model], &[]);
frame_input.screen.into_framebuffer() // Take back the screen fbo, we will continue to use it.
}
} }

View file

@ -99,8 +99,8 @@ fn custon_window_frame(
rect rect
}; };
let title_bar_response = let title_bar_response =
ui.interact(title_bar_rect, Id::new("title_bar"), Sense::drag()); ui.interact(title_bar_rect, Id::new("title_bar"), Sense::click());
if title_bar_response.drag_started() { if title_bar_response.is_pointer_button_down_on() {
frame.drag_window(); frame.drag_window();
} }

View file

@ -0,0 +1,12 @@
[package]
name = "serial_windows"
version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.61"
publish = false
[dependencies]
eframe = { path = "../../eframe" }

View file

@ -0,0 +1,8 @@
Demonstrates how to open several windows after each other.
NOTE: this doesn't work on Mac due to <https://github.com/rust-windowing/winit/issues/2431>.
See also <https://github.com/emilk/egui/issues/1918>.
```sh
cargo run -p serial_windows
```

View file

@ -0,0 +1,53 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui;
fn main() {
if cfg!(target_os = "macos") {
eprintln!("WARNING: this example does not work on Mac! See https://github.com/emilk/egui/issues/1918");
}
let options = eframe::NativeOptions {
run_and_return: true,
..Default::default()
};
eprintln!("Starting first window…");
eframe::run_native(
"First Window",
options.clone(),
Box::new(|_cc| Box::new(MyApp::default())),
);
std::thread::sleep(std::time::Duration::from_secs(2));
eprintln!("Starting second window…");
eframe::run_native(
"Second Window",
options.clone(),
Box::new(|_cc| Box::new(MyApp::default())),
);
std::thread::sleep(std::time::Duration::from_secs(2));
eprintln!("Starting third window…");
eframe::run_native(
"Third Window",
options,
Box::new(|_cc| Box::new(MyApp::default())),
);
}
#[derive(Default)]
struct MyApp {}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
if ui.button("Close").clicked() {
eprintln!("Pressed Close button");
frame.quit();
}
});
}
}