Browse Source

Multiple viewports/windows (#3172)

* Closes #1044

---
(new PR description written by @emilk)

## Overview
This PR introduces the concept of `Viewports`, which on the native
eframe backend corresponds to native OS windows.

You can spawn a new viewport using `Context::show_viewport` and
`Cotext::show_viewport_immediate`.
These needs to be called every frame the viewport should be visible.

This is implemented by the native `eframe` backend, but not the web one.

## Viewport classes
The viewports form a tree of parent-child relationships.

There are different classes of viewports.

### Root vieport
The root viewport is the original viewport, and cannot be closed without
closing the application.

### Deferred viewports
These are created with `Context::show_viewport`.
Deferred viewports take a closure that is called by the integration at a
later time, perhaps multiple times.
Deferred viewports are repainted independenantly of the parent viewport.
This means communication with them need to done via channels, or
`Arc/Mutex`.

This is the most performant type of child viewport, though a bit more
cumbersome to work with compared to immediate viewports.

### Immediate viewports
These are created with `Context::show_viewport_immediate`.
Immediate viewports take a `FnOnce` closure, similar to other egui
functions, and is called immediately. This makes communication with them
much simpler than with deferred viewports, but this simplicity comes at
a cost: whenever tha parent viewports needs to be repainted, so will the
child viewport, and vice versa. This means that if you have `N`
viewports you are poentially doing `N` times as much CPU work. However,
if all your viewports are showing animations, and thus are repainting
constantly anyway, this doesn't matter.

In short: immediate viewports are simpler to use, but can waste a lot of
CPU time.

### Embedded viewports
These are not real, independenant viewports, but is a fallback mode for
when the integration does not support real viewports. In your callback
is called with `ViewportClass::Embedded` it means you need to create an
`egui::Window` to wrap your ui in, which will then be embedded in the
parent viewport, unable to escape it.


## Using the viewports
Only one viewport is active at any one time, identified wth
`Context::viewport_id`.
You can send commands to other viewports using
`Context::send_viewport_command_to`.

There is an example in
<https://github.com/emilk/egui/tree/master/examples/multiple_viewports/src/main.rs>.

## For integrations
There are several changes relevant to integrations.

* There is a [`crate::RawInput::viewport`] with information about the
current viewport.
* The repaint callback set by `Context::set_request_repaint_callback`
now points to which viewport should be repainted.
* `Context::run` now returns a list of viewports in `FullOutput` which
should result in their own independant windows
* There is a new `Context::set_immediate_viewport_renderer` for setting
up the immediate viewport integration
* If you support viewports, you need to call
`Context::set_embed_viewports(false)`, or all new viewports will be
embedded (the default behavior).


## Future work
* Make it easy to wrap child viewports in the same chrome as
`egui::Window`
* Automatically show embedded viewports using `egui::Window`
* Use the new `ViewportBuilder` in `eframe::NativeOptions`
* Automatically position new viewport windows (they currently cover each
other)
* Add a `Context` method for listing all existing viewports

Find more at https://github.com/emilk/egui/issues/3556




---

<details>
<summary>
Outdated PR description by @konkitoman
</summary>


## Inspiration
- Godot because the app always work desktop or single_window because of
embedding
- Dear ImGui viewport system

## What is a Viewport

A Viewport is a egui isolated component!
Can be used by the egui integration to create native windows!

When you create a Viewport is possible that the backend do not supports
that!
So you need to check if the Viewport was created or you are in the
normal egui context!
This is how you can do that:
```rust
if ctx.viewport_id() != ctx.parent_viewport_id() {
    // In here you add the code for the viewport context, like
    egui::CentralPanel::default().show(ctx, |ui|{
        ui.label("This is in a native window!");
    });
}else{
    // In here you add the code for when viewport cannot be created!
   // You cannot use CentralPanel in here because you will override the app CentralPanel
   egui::Window::new("Virtual Viewport").show(ctx, |ui|{
       ui.label("This is without a native window!\nThis is in a embedded viewport");
   });
}
```

This PR do not support for drag and drop between Viewports!

After this PR is accepted i will begin work to intregrate the Viewport
system in `egui::Window`!
The `egui::Window` i want to behave the same on desktop and web
The `egui::Window` will be like Godot Window

## Changes and new

These are only public structs and functions!

<details>
<summary>

## New
</summary>

- `egui::ViewportId`
- `egui::ViewportBuilder`
This is like winit WindowBuilder

- `egui::ViewportCommand`
With this you can set any winit property on a viewport, when is a native
window!

- `egui::Context::new`
- `egui::Context::create_viewport`
- `egui::Context::create_viewport_sync`
- `egui::Context::viewport_id`
- `egui::Context::parent_viewport_id`
- `egui::Context::viewport_id_pair`
- `egui::Context::set_render_sync_callback`
- `egui::Context::is_desktop`
- `egui::Context::force_embedding`
- `egui::Context::set_force_embedding`
- `egui::Context::viewport_command`
- `egui::Context::send_viewport_command_to`
- `egui::Context::input_for`
- `egui::Context::input_mut_for`
- `egui::Context::frame_nr_for`
- `egui::Context::request_repaint_for`
- `egui::Context::request_repaint_after_for`
- `egui::Context::requested_repaint_last_frame`
- `egui::Context::requested_repaint_last_frame_for`
- `egui::Context::requested_repaint`
- `egui::Context::requested_repaint_for`
- `egui::Context::inner_rect`
- `egui::Context::outer_rect`

- `egui::InputState::inner_rect`
- `egui::InputState::outer_rect`

- `egui::WindowEvent`

</details>

<details>
<summary>

## Changes
</summary>

- `egui::Context::run`
Now needs the viewport that we want to render!

- `egui::Context::begin_frame`
Now needs the viewport that we want to render!

- `egui::Context::tessellate`
Now needs the viewport that we want to render!

- `egui::FullOutput`
```diff
- repaint_after
+ viewports
+ viewport_commands
```

- `egui::RawInput`
```diff
+ inner_rect
+ outer_rect
```

- `egui::Event`
```diff
+ WindowEvent
```
</details>

### Async Viewport

Async means that is independent from other viewports!

Is created by `egui::Context::create_viewport`

To be used you will need to wrap your state in `Arc<RwLock<T>>`
Look at viewports example to understand how to use it!

### Sync Viewport

Sync means that is dependent on his parent!

Is created by `egui::Context::create_viewport_sync`

This will pause the parent then render itself the resumes his parent!

#### ⚠️ This currently will make the fps/2 for every sync
viewport

### Common

#### ⚠️ Attention

You will need to do this when you render your content
```rust
ctx.create_viewport(ViewportBuilder::new("Simple Viewport"), | ctx | {
    let content = |ui: &mut egui::Ui|{
        ui.label("Content");
    };

    // This will make the content a popup if cannot create a native window
    if ctx.viewport_id() != ctx.parent_viewport_id() {
        egui::CentralPanel::default().show(ctx, content);
    } else {
        egui::Area::new("Simple Viewport").show(ctx, |ui| {
            egui::Frame::popup(ui.style()).show(ui, content);
        });
    };
});
````

## What you need to know as egui user

### If you are using eframe

You don't need to change anything!

### If you have a manual implementation

Now `egui::run` or `egui::begin` and `egui::tessellate` will need the
current viewport id!
You cannot create a `ViewportId` only `ViewportId::MAIN`

If you make a single window app you will set the viewport id to be
`egui::ViewportId::MAIN` or see the `examples/pure_glow`
If you want to have multiples window support look at `crates/eframe`
glow or wgpu implementations!

## If you want to try this

- cargo run -p viewports

## This before was wanted to change

This will probably be in feature PR's

### egui::Window

To create a native window when embedded was set to false
You can try that in viewports example before:
[78a0ae8](78a0ae879e)

### egui popups, context_menu, tooltip

To be a native window

</details>

---------

Co-authored-by: Konkitoman <konkitoman@users.noreply.github.com>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Co-authored-by: Pablo Sichert <mail@pablosichert.com>
pull/3557/head
Konkitoman 12 months ago
committed by GitHub
parent
commit
83aa3109d3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      Cargo.lock
  2. 6
      crates/eframe/src/epi/mod.rs
  3. 7
      crates/eframe/src/lib.rs
  4. 242
      crates/eframe/src/native/epi_integration.rs
  5. 2659
      crates/eframe/src/native/run.rs
  6. 18
      crates/eframe/src/web/app_runner.rs
  7. 5
      crates/eframe/src/web/events.rs
  8. 11
      crates/eframe/src/web/web_painter_wgpu.rs
  9. 1
      crates/egui-wgpu/Cargo.toml
  10. 167
      crates/egui-wgpu/src/winit.rs
  11. 4
      crates/egui-winit/Cargo.toml
  12. 4
      crates/egui-winit/src/clipboard.rs
  13. 366
      crates/egui-winit/src/lib.rs
  14. 31
      crates/egui-winit/src/window_settings.rs
  15. 12
      crates/egui/src/containers/area.rs
  16. 2
      crates/egui/src/containers/combo_box.rs
  17. 4
      crates/egui/src/containers/panel.rs
  18. 2
      crates/egui/src/containers/popup.rs
  19. 19
      crates/egui/src/containers/window.rs
  20. 938
      crates/egui/src/context.rs
  21. 51
      crates/egui/src/data/input.rs
  22. 52
      crates/egui/src/data/output.rs
  23. 2
      crates/egui/src/id.rs
  24. 1
      crates/egui/src/input_state.rs
  25. 6
      crates/egui/src/layers.rs
  26. 8
      crates/egui/src/lib.rs
  27. 4
      crates/egui/src/load.rs
  28. 143
      crates/egui/src/memory.rs
  29. 804
      crates/egui/src/viewport.rs
  30. 4
      crates/egui_demo_lib/benches/benchmark.rs
  31. 4
      crates/egui_demo_lib/src/lib.rs
  32. 39
      crates/egui_glow/examples/pure_glow.rs
  33. 2
      crates/egui_glow/src/lib.rs
  34. 31
      crates/egui_glow/src/painter.rs
  35. 63
      crates/egui_glow/src/winit.rs
  36. 9
      crates/epaint/src/image.rs
  37. 15
      examples/multiple_viewports/Cargo.toml
  38. 7
      examples/multiple_viewports/README.md
  39. 102
      examples/multiple_viewports/src/main.rs
  40. 17
      examples/test_viewports/Cargo.toml
  41. 3
      examples/test_viewports/README.md
  42. 476
      examples/test_viewports/src/main.rs
  43. 2
      scripts/check.sh

17
Cargo.lock

@ -1274,6 +1274,7 @@ version = "0.23.0"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"document-features", "document-features",
"egui",
"epaint", "epaint",
"log", "log",
"puffin", "puffin",
@ -2554,6 +2555,14 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "multiple_viewports"
version = "0.1.0"
dependencies = [
"eframe",
"env_logger",
]
[[package]] [[package]]
name = "naga" name = "naga"
version = "0.14.0" version = "0.14.0"
@ -3795,6 +3804,14 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "test_viewports"
version = "0.1.0"
dependencies = [
"eframe",
"env_logger",
]
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.16.0" version = "0.16.0"

6
crates/eframe/src/epi/mod.rs

@ -44,7 +44,7 @@ pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)
/// done by `eframe`. /// done by `eframe`.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))] #[cfg(any(feature = "glow", feature = "wgpu"))]
pub type WindowBuilderHook = Box<dyn FnOnce(WindowBuilder) -> WindowBuilder>; pub type WindowBuilderHook = Box<dyn FnOnce(egui::ViewportBuilder) -> egui::ViewportBuilder>;
/// This is how your app is created. /// This is how your app is created.
/// ///
@ -120,6 +120,10 @@ pub trait App {
/// The [`egui::Context`] can be cloned and saved if you like. /// The [`egui::Context`] can be cloned and saved if you like.
/// ///
/// To force a repaint, call [`egui::Context::request_repaint`] at any time (e.g. from another thread). /// To force a repaint, call [`egui::Context::request_repaint`] at any time (e.g. from another thread).
///
/// This is called for the root viewport ([`egui::ViewportId::ROOT`]).
/// Use [`egui::Context::show_viewport`] to spawn additional viewports (windows).
/// (A "viewport" in egui means an native OS window).
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame); fn update(&mut self, ctx: &egui::Context, frame: &mut Frame);
/// Get a handle to the app. /// Get a handle to the app.

7
crates/eframe/src/lib.rs

@ -313,6 +313,11 @@ pub enum Error {
#[error("Found no glutin configs matching the template: {0:?}. Error: {1:?}")] #[error("Found no glutin configs matching the template: {0:?}. Error: {1:?}")]
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn std::error::Error>), NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn std::error::Error>),
/// An error from [`glutin`] when using [`glow`].
#[cfg(feature = "glow")]
#[error("egui_glow: {0}")]
OpenGL(#[from] egui_glow::PainterError),
/// An error from [`wgpu`]. /// An error from [`wgpu`].
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
#[error("WGPU error: {0}")] #[error("WGPU error: {0}")]
@ -320,7 +325,7 @@ pub enum Error {
} }
/// Short for `Result<T, eframe::Error>`. /// Short for `Result<T, eframe::Error>`.
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T, E = Error> = std::result::Result<T, E>;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

242
crates/eframe/src/native/epi_integration.rs

@ -1,15 +1,10 @@
use winit::event_loop::EventLoopWindowTarget; use std::time::Instant;
#[cfg(target_os = "macos")] use winit::event_loop::EventLoopWindowTarget;
use winit::platform::macos::WindowBuilderExtMacOS as _;
use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _}; use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _};
#[cfg(feature = "accesskit")] use egui::{DeferredViewportUiCallback, NumExt as _, ViewportBuilder, ViewportId, ViewportIdPair};
use egui::accesskit;
use egui::NumExt as _;
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings}; use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings};
use crate::{epi, Theme, WindowInfo}; use crate::{epi, Theme, WindowInfo};
@ -22,13 +17,6 @@ pub struct WindowState {
pub maximized: bool, pub maximized: bool,
} }
pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize<f64> {
winit::dpi::LogicalSize {
width: points.x as f64,
height: points.y as f64,
}
}
pub fn read_window_info( pub fn read_window_info(
window: &winit::window::Window, window: &winit::window::Window,
pixels_per_point: f32, pixels_per_point: f32,
@ -77,7 +65,7 @@ pub fn window_builder<E>(
title: &str, title: &str,
native_options: &mut epi::NativeOptions, native_options: &mut epi::NativeOptions,
window_settings: Option<WindowSettings>, window_settings: Option<WindowSettings>,
) -> winit::window::WindowBuilder { ) -> ViewportBuilder {
let epi::NativeOptions { let epi::NativeOptions {
maximized, maximized,
decorated, decorated,
@ -97,24 +85,29 @@ pub fn window_builder<E>(
.. ..
} = native_options; } = native_options;
let window_icon = icon_data.clone().and_then(load_icon); let mut viewport_builder = egui::ViewportBuilder::default()
let mut window_builder = winit::window::WindowBuilder::new()
.with_title(title) .with_title(title)
.with_decorations(*decorated) .with_decorations(*decorated)
.with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None))) .with_fullscreen(*fullscreen)
.with_maximized(*maximized) .with_maximized(*maximized)
.with_resizable(*resizable) .with_resizable(*resizable)
.with_transparent(*transparent) .with_transparent(*transparent)
.with_window_icon(window_icon)
.with_active(*active) .with_active(*active)
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279 // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
// We must also keep the window hidden until AccessKit is initialized. // We must also keep the window hidden until AccessKit is initialized.
.with_visible(false); .with_visible(false);
if let Some(icon_data) = icon_data {
viewport_builder =
viewport_builder.with_window_icon(egui::ColorImage::from_rgba_premultiplied(
[icon_data.width as usize, icon_data.height as usize],
&icon_data.rgba,
));
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
if *fullsize_content { if *fullsize_content {
window_builder = window_builder viewport_builder = viewport_builder
.with_title_hidden(true) .with_title_hidden(true)
.with_titlebar_transparent(true) .with_titlebar_transparent(true)
.with_fullsize_content_view(true); .with_fullsize_content_view(true);
@ -122,21 +115,20 @@ pub fn window_builder<E>(
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(all(feature = "wayland", target_os = "linux"))]
{ {
use winit::platform::wayland::WindowBuilderExtWayland as _; viewport_builder = match &native_options.app_id {
match &native_options.app_id { Some(app_id) => viewport_builder.with_name(app_id, ""),
Some(app_id) => window_builder = window_builder.with_name(app_id, ""), None => viewport_builder.with_name(title, ""),
None => window_builder = window_builder.with_name(title, ""), };
}
} }
if let Some(min_size) = *min_window_size { if let Some(min_size) = *min_window_size {
window_builder = window_builder.with_min_inner_size(points_to_size(min_size)); viewport_builder = viewport_builder.with_min_inner_size(min_size);
} }
if let Some(max_size) = *max_window_size { if let Some(max_size) = *max_window_size {
window_builder = window_builder.with_max_inner_size(points_to_size(max_size)); viewport_builder = viewport_builder.with_max_inner_size(max_size);
} }
window_builder = window_builder_drag_and_drop(window_builder, *drag_and_drop_support); viewport_builder = viewport_builder.with_drag_and_drop(*drag_and_drop_support);
// Always use the default window size / position on iOS. Trying to restore the previous position // Always use the default window size / position on iOS. Trying to restore the previous position
// causes the window to be shown too small. // causes the window to be shown too small.
@ -147,20 +139,17 @@ pub fn window_builder<E>(
window_settings.clamp_size_to_sane_values(largest_monitor_point_size(event_loop)); window_settings.clamp_size_to_sane_values(largest_monitor_point_size(event_loop));
window_settings.clamp_position_to_monitors(event_loop); window_settings.clamp_position_to_monitors(event_loop);
window_builder = window_settings.initialize_window_builder(window_builder); viewport_builder = window_settings.initialize_viewport_builder(viewport_builder);
window_settings.inner_size_points() window_settings.inner_size_points()
} else { } else {
if let Some(pos) = *initial_window_pos { if let Some(pos) = *initial_window_pos {
window_builder = window_builder.with_position(winit::dpi::LogicalPosition { viewport_builder = viewport_builder.with_position(pos);
x: pos.x as f64,
y: pos.y as f64,
});
} }
if let Some(initial_window_size) = *initial_window_size { if let Some(initial_window_size) = *initial_window_size {
let initial_window_size = let initial_window_size =
initial_window_size.at_most(largest_monitor_point_size(event_loop)); initial_window_size.at_most(largest_monitor_point_size(event_loop));
window_builder = window_builder.with_inner_size(points_to_size(initial_window_size)); viewport_builder = viewport_builder.with_inner_size(initial_window_size);
} }
*initial_window_size *initial_window_size
@ -169,19 +158,19 @@ pub fn window_builder<E>(
#[cfg(not(target_os = "ios"))] #[cfg(not(target_os = "ios"))]
if *centered { if *centered {
if let Some(monitor) = event_loop.available_monitors().next() { if let Some(monitor) = event_loop.available_monitors().next() {
let monitor_size = monitor.size().to_logical::<f64>(monitor.scale_factor()); let monitor_size = monitor.size().to_logical::<f32>(monitor.scale_factor());
let inner_size = inner_size_points.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 }); let inner_size = inner_size_points.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 });
if monitor_size.width > 0.0 && monitor_size.height > 0.0 { if monitor_size.width > 0.0 && monitor_size.height > 0.0 {
let x = (monitor_size.width - inner_size.x as f64) / 2.0; let x = (monitor_size.width - inner_size.x) / 2.0;
let y = (monitor_size.height - inner_size.y as f64) / 2.0; let y = (monitor_size.height - inner_size.y) / 2.0;
window_builder = window_builder.with_position(winit::dpi::LogicalPosition { x, y }); viewport_builder = viewport_builder.with_position([x, y]);
} }
} }
} }
match std::mem::take(&mut native_options.window_builder) { match std::mem::take(&mut native_options.window_builder) {
Some(hook) => hook(window_builder), Some(hook) => hook(viewport_builder),
None => window_builder, None => viewport_builder,
} }
} }
@ -219,34 +208,14 @@ fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui:
} }
} }
fn load_icon(icon_data: epi::IconData) -> Option<winit::window::Icon> {
winit::window::Icon::from_rgba(icon_data.rgba, icon_data.width, icon_data.height).ok()
}
#[cfg(target_os = "windows")]
fn window_builder_drag_and_drop(
window_builder: winit::window::WindowBuilder,
enable: bool,
) -> winit::window::WindowBuilder {
use winit::platform::windows::WindowBuilderExtWindows as _;
window_builder.with_drag_and_drop(enable)
}
#[cfg(not(target_os = "windows"))]
fn window_builder_drag_and_drop(
window_builder: winit::window::WindowBuilder,
_enable: bool,
) -> winit::window::WindowBuilder {
// drag and drop can only be disabled on windows
window_builder
}
pub fn handle_app_output( pub fn handle_app_output(
window: &winit::window::Window, window: &winit::window::Window,
current_pixels_per_point: f32, current_pixels_per_point: f32,
app_output: epi::backend::AppOutput, app_output: epi::backend::AppOutput,
window_state: &mut WindowState, window_state: &mut WindowState,
) { ) {
crate::profile_function!();
let epi::backend::AppOutput { let epi::backend::AppOutput {
close: _, close: _,
window_size, window_size,
@ -294,7 +263,7 @@ pub fn handle_app_output(
} }
if drag_window { if drag_window {
let _ = window.drag_window(); window.drag_window().ok();
} }
if let Some(always_on_top) = always_on_top { if let Some(always_on_top) = always_on_top {
@ -346,10 +315,11 @@ pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
/// Everything needed to make a winit-based integration for [`epi`]. /// Everything needed to make a winit-based integration for [`epi`].
pub struct EpiIntegration { pub struct EpiIntegration {
pub frame: epi::Frame, pub frame: epi::Frame,
last_auto_save: std::time::Instant, last_auto_save: Instant,
pub beginning: Instant,
pub frame_start: Instant,
pub egui_ctx: egui::Context, pub egui_ctx: egui::Context,
pending_full_output: egui::FullOutput, pending_full_output: egui::FullOutput,
egui_winit: egui_winit::State,
/// When set, it is time to close the native window. /// When set, it is time to close the native window.
close: bool, close: bool,
@ -364,18 +334,18 @@ pub struct EpiIntegration {
impl EpiIntegration { impl EpiIntegration {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new<E>( pub fn new(
event_loop: &EventLoopWindowTarget<E>,
max_texture_side: usize,
window: &winit::window::Window, window: &winit::window::Window,
system_theme: Option<Theme>, system_theme: Option<Theme>,
app_name: &str, app_name: &str,
native_options: &crate::NativeOptions, native_options: &crate::NativeOptions,
storage: Option<Box<dyn epi::Storage>>, storage: Option<Box<dyn epi::Storage>>,
is_desktop: bool,
#[cfg(feature = "glow")] gl: Option<std::sync::Arc<glow::Context>>, #[cfg(feature = "glow")] gl: Option<std::sync::Arc<glow::Context>>,
#[cfg(feature = "wgpu")] 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();
egui_ctx.set_embed_viewports(!is_desktop);
let memory = load_egui_memory(storage.as_deref()).unwrap_or_default(); let memory = load_egui_memory(storage.as_deref()).unwrap_or_default();
egui_ctx.memory_mut(|mem| *mem = memory); egui_ctx.memory_mut(|mem| *mem = memory);
@ -408,10 +378,6 @@ impl EpiIntegration {
raw_window_handle: window.raw_window_handle(), raw_window_handle: window.raw_window_handle(),
}; };
let mut egui_winit = egui_winit::State::new(event_loop);
egui_winit.set_max_texture_side(max_texture_side);
egui_winit.set_pixels_per_point(native_pixels_per_point);
let app_icon_setter = super::app_icon::AppTitleIconSetter::new( let app_icon_setter = super::app_icon::AppTitleIconSetter::new(
app_name.to_owned(), app_name.to_owned(),
native_options.icon_data.clone(), native_options.icon_data.clone(),
@ -419,9 +385,8 @@ impl EpiIntegration {
Self { Self {
frame, frame,
last_auto_save: std::time::Instant::now(), last_auto_save: Instant::now(),
egui_ctx, egui_ctx,
egui_winit,
pending_full_output: Default::default(), pending_full_output: Default::default(),
close: false, close: false,
can_drag_window: false, can_drag_window: false,
@ -430,34 +395,47 @@ impl EpiIntegration {
#[cfg(feature = "persistence")] #[cfg(feature = "persistence")]
persist_window: native_options.persist_window, persist_window: native_options.persist_window,
app_icon_setter, app_icon_setter,
beginning: Instant::now(),
frame_start: Instant::now(),
} }
} }
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
pub fn init_accesskit<E: From<accesskit_winit::ActionRequestEvent> + Send>( pub fn init_accesskit<E: From<egui_winit::accesskit_winit::ActionRequestEvent> + Send>(
&mut self, &mut self,
egui_winit: &mut egui_winit::State,
window: &winit::window::Window, window: &winit::window::Window,
event_loop_proxy: winit::event_loop::EventLoopProxy<E>, event_loop_proxy: winit::event_loop::EventLoopProxy<E>,
) { ) {
crate::profile_function!();
let egui_ctx = self.egui_ctx.clone(); let egui_ctx = self.egui_ctx.clone();
self.egui_winit egui_winit.init_accesskit(window, event_loop_proxy, move || {
.init_accesskit(window, event_loop_proxy, move || { // This function is called when an accessibility client
// This function is called when an accessibility client // (e.g. screen reader) makes its first request. If we got here,
// (e.g. screen reader) makes its first request. If we got here, // we know that an accessibility tree is actually wanted.
// we know that an accessibility tree is actually wanted. egui_ctx.enable_accesskit();
egui_ctx.enable_accesskit(); // Enqueue a repaint so we'll receive a full tree update soon.
// Enqueue a repaint so we'll receive a full tree update soon. egui_ctx.request_repaint();
egui_ctx.request_repaint(); egui_ctx.accesskit_placeholder_tree_update()
egui_ctx.accesskit_placeholder_tree_update() });
});
} }
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { pub fn warm_up(
&mut self,
app: &mut dyn epi::App,
window: &winit::window::Window,
egui_winit: &mut egui_winit::State,
) {
crate::profile_function!(); crate::profile_function!();
let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone()); let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone());
self.egui_ctx self.egui_ctx
.memory_mut(|mem| mem.set_everything_is_visible(true)); .memory_mut(|mem| mem.set_everything_is_visible(true));
let full_output = self.update(app, window);
let raw_input = egui_winit.take_egui_input(window, ViewportIdPair::ROOT);
self.pre_update(window);
let full_output = self.update(app, None, raw_input);
self.post_update(app, window);
self.pending_full_output.append(full_output); // Handle it next frame self.pending_full_output.append(full_output); // Handle it next frame
self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge. self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge.
self.egui_ctx.clear_animations(); self.egui_ctx.clear_animations();
@ -472,6 +450,8 @@ impl EpiIntegration {
&mut self, &mut self,
app: &mut dyn epi::App, app: &mut dyn epi::App,
event: &winit::event::WindowEvent<'_>, event: &winit::event::WindowEvent<'_>,
egui_winit: &mut egui_winit::State,
viewport_id: ViewportId,
) -> EventResponse { ) -> EventResponse {
crate::profile_function!(); crate::profile_function!();
@ -480,7 +460,7 @@ impl EpiIntegration {
match event { match event {
WindowEvent::CloseRequested => { WindowEvent::CloseRequested => {
log::debug!("Received WindowEvent::CloseRequested"); log::debug!("Received WindowEvent::CloseRequested");
self.close = app.on_close_event(); self.close = app.on_close_event() && viewport_id == ViewportId::ROOT;
log::debug!("App::on_close_event returned {}", self.close); log::debug!("App::on_close_event returned {}", self.close);
} }
WindowEvent::Destroyed => { WindowEvent::Destroyed => {
@ -503,37 +483,47 @@ impl EpiIntegration {
_ => {} _ => {}
} }
self.egui_winit.on_event(&self.egui_ctx, event) egui_winit.on_event(&self.egui_ctx, event)
} }
#[cfg(feature = "accesskit")] pub fn pre_update(&mut self, window: &winit::window::Window) {
pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) { self.frame_start = Instant::now();
self.egui_winit.on_accesskit_action_request(request);
self.app_icon_setter.update();
self.frame.info.window_info =
read_window_info(window, self.egui_ctx.pixels_per_point(), &self.window_state);
} }
/// Run user code - this can create immediate viewports, so hold no locks over this!
///
/// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::update`].
pub fn update( pub fn update(
&mut self, &mut self,
app: &mut dyn epi::App, app: &mut dyn epi::App,
window: &winit::window::Window, viewport_ui_cb: Option<&DeferredViewportUiCallback>,
mut raw_input: egui::RawInput,
) -> egui::FullOutput { ) -> egui::FullOutput {
let frame_start = std::time::Instant::now(); raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
self.app_icon_setter.update();
self.frame.info.window_info =
read_window_info(window, self.egui_ctx.pixels_per_point(), &self.window_state);
let raw_input = self.egui_winit.take_egui_input(window);
// Run user code:
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
crate::profile_scope!("App::update"); if let Some(viewport_ui_cb) = viewport_ui_cb {
app.update(egui_ctx, &mut self.frame); // Child viewport
crate::profile_scope!("viewport_callback");
viewport_ui_cb(egui_ctx);
} else {
// Root viewport
crate::profile_scope!("App::update");
app.update(egui_ctx, &mut self.frame);
}
}); });
self.pending_full_output.append(full_output); self.pending_full_output.append(full_output);
let full_output = std::mem::take(&mut self.pending_full_output); std::mem::take(&mut self.pending_full_output)
}
{ pub fn post_update(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
let app_output = {
let mut app_output = self.frame.take_app_output(); let mut app_output = self.frame.take_app_output();
app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108 app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108
self.can_drag_window = false; self.can_drag_window = false;
@ -546,29 +536,30 @@ impl EpiIntegration {
if self.frame.output.attention.is_some() { if self.frame.output.attention.is_some() {
self.frame.output.attention = None; self.frame.output.attention = None;
} }
handle_app_output( app_output
window, };
self.egui_ctx.pixels_per_point(),
app_output,
&mut self.window_state,
);
}
let frame_time = frame_start.elapsed().as_secs_f64() as f32; handle_app_output(
self.frame.info.cpu_usage = Some(frame_time); window,
self.egui_ctx.pixels_per_point(),
app_output,
&mut self.window_state,
);
full_output let frame_time = self.frame_start.elapsed().as_secs_f64() as f32;
self.frame.info.cpu_usage = Some(frame_time);
} }
pub fn post_rendering(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { pub fn post_rendering(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
crate::profile_function!();
let inner_size = window.inner_size(); let inner_size = window.inner_size();
let window_size_px = [inner_size.width, inner_size.height]; let window_size_px = [inner_size.width, inner_size.height];
app.post_rendering(window_size_px, &self.frame); app.post_rendering(window_size_px, &self.frame);
} }
pub fn post_present(&mut self, window: &winit::window::Window) { pub fn post_present(&mut self, window: &winit::window::Window) {
if let Some(visible) = self.frame.output.visible.take() { if let Some(visible) = self.frame.output.visible.take() {
crate::profile_scope!("window.set_visible");
window.set_visible(visible); window.set_visible(visible);
} }
} }
@ -576,19 +567,24 @@ impl EpiIntegration {
pub fn handle_platform_output( pub fn handle_platform_output(
&mut self, &mut self,
window: &winit::window::Window, window: &winit::window::Window,
viewport_id: ViewportId,
platform_output: egui::PlatformOutput, platform_output: egui::PlatformOutput,
egui_winit: &mut egui_winit::State,
) { ) {
self.egui_winit egui_winit.handle_platform_output(window, viewport_id, &self.egui_ctx, platform_output);
.handle_platform_output(window, &self.egui_ctx, platform_output);
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Persistence stuff: // Persistence stuff:
pub fn maybe_autosave(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { pub fn maybe_autosave(
let now = std::time::Instant::now(); &mut self,
app: &mut dyn epi::App,
window: Option<&winit::window::Window>,
) {
let now = Instant::now();
if now - self.last_auto_save > app.auto_save_interval() { if now - self.last_auto_save > app.auto_save_interval() {
self.save(app, Some(window)); self.save(app, window);
self.last_auto_save = now; self.last_auto_save = now;
} }
} }

2659
crates/eframe/src/native/run.rs

File diff suppressed because it is too large

18
crates/eframe/src/web/app_runner.rs

@ -94,7 +94,7 @@ impl AppRunner {
{ {
let needs_repaint = needs_repaint.clone(); let needs_repaint = needs_repaint.clone();
egui_ctx.set_request_repaint_callback(move |info| { egui_ctx.set_request_repaint_callback(move |info| {
needs_repaint.repaint_after(info.after.as_secs_f64()); needs_repaint.repaint_after(info.delay.as_secs_f64());
}); });
} }
@ -170,10 +170,8 @@ impl AppRunner {
self.painter.destroy(); self.painter.destroy();
} }
/// Returns how long to wait until the next repaint.
///
/// Call [`Self::paint`] later to paint /// Call [`Self::paint`] later to paint
pub fn logic(&mut self) -> (std::time::Duration, Vec<egui::ClippedPrimitive>) { pub fn logic(&mut self) -> Vec<egui::ClippedPrimitive> {
let frame_start = now_sec(); let frame_start = now_sec();
super::resize_canvas_to_screen_size(self.canvas_id(), self.web_options.max_size_points); super::resize_canvas_to_screen_size(self.canvas_id(), self.web_options.max_size_points);
@ -185,14 +183,20 @@ impl AppRunner {
}); });
let egui::FullOutput { let egui::FullOutput {
platform_output, platform_output,
repaint_after,
textures_delta, textures_delta,
shapes, shapes,
pixels_per_point,
viewport_output,
} = full_output; } = full_output;
if viewport_output.len() > 1 {
log::warn!("Multiple viewports not yet supported on the web");
}
// TODO(emilk): handle some of the command in `viewport_output`, like setting the title and icon?
self.handle_platform_output(platform_output); self.handle_platform_output(platform_output);
self.textures_delta.append(textures_delta); self.textures_delta.append(textures_delta);
let clipped_primitives = self.egui_ctx.tessellate(shapes); let clipped_primitives = self.egui_ctx.tessellate(shapes, pixels_per_point);
{ {
let app_output = self.frame.take_app_output(); let app_output = self.frame.take_app_output();
@ -201,7 +205,7 @@ impl AppRunner {
self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32); self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32);
(repaint_after, clipped_primitives) clipped_primitives
} }
/// Paint the results of the last call to [`Self::logic`]. /// Paint the results of the last call to [`Self::logic`].

5
crates/eframe/src/web/events.rs

@ -19,11 +19,8 @@ fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> {
fn paint_if_needed(runner: &mut AppRunner) -> Result<(), JsValue> { fn paint_if_needed(runner: &mut AppRunner) -> Result<(), JsValue> {
if runner.needs_repaint.when_to_repaint() <= now_sec() { if runner.needs_repaint.when_to_repaint() <= now_sec() {
runner.needs_repaint.clear(); runner.needs_repaint.clear();
let (repaint_after, clipped_primitives) = runner.logic(); let clipped_primitives = runner.logic();
runner.paint(&clipped_primitives)?; runner.paint(&clipped_primitives)?;
runner
.needs_repaint
.repaint_after(repaint_after.as_secs_f64());
runner.auto_save_if_needed(); runner.auto_save_if_needed();
} }
Ok(()) Ok(())

11
crates/eframe/src/web/web_painter_wgpu.rs

@ -208,8 +208,7 @@ impl WebPainter for WebPainterWgpu {
let frame = match self.surface.get_current_texture() { let frame = match self.surface.get_current_texture() {
Ok(frame) => frame, Ok(frame) => frame,
#[allow(clippy::single_match_else)] Err(err) => match (*self.on_surface_error)(err) {
Err(e) => match (*self.on_surface_error)(e) {
SurfaceErrorAction::RecreateSurface => { SurfaceErrorAction::RecreateSurface => {
self.surface self.surface
.configure(&render_state.device, &self.surface_configuration); .configure(&render_state.device, &self.surface_configuration);
@ -271,11 +270,9 @@ impl WebPainter for WebPainterWgpu {
} }
// Submit the commands: both the main buffer and user-defined ones. // Submit the commands: both the main buffer and user-defined ones.
render_state.queue.submit( render_state
user_cmd_bufs .queue
.into_iter() .submit(user_cmd_bufs.into_iter().chain([encoder.finish()]));
.chain(std::iter::once(encoder.finish())),
);
if let Some(frame) = frame { if let Some(frame) = frame {
frame.present(); frame.present();

1
crates/egui-wgpu/Cargo.toml

@ -36,6 +36,7 @@ winit = ["dep:winit"]
[dependencies] [dependencies]
egui = { version = "0.23.0", path = "../egui", default-features = false }
epaint = { version = "0.23.0", path = "../epaint", default-features = false, features = [ epaint = { version = "0.23.0", path = "../epaint", default-features = false, features = [
"bytemuck", "bytemuck",
] } ] }

167
crates/egui-wgpu/src/winit.rs

@ -1,4 +1,6 @@
use std::sync::Arc; use std::{num::NonZeroU32, sync::Arc};
use egui::{ViewportId, ViewportIdMap, ViewportIdSet};
use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration}; use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration};
@ -77,13 +79,15 @@ pub struct Painter {
msaa_samples: u32, msaa_samples: u32,
support_transparent_backbuffer: bool, support_transparent_backbuffer: bool,
depth_format: Option<wgpu::TextureFormat>, depth_format: Option<wgpu::TextureFormat>,
depth_texture_view: Option<wgpu::TextureView>,
msaa_texture_view: Option<wgpu::TextureView>,
screen_capture_state: Option<CaptureState>, screen_capture_state: Option<CaptureState>,
instance: wgpu::Instance, instance: wgpu::Instance,
render_state: Option<RenderState>, render_state: Option<RenderState>,
surface_state: Option<SurfaceState>,
// Per viewport/window:
depth_texture_view: ViewportIdMap<wgpu::TextureView>,
msaa_texture_view: ViewportIdMap<wgpu::TextureView>,
surfaces: ViewportIdMap<SurfaceState>,
} }
impl Painter { impl Painter {
@ -115,13 +119,14 @@ impl Painter {
msaa_samples, msaa_samples,
support_transparent_backbuffer, support_transparent_backbuffer,
depth_format, depth_format,
depth_texture_view: None,
screen_capture_state: None, screen_capture_state: None,
instance, instance,
render_state: None, render_state: None,
surface_state: None,
msaa_texture_view: None, depth_texture_view: Default::default(),
surfaces: Default::default(),
msaa_texture_view: Default::default(),
} }
} }
@ -180,11 +185,14 @@ impl Painter {
/// If the provided wgpu configuration does not match an available device. /// If the provided wgpu configuration does not match an available device.
pub async fn set_window( pub async fn set_window(
&mut self, &mut self,
viewport_id: ViewportId,
window: Option<&winit::window::Window>, window: Option<&winit::window::Window>,
) -> Result<(), crate::WgpuError> { ) -> Result<(), crate::WgpuError> {
crate::profile_function!(); crate::profile_function!();
match window {
Some(window) => { if let Some(window) = window {
let size = window.inner_size();
if self.surfaces.get(&viewport_id).is_none() {
let surface = unsafe { self.instance.create_surface(&window)? }; let surface = unsafe { self.instance.create_surface(&window)? };
let render_state = if let Some(render_state) = &self.render_state { let render_state = if let Some(render_state) = &self.render_state {
@ -223,19 +231,30 @@ impl Painter {
let supports_screenshot = let supports_screenshot =
!matches!(render_state.adapter.get_info().backend, wgpu::Backend::Gl); !matches!(render_state.adapter.get_info().backend, wgpu::Backend::Gl);
let size = window.inner_size(); self.surfaces.insert(
self.surface_state = Some(SurfaceState { viewport_id,
surface, SurfaceState {
width: size.width, surface,
height: size.height, width: size.width,
alpha_mode, height: size.height,
supports_screenshot, alpha_mode,
}); supports_screenshot,
self.resize_and_generate_depth_texture_view_and_msaa_view(size.width, size.height); },
} );
None => {
self.surface_state = None;
} }
let Some(width) = NonZeroU32::new(size.width) else {
log::debug!("The window width was zero; skipping generate textures");
return Ok(());
};
let Some(height) = NonZeroU32::new(size.height) else {
log::debug!("The window height was zero; skipping generate textures");
return Ok(());
};
self.resize_and_generate_depth_texture_view_and_msaa_view(viewport_id, width, height);
} else {
log::warn!("All surfaces was deleted!");
self.surfaces.clear();
} }
Ok(()) Ok(())
} }
@ -253,51 +272,61 @@ impl Painter {
fn resize_and_generate_depth_texture_view_and_msaa_view( fn resize_and_generate_depth_texture_view_and_msaa_view(
&mut self, &mut self,
width_in_pixels: u32, viewport_id: ViewportId,
height_in_pixels: u32, width_in_pixels: NonZeroU32,
height_in_pixels: NonZeroU32,
) { ) {
crate::profile_function!(); crate::profile_function!();
let width = width_in_pixels.get();
let height = height_in_pixels.get();
let render_state = self.render_state.as_ref().unwrap(); let render_state = self.render_state.as_ref().unwrap();
let surface_state = self.surface_state.as_mut().unwrap(); let surface_state = self.surfaces.get_mut(&viewport_id).unwrap();
surface_state.width = width_in_pixels; surface_state.width = width;
surface_state.height = height_in_pixels; surface_state.height = height;
Self::configure_surface(surface_state, render_state, self.configuration.present_mode); Self::configure_surface(surface_state, render_state, self.configuration.present_mode);
self.depth_texture_view = self.depth_format.map(|depth_format| { if let Some(depth_format) = self.depth_format {
render_state self.depth_texture_view.insert(
.device viewport_id,
.create_texture(&wgpu::TextureDescriptor { render_state
label: Some("egui_depth_texture"), .device
size: wgpu::Extent3d { .create_texture(&wgpu::TextureDescriptor {
width: width_in_pixels, label: Some("egui_depth_texture"),
height: height_in_pixels, size: wgpu::Extent3d {
depth_or_array_layers: 1, width,
}, height,
mip_level_count: 1, depth_or_array_layers: 1,
sample_count: self.msaa_samples, },
dimension: wgpu::TextureDimension::D2, mip_level_count: 1,
format: depth_format, sample_count: self.msaa_samples,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT dimension: wgpu::TextureDimension::D2,
| wgpu::TextureUsages::TEXTURE_BINDING, format: depth_format,
view_formats: &[depth_format], usage: wgpu::TextureUsages::RENDER_ATTACHMENT
}) | wgpu::TextureUsages::TEXTURE_BINDING,
.create_view(&wgpu::TextureViewDescriptor::default()) view_formats: &[depth_format],
}); })
.create_view(&wgpu::TextureViewDescriptor::default()),
);
}
self.msaa_texture_view = (self.msaa_samples > 1) if let Some(render_state) = (self.msaa_samples > 1)
.then_some(self.render_state.as_ref()) .then_some(self.render_state.as_ref())
.flatten() .flatten()
.map(|render_state| { {
let texture_format = render_state.target_format; let texture_format = render_state.target_format;
self.msaa_texture_view.insert(
viewport_id,
render_state render_state
.device .device
.create_texture(&wgpu::TextureDescriptor { .create_texture(&wgpu::TextureDescriptor {
label: Some("egui_msaa_texture"), label: Some("egui_msaa_texture"),
size: wgpu::Extent3d { size: wgpu::Extent3d {
width: width_in_pixels, width,
height: height_in_pixels, height,
depth_or_array_layers: 1, depth_or_array_layers: 1,
}, },
mip_level_count: 1, mip_level_count: 1,
@ -307,14 +336,22 @@ impl Painter {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT, usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[texture_format], view_formats: &[texture_format],
}) })
.create_view(&wgpu::TextureViewDescriptor::default()) .create_view(&wgpu::TextureViewDescriptor::default()),
}); );
};
} }
pub fn on_window_resized(&mut self, width_in_pixels: u32, height_in_pixels: u32) { pub fn on_window_resized(
&mut self,
viewport_id: ViewportId,
width_in_pixels: NonZeroU32,
height_in_pixels: NonZeroU32,
) {
crate::profile_function!(); crate::profile_function!();
if self.surface_state.is_some() {
if self.surfaces.contains_key(&viewport_id) {
self.resize_and_generate_depth_texture_view_and_msaa_view( self.resize_and_generate_depth_texture_view_and_msaa_view(
viewport_id,
width_in_pixels, width_in_pixels,
height_in_pixels, height_in_pixels,
); );
@ -425,6 +462,7 @@ impl Painter {
// Returns a vector with the frame's pixel data if it was requested. // Returns a vector with the frame's pixel data if it was requested.
pub fn paint_and_update_textures( pub fn paint_and_update_textures(
&mut self, &mut self,
viewport_id: ViewportId,
pixels_per_point: f32, pixels_per_point: f32,
clear_color: [f32; 4], clear_color: [f32; 4],
clipped_primitives: &[epaint::ClippedPrimitive], clipped_primitives: &[epaint::ClippedPrimitive],
@ -434,7 +472,7 @@ impl Painter {
crate::profile_function!(); crate::profile_function!();
let render_state = self.render_state.as_mut()?; let render_state = self.render_state.as_mut()?;
let surface_state = self.surface_state.as_ref()?; let surface_state = self.surfaces.get(&viewport_id)?;
let output_frame = { let output_frame = {
crate::profile_scope!("get_current_texture"); crate::profile_scope!("get_current_texture");
@ -444,8 +482,7 @@ impl Painter {
let output_frame = match output_frame { let output_frame = match output_frame {
Ok(frame) => frame, Ok(frame) => frame,
#[allow(clippy::single_match_else)] Err(err) => match (*self.configuration.on_surface_error)(err) {
Err(e) => match (*self.configuration.on_surface_error)(e) {
SurfaceErrorAction::RecreateSurface => { SurfaceErrorAction::RecreateSurface => {
Self::configure_surface( Self::configure_surface(
surface_state, surface_state,
@ -521,7 +558,7 @@ impl Painter {
}; };
let (view, resolve_target) = (self.msaa_samples > 1) let (view, resolve_target) = (self.msaa_samples > 1)
.then_some(self.msaa_texture_view.as_ref()) .then_some(self.msaa_texture_view.get(&viewport_id))
.flatten() .flatten()
.map_or((&frame_view, None), |texture_view| { .map_or((&frame_view, None), |texture_view| {
(texture_view, Some(&frame_view)) (texture_view, Some(&frame_view))
@ -542,7 +579,7 @@ impl Painter {
store: wgpu::StoreOp::Store, store: wgpu::StoreOp::Store,
}, },
})], })],
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| { depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
wgpu::RenderPassDepthStencilAttachment { wgpu::RenderPassDepthStencilAttachment {
view, view,
depth_ops: Some(wgpu::Operations { depth_ops: Some(wgpu::Operations {
@ -578,7 +615,7 @@ impl Painter {
crate::profile_scope!("Queue::submit"); crate::profile_scope!("Queue::submit");
render_state render_state
.queue .queue
.submit(user_cmd_bufs.into_iter().chain(std::iter::once(encoded))); .submit(user_cmd_bufs.into_iter().chain([encoded]));
}; };
let screenshot = if capture { let screenshot = if capture {
@ -595,6 +632,14 @@ impl Painter {
screenshot screenshot
} }
pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) {
self.surfaces.retain(|id, _| active_viewports.contains(id));
self.depth_texture_view
.retain(|id, _| active_viewports.contains(id));
self.msaa_texture_view
.retain(|id, _| active_viewports.contains(id));
}
#[allow(clippy::unused_self)] #[allow(clippy::unused_self)]
pub fn destroy(&mut self) { pub fn destroy(&mut self) {
// TODO(emilk): something here? // TODO(emilk): something here?

4
crates/egui-winit/Cargo.toml

@ -49,10 +49,10 @@ puffin = ["dep:puffin", "egui/puffin"]
serde = ["egui/serde", "dep:serde"] serde = ["egui/serde", "dep:serde"]
## Enables Wayland support. ## Enables Wayland support.
wayland = ["winit/wayland"] wayland = ["winit/wayland", "bytemuck"]
## Enables compiling for x11. ## Enables compiling for x11.
x11 = ["winit/x11"] x11 = ["winit/x11", "bytemuck"]
[dependencies] [dependencies]
egui = { version = "0.23.0", path = "../egui", default-features = false, features = [ egui = { version = "0.23.0", path = "../egui", default-features = false, features = [

4
crates/egui-winit/src/clipboard.rs

@ -112,6 +112,8 @@ impl Clipboard {
#[cfg(all(feature = "arboard", not(target_os = "android")))] #[cfg(all(feature = "arboard", not(target_os = "android")))]
fn init_arboard() -> Option<arboard::Clipboard> { fn init_arboard() -> Option<arboard::Clipboard> {
crate::profile_function!();
log::debug!("Initializing arboard clipboard…"); log::debug!("Initializing arboard clipboard…");
match arboard::Clipboard::new() { match arboard::Clipboard::new() {
Ok(clipboard) => Some(clipboard), Ok(clipboard) => Some(clipboard),
@ -135,6 +137,8 @@ fn init_arboard() -> Option<arboard::Clipboard> {
fn init_smithay_clipboard( fn init_smithay_clipboard(
_display_target: &dyn HasRawDisplayHandle, _display_target: &dyn HasRawDisplayHandle,
) -> Option<smithay_clipboard::Clipboard> { ) -> Option<smithay_clipboard::Clipboard> {
crate::profile_function!();
use raw_window_handle::RawDisplayHandle; use raw_window_handle::RawDisplayHandle;
if let RawDisplayHandle::Wayland(display) = _display_target.raw_display_handle() { if let RawDisplayHandle::Wayland(display) = _display_target.raw_display_handle() {
log::debug!("Initializing smithay clipboard…"); log::debug!("Initializing smithay clipboard…");

366
crates/egui-winit/src/lib.rs

@ -14,6 +14,7 @@ pub use accesskit_winit;
pub use egui; pub use egui;
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
use egui::accesskit; use egui::accesskit;
use egui::{Pos2, Rect, Vec2, ViewportBuilder, ViewportCommand, ViewportId, ViewportIdPair};
pub use winit; pub use winit;
pub mod clipboard; pub mod clipboard;
@ -51,7 +52,9 @@ pub struct EventResponse {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Handles the integration between egui and winit. /// Handles the integration between egui and a winit Window.
///
/// Instantiate one of these per viewport/window.
pub struct State { pub struct State {
start_time: web_time::Instant, start_time: web_time::Instant,
egui_input: egui::RawInput, egui_input: egui::RawInput,
@ -86,13 +89,19 @@ pub struct State {
impl State { impl State {
/// Construct a new instance /// Construct a new instance
pub fn new(display_target: &dyn HasRawDisplayHandle) -> Self { pub fn new(
display_target: &dyn HasRawDisplayHandle,
native_pixels_per_point: Option<f32>,
max_texture_side: Option<usize>,
) -> Self {
crate::profile_function!();
let egui_input = egui::RawInput { let egui_input = egui::RawInput {
focused: false, // winit will tell us when we have focus focused: false, // winit will tell us when we have focus
..Default::default() ..Default::default()
}; };
Self { let mut slf = Self {
start_time: web_time::Instant::now(), start_time: web_time::Instant::now(),
egui_input, egui_input,
pointer_pos_in_points: None, pointer_pos_in_points: None,
@ -111,7 +120,14 @@ impl State {
accesskit: None, accesskit: None,
allow_ime: false, allow_ime: false,
};
if let Some(native_pixels_per_point) = native_pixels_per_point {
slf.set_pixels_per_point(native_pixels_per_point);
}
if let Some(max_texture_side) = max_texture_side {
slf.set_max_texture_side(max_texture_side);
} }
slf
} }
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
@ -121,6 +137,7 @@ impl State {
event_loop_proxy: winit::event_loop::EventLoopProxy<T>, event_loop_proxy: winit::event_loop::EventLoopProxy<T>,
initial_tree_update_factory: impl 'static + FnOnce() -> accesskit::TreeUpdate + Send, initial_tree_update_factory: impl 'static + FnOnce() -> accesskit::TreeUpdate + Send,
) { ) {
crate::profile_function!();
self.accesskit = Some(accesskit_winit::Adapter::new( self.accesskit = Some(accesskit_winit::Adapter::new(
window, window,
initial_tree_update_factory, initial_tree_update_factory,
@ -163,22 +180,81 @@ impl State {
/// Prepare for a new frame by extracting the accumulated input, /// Prepare for a new frame by extracting the accumulated input,
/// as well as setting [the time](egui::RawInput::time) and [screen rectangle](egui::RawInput::screen_rect). /// as well as setting [the time](egui::RawInput::time) and [screen rectangle](egui::RawInput::screen_rect).
pub fn take_egui_input(&mut self, window: &winit::window::Window) -> egui::RawInput { pub fn take_egui_input(
&mut self,
window: &winit::window::Window,
ids: ViewportIdPair,
) -> egui::RawInput {
crate::profile_function!();
let pixels_per_point = self.pixels_per_point(); let pixels_per_point = self.pixels_per_point();
self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64()); self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
// TODO remove this in winit 0.29
// This hack make the window outer_position and size to be valid, X11 Only
// That was happending because winit get the window state before the compositor adds decorations!
#[cfg(all(feature = "x11", target_os = "linux"))]
window.set_maximized(window.is_maximized());
// On Windows, a minimized window will have 0 width and height. // On Windows, a minimized window will have 0 width and height.
// See: https://github.com/rust-windowing/winit/issues/208 // See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where egui window positions would be changed when minimizing on Windows. // This solves an issue where egui window positions would be changed when minimizing on Windows.
let screen_size_in_pixels = screen_size_in_pixels(window); let screen_size_in_pixels = screen_size_in_pixels(window);
let screen_size_in_points = screen_size_in_pixels / pixels_per_point; let screen_size_in_points = screen_size_in_pixels / pixels_per_point;
self.egui_input.screen_rect =
if screen_size_in_points.x > 0.0 && screen_size_in_points.y > 0.0 { self.egui_input.screen_rect = (screen_size_in_points.x > 0.0
Some(egui::Rect::from_min_size( && screen_size_in_points.y > 0.0)
egui::Pos2::ZERO, .then(|| Rect::from_min_size(Pos2::ZERO, screen_size_in_points));
screen_size_in_points,
)) let has_a_position = match window.is_minimized() {
None | Some(true) => false,
Some(false) => true,
};
let inner_pos_px = if has_a_position {
window
.inner_position()
.map(|pos| Pos2::new(pos.x as f32, pos.y as f32))
.ok()
} else {
None
};
let outer_pos_px = if has_a_position {
window
.outer_position()
.map(|pos| Pos2::new(pos.x as f32, pos.y as f32))
.ok()
} else {
None
};
let inner_size_px = if has_a_position {
let size = window.inner_size();
Some(Vec2::new(size.width as f32, size.height as f32))
} else {
None
};
let outer_size_px = if has_a_position {
let size = window.outer_size();
Some(Vec2::new(size.width as f32, size.height as f32))
} else {
None
};
self.egui_input.viewport.ids = ids;
self.egui_input.viewport.inner_rect_px =
if let (Some(pos), Some(size)) = (inner_pos_px, inner_size_px) {
Some(Rect::from_min_size(pos, size))
} else {
None
};
self.egui_input.viewport.outer_rect_px =
if let (Some(pos), Some(size)) = (outer_pos_px, outer_size_px) {
Some(Rect::from_min_size(pos, size))
} else { } else {
None None
}; };
@ -376,11 +452,18 @@ impl State {
} }
// Things that may require repaint: // Things that may require repaint:
WindowEvent::CloseRequested WindowEvent::CloseRequested => {
| WindowEvent::CursorEntered { .. } self.egui_input.viewport.close_requested = true;
EventResponse {
consumed: true,
repaint: true,
}
}
WindowEvent::CursorEntered { .. }
| WindowEvent::Destroyed | WindowEvent::Destroyed
| WindowEvent::Occluded(_) | WindowEvent::Occluded(_)
| WindowEvent::Resized(_) | WindowEvent::Resized(_)
| WindowEvent::Moved(_)
| WindowEvent::ThemeChanged(_) | WindowEvent::ThemeChanged(_)
| WindowEvent::TouchpadPressure { .. } => EventResponse { | WindowEvent::TouchpadPressure { .. } => EventResponse {
repaint: true, repaint: true,
@ -389,7 +472,6 @@ impl State {
// Things we completely ignore: // Things we completely ignore:
WindowEvent::AxisMotion { .. } WindowEvent::AxisMotion { .. }
| WindowEvent::Moved(_)
| WindowEvent::SmartMagnify { .. } | WindowEvent::SmartMagnify { .. }
| WindowEvent::TouchpadRotate { .. } => EventResponse { | WindowEvent::TouchpadRotate { .. } => EventResponse {
repaint: false, repaint: false,
@ -643,20 +725,24 @@ impl State {
pub fn handle_platform_output( pub fn handle_platform_output(
&mut self, &mut self,
window: &winit::window::Window, window: &winit::window::Window,
viewport_id: ViewportId,
egui_ctx: &egui::Context, egui_ctx: &egui::Context,
platform_output: egui::PlatformOutput, platform_output: egui::PlatformOutput,
) { ) {
crate::profile_function!();
let egui::PlatformOutput { let egui::PlatformOutput {
cursor_icon, cursor_icon,
open_url, open_url,
copied_text, copied_text,
events: _, // handled above events: _, // handled elsewhere
mutable_text_under_cursor: _, // only used in eframe web mutable_text_under_cursor: _, // only used in eframe web
text_cursor_pos, text_cursor_pos,
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
accesskit_update, accesskit_update,
} = platform_output; } = platform_output;
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
self.current_pixels_per_point = egui_ctx.input_for(viewport_id, |i| i.pixels_per_point); // someone can have changed it to scale the UI
self.set_cursor_icon(window, cursor_icon); self.set_cursor_icon(window, cursor_icon);
@ -905,6 +991,252 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option<winit::window::Curs
} }
} }
// Helpers for egui Viewports
// ---------------------------------------------------------------------------
pub fn process_viewport_commands(
commands: impl IntoIterator<Item = ViewportCommand>,
window: &winit::window::Window,
is_viewport_focused: bool,
) {
crate::profile_function!();
use winit::window::ResizeDirection;
for command in commands {
match command {
egui::ViewportCommand::StartDrag => {
// if this is not checked on x11 the input will be permanently taken until the app is killed!
if is_viewport_focused {
if let Err(err) = window.drag_window() {
log::warn!("{command:?}: {err}");
}
}
}
egui::ViewportCommand::InnerSize(size) => {
let width = size.x.max(1.0);
let height = size.y.max(1.0);
window.set_inner_size(LogicalSize::new(width, height));
}
egui::ViewportCommand::BeginResize(direction) => {
if let Err(err) = window.drag_resize_window(match direction {
egui::viewport::ResizeDirection::North => ResizeDirection::North,
egui::viewport::ResizeDirection::South => ResizeDirection::South,
egui::viewport::ResizeDirection::West => ResizeDirection::West,
egui::viewport::ResizeDirection::NorthEast => ResizeDirection::NorthEast,
egui::viewport::ResizeDirection::SouthEast => ResizeDirection::SouthEast,
egui::viewport::ResizeDirection::NorthWest => ResizeDirection::NorthWest,
egui::viewport::ResizeDirection::SouthWest => ResizeDirection::SouthWest,
}) {
log::warn!("{command:?}: {err}");
}
}
ViewportCommand::Title(title) => window.set_title(&title),
ViewportCommand::Transparent(v) => window.set_transparent(v),
ViewportCommand::Visible(v) => window.set_visible(v),
ViewportCommand::OuterPosition(pos) => {
window.set_outer_position(LogicalPosition::new(pos.x, pos.y));
}
ViewportCommand::MinInnerSize(s) => {
window.set_min_inner_size(
(s.is_finite() && s != Vec2::ZERO).then_some(LogicalSize::new(s.x, s.y)),
);
}
ViewportCommand::MaxInnerSize(s) => {
window.set_max_inner_size(
(s.is_finite() && s != Vec2::INFINITY).then_some(LogicalSize::new(s.x, s.y)),
);
}
ViewportCommand::ResizeIncrements(s) => {
window.set_resize_increments(s.map(|s| LogicalSize::new(s.x, s.y)));
}
ViewportCommand::Resizable(v) => window.set_resizable(v),
ViewportCommand::EnableButtons {
close,
minimized,
maximize,
} => window.set_enabled_buttons(
if close {
WindowButtons::CLOSE
} else {
WindowButtons::empty()
} | if minimized {
WindowButtons::MINIMIZE
} else {
WindowButtons::empty()
} | if maximize {
WindowButtons::MAXIMIZE
} else {
WindowButtons::empty()
},
),
ViewportCommand::Minimized(v) => window.set_minimized(v),
ViewportCommand::Maximized(v) => window.set_maximized(v),
ViewportCommand::Fullscreen(v) => {
window.set_fullscreen(v.then_some(winit::window::Fullscreen::Borderless(None)));
}
ViewportCommand::Decorations(v) => window.set_decorations(v),
ViewportCommand::WindowLevel(l) => window.set_window_level(match l {
egui::viewport::WindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom,
egui::viewport::WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop,
egui::viewport::WindowLevel::Normal => WindowLevel::Normal,
}),
ViewportCommand::WindowIcon(icon) => {
window.set_window_icon(icon.map(|icon| {
winit::window::Icon::from_rgba(
icon.as_raw().to_owned(),
icon.width() as u32,
icon.height() as u32,
)
.expect("Invalid ICON data!")
}));
}
ViewportCommand::IMEPosition(pos) => {
window.set_ime_position(LogicalPosition::new(pos.x, pos.y));
}
ViewportCommand::IMEAllowed(v) => window.set_ime_allowed(v),
ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(match p {
egui::viewport::IMEPurpose::Password => winit::window::ImePurpose::Password,
egui::viewport::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal,
egui::viewport::IMEPurpose::Normal => winit::window::ImePurpose::Normal,
}),
ViewportCommand::RequestUserAttention(a) => {
window.request_user_attention(a.map(|a| match a {
egui::viewport::UserAttentionType::Critical => {
winit::window::UserAttentionType::Critical
}
egui::viewport::UserAttentionType::Informational => {
winit::window::UserAttentionType::Informational
}
}));
}
ViewportCommand::SetTheme(t) => window.set_theme(match t {
egui::SystemTheme::Light => Some(winit::window::Theme::Light),
egui::SystemTheme::Dark => Some(winit::window::Theme::Dark),
egui::SystemTheme::SystemDefault => None,
}),
ViewportCommand::ContentProtected(v) => window.set_content_protected(v),
ViewportCommand::CursorPosition(pos) => {
if let Err(err) = window.set_cursor_position(LogicalPosition::new(pos.x, pos.y)) {
log::warn!("{command:?}: {err}");
}
}
ViewportCommand::CursorGrab(o) => {
if let Err(err) = window.set_cursor_grab(match o {
egui::viewport::CursorGrab::None => CursorGrabMode::None,
egui::viewport::CursorGrab::Confined => CursorGrabMode::Confined,
egui::viewport::CursorGrab::Locked => CursorGrabMode::Locked,
}) {
log::warn!("{command:?}: {err}");
}
}
ViewportCommand::CursorVisible(v) => window.set_cursor_visible(v),
ViewportCommand::CursorHitTest(v) => {
if let Err(err) = window.set_cursor_hittest(v) {
log::warn!("{command:?}: {err}");
}
}
}
}
}
pub fn create_winit_window_builder(builder: &ViewportBuilder) -> winit::window::WindowBuilder {
crate::profile_function!();
let mut window_builder = winit::window::WindowBuilder::new()
.with_title(
builder
.title
.clone()
.unwrap_or_else(|| "egui window".to_owned()),
)
.with_transparent(builder.transparent.unwrap_or(false))
.with_decorations(builder.decorations.unwrap_or(true))
.with_resizable(builder.resizable.unwrap_or(true))
.with_visible(builder.visible.unwrap_or(true))
.with_maximized(builder.maximized.unwrap_or(false))
.with_fullscreen(
builder
.fullscreen
.and_then(|e| e.then_some(winit::window::Fullscreen::Borderless(None))),
)
.with_enabled_buttons({
let mut buttons = WindowButtons::empty();
if builder.minimize_button.unwrap_or(true) {
buttons |= WindowButtons::MINIMIZE;
}
if builder.maximize_button.unwrap_or(true) {
buttons |= WindowButtons::MAXIMIZE;
}
if builder.close_button.unwrap_or(true) {
buttons |= WindowButtons::CLOSE;
}
buttons
})
.with_active(builder.active.unwrap_or(true));
if let Some(inner_size) = builder.inner_size {
window_builder = window_builder
.with_inner_size(winit::dpi::LogicalSize::new(inner_size.x, inner_size.y));
}
if let Some(min_inner_size) = builder.min_inner_size {
window_builder = window_builder.with_min_inner_size(winit::dpi::LogicalSize::new(
min_inner_size.x,
min_inner_size.y,
));
}
if let Some(max_inner_size) = builder.max_inner_size {
window_builder = window_builder.with_max_inner_size(winit::dpi::LogicalSize::new(
max_inner_size.x,
max_inner_size.y,
));
}
if let Some(position) = builder.position {
window_builder =
window_builder.with_position(winit::dpi::LogicalPosition::new(position.x, position.y));
}
if let Some(icon) = builder.icon.clone() {
window_builder = window_builder.with_window_icon(Some(
winit::window::Icon::from_rgba(
icon.as_raw().to_owned(),
icon.width() as u32,
icon.height() as u32,
)
.expect("Invalid Icon Data!"),
));
}
#[cfg(all(feature = "wayland", target_os = "linux"))]
if let Some(name) = builder.name.clone() {
use winit::platform::wayland::WindowBuilderExtWayland as _;
window_builder = window_builder.with_name(name.0, name.1);
}
#[cfg(target_os = "windows")]
if let Some(enable) = builder.drag_and_drop {
use winit::platform::windows::WindowBuilderExtWindows as _;
window_builder = window_builder.with_drag_and_drop(enable);
}
#[cfg(target_os = "macos")]
{
use winit::platform::macos::WindowBuilderExtMacOS as _;
window_builder = window_builder
.with_title_hidden(builder.title_hidden.unwrap_or(false))
.with_titlebar_transparent(builder.titlebar_transparent.unwrap_or(false))
.with_fullsize_content_view(builder.fullsize_content_view.unwrap_or(false));
}
// TODO: implement `ViewportBuilder::hittest`
// Is not implemented because winit in his current state will not allow to set cursor_hittest on a `WindowBuilder`
window_builder
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
mod profiling_scopes { mod profiling_scopes {
@ -934,3 +1266,7 @@ mod profiling_scopes {
#[allow(unused_imports)] #[allow(unused_imports)]
pub(crate) use profiling_scopes::*; pub(crate) use profiling_scopes::*;
use winit::{
dpi::{LogicalPosition, LogicalSize},
window::{CursorGrabMode, WindowButtons, WindowLevel},
};

31
crates/egui-winit/src/window_settings.rs

@ -1,3 +1,5 @@
use egui::ViewportBuilder;
/// Can be used to store native window settings (position and size). /// Can be used to store native window settings (position and size).
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@ -46,10 +48,10 @@ impl WindowSettings {
self.inner_size_points self.inner_size_points
} }
pub fn initialize_window_builder( pub fn initialize_viewport_builder(
&self, &self,
mut window: winit::window::WindowBuilder, mut viewport_builder: ViewportBuilder,
) -> winit::window::WindowBuilder { ) -> ViewportBuilder {
// `WindowBuilder::with_position` expects inner position in Macos, and outer position elsewhere // `WindowBuilder::with_position` expects inner position in Macos, and outer position elsewhere
// See [`winit::window::WindowBuilder::with_position`] for details. // See [`winit::window::WindowBuilder::with_position`] for details.
let pos_px = if cfg!(target_os = "macos") { let pos_px = if cfg!(target_os = "macos") {
@ -57,26 +59,17 @@ impl WindowSettings {
} else { } else {
self.outer_position_pixels self.outer_position_pixels
}; };
if let Some(pos_px) = pos_px { if let Some(pos) = pos_px {
window = window.with_position(winit::dpi::PhysicalPosition { viewport_builder = viewport_builder.with_position(pos);
x: pos_px.x as f64,
y: pos_px.y as f64,
});
} }
if let Some(inner_size_points) = self.inner_size_points { if let Some(inner_size_points) = self.inner_size_points {
window viewport_builder = viewport_builder
.with_inner_size(winit::dpi::LogicalSize { .with_inner_size(inner_size_points)
width: inner_size_points.x as f64, .with_fullscreen(self.fullscreen);
height: inner_size_points.y as f64,
})
.with_fullscreen(
self.fullscreen
.then_some(winit::window::Fullscreen::Borderless(None)),
)
} else {
window
} }
viewport_builder
} }
pub fn initialize_window(&self, window: &winit::window::Window) { pub fn initialize_window(&self, window: &winit::window::Window) {

12
crates/egui/src/containers/area.rs

@ -259,7 +259,7 @@ impl Area {
let layer_id = LayerId::new(order, id); let layer_id = LayerId::new(order, id);
let state = ctx.memory(|mem| mem.areas.get(id).copied()); let state = ctx.memory(|mem| mem.areas().get(id).copied());
let is_new = state.is_none(); let is_new = state.is_none();
if is_new { if is_new {
ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place
@ -307,9 +307,9 @@ impl Area {
if (move_response.dragged() || move_response.clicked()) if (move_response.dragged() || move_response.clicked())
|| pointer_pressed_on_area(ctx, layer_id) || pointer_pressed_on_area(ctx, layer_id)
|| !ctx.memory(|m| m.areas.visible_last_frame(&layer_id)) || !ctx.memory(|m| m.areas().visible_last_frame(&layer_id))
{ {
ctx.memory_mut(|m| m.areas.move_to_top(layer_id)); ctx.memory_mut(|m| m.areas_mut().move_to_top(layer_id));
ctx.request_repaint(); ctx.request_repaint();
} }
@ -353,7 +353,7 @@ impl Area {
} }
let layer_id = LayerId::new(self.order, self.id); let layer_id = LayerId::new(self.order, self.id);
let area_rect = ctx.memory(|mem| mem.areas.get(self.id).map(|area| area.rect())); let area_rect = ctx.memory(|mem| mem.areas().get(self.id).map(|area| area.rect()));
if let Some(area_rect) = area_rect { if let Some(area_rect) = area_rect {
let clip_rect = ctx.available_rect(); let clip_rect = ctx.available_rect();
let painter = Painter::new(ctx.clone(), layer_id, clip_rect); let painter = Painter::new(ctx.clone(), layer_id, clip_rect);
@ -441,7 +441,7 @@ impl Prepared {
state.size = content_ui.min_size(); state.size = content_ui.min_size();
ctx.memory_mut(|m| m.areas.set_state(layer_id, state)); ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state));
move_response move_response
} }
@ -458,7 +458,7 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
fn automatic_area_position(ctx: &Context) -> Pos2 { fn automatic_area_position(ctx: &Context) -> Pos2 {
let mut existing: Vec<Rect> = ctx.memory(|mem| { let mut existing: Vec<Rect> = ctx.memory(|mem| {
mem.areas mem.areas()
.visible_windows() .visible_windows()
.into_iter() .into_iter()
.map(State::rect) .map(State::rect)

2
crates/egui/src/containers/combo_box.rs

@ -244,7 +244,7 @@ fn combo_box_dyn<'c, R>(
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id)); let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
let popup_height = ui.memory(|m| m.areas.get(popup_id).map_or(100.0, |state| state.size.y)); let popup_height = ui.memory(|m| m.areas().get(popup_id).map_or(100.0, |state| state.size.y));
let above_or_below = let above_or_below =
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height

4
crates/egui/src/containers/panel.rs

@ -702,9 +702,9 @@ impl TopBottomPanel {
if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down()) if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down())
&& mouse_over_resize_line && mouse_over_resize_line
{ {
ui.memory_mut(|mem| mem.interaction.drag_id = Some(resize_id)); ui.memory_mut(|mem| mem.interaction_mut().drag_id = Some(resize_id));
} }
is_resizing = ui.memory(|mem| mem.interaction.drag_id == Some(resize_id)); is_resizing = ui.memory(|mem| mem.interaction().drag_id == Some(resize_id));
if is_resizing { if is_resizing {
let height = (pointer.y - side.side_y(panel_rect)).abs(); let height = (pointer.y - side.side_y(panel_rect)).abs();
let height = let height =

2
crates/egui/src/containers/popup.rs

@ -280,7 +280,7 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
if *individual_id == tooltip_id { if *individual_id == tooltip_id {
let area_id = common_id.with(count); let area_id = common_id.with(count);
let layer_id = LayerId::new(Order::Tooltip, area_id); let layer_id = LayerId::new(Order::Tooltip, area_id);
if ctx.memory(|mem| mem.areas.visible_last_frame(&layer_id)) { if ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id)) {
return true; return true;
} }
} }

19
crates/egui/src/containers/window.rs

@ -24,6 +24,9 @@ use super::*;
/// ``` /// ```
/// ///
/// The previous rectangle used by this window can be obtained through [`crate::Memory::area_rect()`]. /// The previous rectangle used by this window can be obtained through [`crate::Memory::area_rect()`].
///
/// Note that this is NOT a native OS window.
/// To create a new native OS window, use [`crate::Context::show_viewport`].
#[must_use = "You should call .show()"] #[must_use = "You should call .show()"]
pub struct Window<'open> { pub struct Window<'open> {
title: WidgetText, title: WidgetText,
@ -585,7 +588,7 @@ fn interact(
} }
} }
ctx.memory_mut(|mem| mem.areas.move_to_top(area_layer_id)); ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id));
Some(window_interaction) Some(window_interaction)
} }
@ -638,31 +641,31 @@ fn window_interaction(
rect: Rect, rect: Rect,
) -> Option<WindowInteraction> { ) -> Option<WindowInteraction> {
{ {
let drag_id = ctx.memory(|mem| mem.interaction.drag_id); let drag_id = ctx.memory(|mem| mem.interaction().drag_id);
if drag_id.is_some() && drag_id != Some(id) { if drag_id.is_some() && drag_id != Some(id) {
return None; return None;
} }
} }
let mut window_interaction = ctx.memory(|mem| mem.window_interaction); let mut window_interaction = ctx.memory(|mem| mem.window_interaction());
if window_interaction.is_none() { if window_interaction.is_none() {
if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) { if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) {
hover_window_interaction.set_cursor(ctx); hover_window_interaction.set_cursor(ctx);
if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) { if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) {
ctx.memory_mut(|mem| { ctx.memory_mut(|mem| {
mem.interaction.drag_id = Some(id); mem.interaction_mut().drag_id = Some(id);
mem.interaction.drag_is_window = true; mem.interaction_mut().drag_is_window = true;
window_interaction = Some(hover_window_interaction); window_interaction = Some(hover_window_interaction);
mem.window_interaction = window_interaction; mem.set_window_interaction(window_interaction);
}); });
} }
} }
} }
if let Some(window_interaction) = window_interaction { if let Some(window_interaction) = window_interaction {
let is_active = ctx.memory_mut(|mem| mem.interaction.drag_id == Some(id)); let is_active = ctx.memory_mut(|mem| mem.interaction().drag_id == Some(id));
if is_active && window_interaction.area_layer_id == area_layer_id { if is_active && window_interaction.area_layer_id == area_layer_id {
return Some(window_interaction); return Some(window_interaction);
@ -690,7 +693,7 @@ fn resize_hover(
} }
} }
if ctx.memory(|mem| mem.interaction.drag_interest) { if ctx.memory(|mem| mem.interaction().drag_interest) {
// Another widget will become active if we drag here // Another widget will become active if we drag here
return None; return None;
} }

938
crates/egui/src/context.rs

File diff suppressed because it is too large

51
crates/egui/src/data/input.rs

@ -1,6 +1,6 @@
//! The input needed by egui. //! The input needed by egui.
use crate::emath::*; use crate::{emath::*, ViewportIdPair};
/// What the integrations provides to egui at the start of each frame. /// What the integrations provides to egui at the start of each frame.
/// ///
@ -13,6 +13,9 @@ use crate::emath::*;
#[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 RawInput { pub struct RawInput {
/// Information about the viwport the input is part of.
pub viewport: ViewportInfo,
/// Position and size of the area that egui should use, in points. /// Position and size of the area that egui should use, in points.
/// Usually you would set this to /// Usually you would set this to
/// ///
@ -72,6 +75,7 @@ pub struct RawInput {
impl Default for RawInput { impl Default for RawInput {
fn default() -> Self { fn default() -> Self {
Self { Self {
viewport: ViewportInfo::default(),
screen_rect: None, screen_rect: None,
pixels_per_point: None, pixels_per_point: None,
max_texture_side: None, max_texture_side: None,
@ -93,6 +97,7 @@ impl RawInput {
/// * [`Self::dropped_files`] is moved. /// * [`Self::dropped_files`] is moved.
pub fn take(&mut self) -> RawInput { pub fn take(&mut self) -> RawInput {
RawInput { RawInput {
viewport: self.viewport.take(),
screen_rect: self.screen_rect.take(), screen_rect: self.screen_rect.take(),
pixels_per_point: self.pixels_per_point.take(), pixels_per_point: self.pixels_per_point.take(),
max_texture_side: self.max_texture_side.take(), max_texture_side: self.max_texture_side.take(),
@ -109,6 +114,7 @@ impl RawInput {
/// Add on new input. /// Add on new input.
pub fn append(&mut self, newer: Self) { pub fn append(&mut self, newer: Self) {
let Self { let Self {
viewport,
screen_rect, screen_rect,
pixels_per_point, pixels_per_point,
max_texture_side, max_texture_side,
@ -121,6 +127,7 @@ impl RawInput {
focused, focused,
} = newer; } = newer;
self.viewport = viewport;
self.screen_rect = screen_rect.or(self.screen_rect); self.screen_rect = screen_rect.or(self.screen_rect);
self.pixels_per_point = pixels_per_point.or(self.pixels_per_point); self.pixels_per_point = pixels_per_point.or(self.pixels_per_point);
self.max_texture_side = max_texture_side.or(self.max_texture_side); self.max_texture_side = max_texture_side.or(self.max_texture_side);
@ -134,6 +141,46 @@ impl RawInput {
} }
} }
/// Information about the current viewport,
/// given as input each frame.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ViewportInfo {
/// Id of us and our parent.
pub ids: ViewportIdPair,
/// Viewport inner position and size, only the drowable area
/// unit = physical pixels
pub inner_rect_px: Option<Rect>,
/// Viewport outer position and size, drowable area + decorations
/// unit = physical pixels
pub outer_rect_px: Option<Rect>,
/// The user requested the viewport should close,
/// e.g. by pressing the close button in the window decoration.
pub close_requested: bool,
}
impl ViewportInfo {
pub fn take(&mut self) -> Self {
core::mem::take(self)
}
pub fn ui(&self, ui: &mut crate::Ui) {
let Self {
ids,
inner_rect_px,
outer_rect_px,
close_requested,
} = self;
ui.label(format!("ids: {ids:?}"));
ui.label(format!("inner_rect_px: {inner_rect_px:?}"));
ui.label(format!("outer_rect_px: {outer_rect_px:?}"));
ui.label(format!("close_requested: {close_requested:?}"));
}
}
/// A file about to be dropped into egui. /// A file about to be dropped into egui.
#[derive(Clone, Debug, Default, PartialEq, Eq)] #[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@ -943,8 +990,10 @@ impl RawInput {
hovered_files, hovered_files,
dropped_files, dropped_files,
focused, focused,
viewport,
} = self; } = self;
viewport.ui(ui);
ui.label(format!("screen_rect: {screen_rect:?} points")); ui.label(format!("screen_rect: {screen_rect:?} points"));
ui.label(format!("pixels_per_point: {pixels_per_point:?}")) ui.label(format!("pixels_per_point: {pixels_per_point:?}"))
.on_hover_text( .on_hover_text(

52
crates/egui/src/data/output.rs

@ -1,25 +1,15 @@
//! All the data egui returns to the backend at the end of each frame. //! All the data egui returns to the backend at the end of each frame.
use crate::WidgetType; use crate::{ViewportIdMap, ViewportOutput, WidgetType};
/// What egui emits each frame from [`crate::Context::run`]. /// What egui emits each frame from [`crate::Context::run`].
/// ///
/// The backend should use this. /// The backend should use this.
#[derive(Clone, Default, PartialEq)] #[derive(Clone, Default)]
pub struct FullOutput { pub struct FullOutput {
/// Non-rendering related output. /// Non-rendering related output.
pub platform_output: PlatformOutput, pub platform_output: PlatformOutput,
/// If `Duration::is_zero()`, egui is requesting immediate repaint (i.e. on the next frame).
///
/// This happens for instance when there is an animation, or if a user has called `Context::request_repaint()`.
///
/// If `Duration` is greater than zero, egui wants to be repainted at or before the specified
/// duration elapses. when in reactive mode, egui spends forever waiting for input and only then,
/// will it repaint itself. this can be used to make sure that backend will only wait for a
/// specified amount of time, and repaint egui without any new input.
pub repaint_after: std::time::Duration,
/// Texture changes since last frame (including the font texture). /// Texture changes since last frame (including the font texture).
/// ///
/// The backend needs to apply [`crate::TexturesDelta::set`] _before_ painting, /// The backend needs to apply [`crate::TexturesDelta::set`] _before_ painting,
@ -30,6 +20,14 @@ pub struct FullOutput {
/// ///
/// You can use [`crate::Context::tessellate`] to turn this into triangles. /// You can use [`crate::Context::tessellate`] to turn this into triangles.
pub shapes: Vec<epaint::ClippedShape>, pub shapes: Vec<epaint::ClippedShape>,
/// The number of physical pixels per logical ui point, for the viewport that was updated.
///
/// You can pass this to [`crate::Context::tessellate`] together with [`Self::shapes`].
pub pixels_per_point: f32,
/// All the active viewports, including the root.
pub viewport_output: ViewportIdMap<ViewportOutput>,
} }
impl FullOutput { impl FullOutput {
@ -37,15 +35,27 @@ impl FullOutput {
pub fn append(&mut self, newer: Self) { pub fn append(&mut self, newer: Self) {
let Self { let Self {
platform_output, platform_output,
repaint_after,
textures_delta, textures_delta,
shapes, shapes,
pixels_per_point,
viewport_output: viewports,
} = newer; } = newer;
self.platform_output.append(platform_output); self.platform_output.append(platform_output);
self.repaint_after = repaint_after; // if the last frame doesn't need a repaint, then we don't need to repaint
self.textures_delta.append(textures_delta); self.textures_delta.append(textures_delta);
self.shapes = shapes; // Only paint the latest self.shapes = shapes; // Only paint the latest
self.pixels_per_point = pixels_per_point; // Use latest
for (id, new_viewport) in viewports {
match self.viewport_output.entry(id) {
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(new_viewport);
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
entry.get_mut().append(new_viewport);
}
}
}
} }
} }
@ -88,6 +98,9 @@ pub struct PlatformOutput {
/// Iff `Some`, the user is editing text. /// Iff `Some`, the user is editing text.
pub text_cursor_pos: Option<crate::Pos2>, pub text_cursor_pos: Option<crate::Pos2>,
/// The difference in the widget tree since last frame.
///
/// NOTE: this needs to be per-viewport.
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
pub accesskit_update: Option<accesskit::TreeUpdate>, pub accesskit_update: Option<accesskit::TreeUpdate>,
} }
@ -626,16 +639,15 @@ impl WidgetInfo {
} }
if typ == &WidgetType::TextEdit { if typ == &WidgetType::TextEdit {
let text; let text = if let Some(text_value) = text_value {
if let Some(text_value) = text_value {
if text_value.is_empty() { if text_value.is_empty() {
text = "blank".into(); "blank".into()
} else { } else {
text = text_value.to_string(); text_value.to_string()
} }
} else { } else {
text = "blank".into(); "blank".into()
} };
description = format!("{text}: {description}"); description = format!("{text}: {description}");
} }

2
crates/egui/src/id.rs

@ -42,7 +42,7 @@ impl Id {
Self(0) Self(0)
} }
pub(crate) fn background() -> Self { pub(crate) const fn background() -> Self {
Self(1) Self(1)
} }

1
crates/egui/src/input_state.rs

@ -150,6 +150,7 @@ impl InputState {
requested_repaint_last_frame: bool, requested_repaint_last_frame: bool,
) -> InputState { ) -> InputState {
crate::profile_function!(); crate::profile_function!();
let time = new.time.unwrap_or(self.time + new.predicted_dt as f64); let time = new.time.unwrap_or(self.time + new.predicted_dt as f64);
let unstable_dt = (time - self.time) as f32; let unstable_dt = (time - self.time) as f32;

6
crates/egui/src/layers.rs

@ -170,7 +170,9 @@ impl GraphicLayers {
.or_default() .or_default()
} }
pub fn drain(&mut self, area_order: &[LayerId]) -> impl ExactSizeIterator<Item = ClippedShape> { pub fn drain(&mut self, area_order: &[LayerId]) -> Vec<ClippedShape> {
crate::profile_function!();
let mut all_shapes: Vec<_> = Default::default(); let mut all_shapes: Vec<_> = Default::default();
for &order in &Order::ALL { for &order in &Order::ALL {
@ -196,6 +198,6 @@ impl GraphicLayers {
} }
} }
all_shapes.into_iter() all_shapes
} }
} }

8
crates/egui/src/lib.rs

@ -96,6 +96,10 @@
//! # }); //! # });
//! ``` //! ```
//! //!
//! ## Viewports
//! Some egui backends support multiple _viewports_, which is what egui calls the native OS windows it resides in.
//! See [`crate::viewport`] for more information.
//!
//! ## Coordinate system //! ## Coordinate system
//! The left-top corner of the screen is `(0.0, 0.0)`, //! The left-top corner of the screen is `(0.0, 0.0)`,
//! with X increasing to the right and Y increasing downwards. //! with X increasing to the right and Y increasing downwards.
@ -134,7 +138,7 @@
//! }); //! });
//! }); //! });
//! handle_platform_output(full_output.platform_output); //! handle_platform_output(full_output.platform_output);
//! let clipped_primitives = ctx.tessellate(full_output.shapes); // create triangles to paint //! let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point);
//! paint(full_output.textures_delta, clipped_primitives); //! paint(full_output.textures_delta, clipped_primitives);
//! } //! }
//! ``` //! ```
@ -357,6 +361,7 @@ mod sense;
pub mod style; pub mod style;
mod ui; mod ui;
pub mod util; pub mod util;
pub mod viewport;
pub mod widget_text; pub mod widget_text;
pub mod widgets; pub mod widgets;
@ -417,6 +422,7 @@ pub use {
style::{FontSelection, Margin, Style, TextStyle, Visuals}, style::{FontSelection, Margin, Style, TextStyle, Visuals},
text::{Galley, TextFormat}, text::{Galley, TextFormat},
ui::Ui, ui::Ui,
viewport::*,
widget_text::{RichText, WidgetText}, widget_text::{RichText, WidgetText},
widgets::*, widgets::*,
}; };

4
crates/egui/src/load.rs

@ -58,7 +58,7 @@ mod texture_loader;
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt::Debug; use std::fmt::Debug;
use std::ops::Deref; use std::ops::Deref;
use std::{error::Error as StdError, fmt::Display, sync::Arc}; use std::{fmt::Display, sync::Arc};
use ahash::HashMap; use ahash::HashMap;
@ -118,7 +118,7 @@ impl Display for LoadError {
} }
} }
impl StdError for LoadError {} impl std::error::Error for LoadError {}
pub type Result<T, E = LoadError> = std::result::Result<T, E>; pub type Result<T, E = LoadError> = std::result::Result<T, E>;

143
crates/egui/src/memory.rs

@ -2,7 +2,12 @@
use epaint::{emath::Rangef, vec2, Vec2}; use epaint::{emath::Rangef, vec2, Vec2};
use crate::{area, window, EventFilter, Id, IdMap, InputState, LayerId, Pos2, Rect, Style}; use crate::{
area,
window::{self, WindowInteraction},
EventFilter, Id, IdMap, InputState, LayerId, Pos2, Rect, Style, ViewportId, ViewportIdMap,
ViewportIdSet,
};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -15,7 +20,7 @@ use crate::{area, window, EventFilter, Id, IdMap, InputState, LayerId, Pos2, Rec
/// For this you need to enable the `persistence`. /// For this you need to enable the `persistence`.
/// ///
/// If you want to store data for your widgets, you should look at [`Memory::data`] /// If you want to store data for your widgets, you should look at [`Memory::data`]
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", serde(default))] #[cfg_attr(feature = "persistence", serde(default))]
pub struct Memory { pub struct Memory {
@ -68,23 +73,19 @@ pub struct Memory {
// ------------------------------------------ // ------------------------------------------
/// new scale that will be applied at the start of the next frame /// new scale that will be applied at the start of the next frame
#[cfg_attr(feature = "persistence", serde(skip))] #[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) new_pixels_per_point: Option<f32>, pub(crate) override_pixels_per_point: Option<f32>,
/// new fonts that will be applied at the start of the next frame /// new fonts that will be applied at the start of the next frame
#[cfg_attr(feature = "persistence", serde(skip))] #[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) new_font_definitions: Option<epaint::text::FontDefinitions>, pub(crate) new_font_definitions: Option<epaint::text::FontDefinitions>,
// Current active viewport
#[cfg_attr(feature = "persistence", serde(skip))] #[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) interaction: Interaction, pub(crate) viewport_id: ViewportId,
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) window_interaction: Option<window::WindowInteraction>,
#[cfg_attr(feature = "persistence", serde(skip))] #[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) drag_value: crate::widgets::drag_value::MonoState, pub(crate) drag_value: crate::widgets::drag_value::MonoState,
pub(crate) areas: Areas,
/// Which popup-window is open (if any)? /// Which popup-window is open (if any)?
/// Could be a combo box, color picker, menu etc. /// Could be a combo box, color picker, menu etc.
#[cfg_attr(feature = "persistence", serde(skip))] #[cfg_attr(feature = "persistence", serde(skip))]
@ -92,6 +93,38 @@ pub struct Memory {
#[cfg_attr(feature = "persistence", serde(skip))] #[cfg_attr(feature = "persistence", serde(skip))]
everything_is_visible: bool, everything_is_visible: bool,
// -------------------------------------------------
// Per-viewport:
areas: ViewportIdMap<Areas>,
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) interactions: ViewportIdMap<Interaction>,
#[cfg_attr(feature = "persistence", serde(skip))]
window_interactions: ViewportIdMap<window::WindowInteraction>,
}
impl Default for Memory {
fn default() -> Self {
let mut slf = Self {
options: Default::default(),
data: Default::default(),
caches: Default::default(),
override_pixels_per_point: Default::default(),
new_font_definitions: Default::default(),
interactions: Default::default(),
viewport_id: Default::default(),
window_interactions: Default::default(),
drag_value: Default::default(),
areas: Default::default(),
popup: Default::default(),
everything_is_visible: Default::default(),
};
slf.interactions.entry(slf.viewport_id).or_default();
slf.areas.entry(slf.viewport_id).or_default();
slf
}
} }
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@ -516,34 +549,63 @@ impl Memory {
&mut self, &mut self,
prev_input: &crate::input_state::InputState, prev_input: &crate::input_state::InputState,
new_input: &crate::data::input::RawInput, new_input: &crate::data::input::RawInput,
viewports: &ViewportIdSet,
) { ) {
crate::profile_function!(); crate::profile_function!();
self.interaction.begin_frame(prev_input, new_input);
// Cleanup
self.interactions.retain(|id, _| viewports.contains(id));
self.areas.retain(|id, _| viewports.contains(id));
self.window_interactions
.retain(|id, _| viewports.contains(id));
self.viewport_id = new_input.viewport.ids.this;
self.interactions
.entry(self.viewport_id)
.or_default()
.begin_frame(prev_input, new_input);
self.areas.entry(self.viewport_id).or_default();
if !prev_input.pointer.any_down() { if !prev_input.pointer.any_down() {
self.window_interaction = None; self.window_interactions.remove(&self.viewport_id);
} }
} }
pub(crate) fn end_frame(&mut self, input: &InputState, used_ids: &IdMap<Rect>) { pub(crate) fn end_frame(&mut self, input: &InputState, used_ids: &IdMap<Rect>) {
self.caches.update(); self.caches.update();
self.areas.end_frame(); self.areas_mut().end_frame();
self.interaction.focus.end_frame(used_ids); self.interaction_mut().focus.end_frame(used_ids);
self.drag_value.end_frame(input); self.drag_value.end_frame(input);
} }
pub(crate) fn set_viewport_id(&mut self, viewport_id: ViewportId) {
self.viewport_id = viewport_id;
}
/// Access memory of the [`Area`](crate::containers::area::Area)s, such as `Window`s.
pub fn areas(&self) -> &Areas {
self.areas
.get(&self.viewport_id)
.expect("Memory broken: no area for the current viewport")
}
/// Access memory of the [`Area`](crate::containers::area::Area)s, such as `Window`s.
pub fn areas_mut(&mut self) -> &mut Areas {
self.areas.entry(self.viewport_id).or_default()
}
/// Top-most layer at the given position. /// Top-most layer at the given position.
pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<LayerId> { pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<LayerId> {
self.areas.layer_id_at(pos, resize_interact_radius_side) self.areas().layer_id_at(pos, resize_interact_radius_side)
} }
/// An iterator over all layers. Back-to-front. Top is last. /// An iterator over all layers. Back-to-front. Top is last.
pub fn layer_ids(&self) -> impl ExactSizeIterator<Item = LayerId> + '_ { pub fn layer_ids(&self) -> impl ExactSizeIterator<Item = LayerId> + '_ {
self.areas.order().iter().copied() self.areas().order().iter().copied()
} }
pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool { pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool {
self.interaction.focus.id_previous_frame == Some(id) self.interaction().focus.id_previous_frame == Some(id)
} }
/// True if the given widget had keyboard focus last frame, but not this one. /// True if the given widget had keyboard focus last frame, but not this one.
@ -564,12 +626,12 @@ impl Memory {
/// from the window and back. /// from the window and back.
#[inline(always)] #[inline(always)]
pub fn has_focus(&self, id: Id) -> bool { pub fn has_focus(&self, id: Id) -> bool {
self.interaction.focus.focused() == Some(id) self.interaction().focus.focused() == Some(id)
} }
/// Which widget has keyboard focus? /// Which widget has keyboard focus?
pub fn focus(&self) -> Option<Id> { pub fn focus(&self) -> Option<Id> {
self.interaction.focus.focused() self.interaction().focus.focused()
} }
/// Set an event filter for a widget. /// Set an event filter for a widget.
@ -580,7 +642,7 @@ impl Memory {
/// You must first give focus to the widget before calling this. /// You must first give focus to the widget before calling this.
pub fn set_focus_lock_filter(&mut self, id: Id, event_filter: EventFilter) { pub fn set_focus_lock_filter(&mut self, id: Id, event_filter: EventFilter) {
if self.had_focus_last_frame(id) && self.has_focus(id) { if self.had_focus_last_frame(id) && self.has_focus(id) {
if let Some(focused) = &mut self.interaction.focus.focused_widget { if let Some(focused) = &mut self.interaction_mut().focus.focused_widget {
if focused.id == id { if focused.id == id {
focused.filter = event_filter; focused.filter = event_filter;
} }
@ -607,15 +669,16 @@ impl Memory {
/// See also [`crate::Response::request_focus`]. /// See also [`crate::Response::request_focus`].
#[inline(always)] #[inline(always)]
pub fn request_focus(&mut self, id: Id) { pub fn request_focus(&mut self, id: Id) {
self.interaction.focus.focused_widget = Some(FocusWidget::new(id)); self.interaction_mut().focus.focused_widget = Some(FocusWidget::new(id));
} }
/// Surrender keyboard focus for a specific widget. /// Surrender keyboard focus for a specific widget.
/// See also [`crate::Response::surrender_focus`]. /// See also [`crate::Response::surrender_focus`].
#[inline(always)] #[inline(always)]
pub fn surrender_focus(&mut self, id: Id) { pub fn surrender_focus(&mut self, id: Id) {
if self.interaction.focus.focused() == Some(id) { let interaction = self.interaction_mut();
self.interaction.focus.focused_widget = None; if interaction.focus.focused() == Some(id) {
interaction.focus.focused_widget = None;
} }
} }
@ -628,37 +691,37 @@ impl Memory {
/// and rendered correctly in a single frame. /// and rendered correctly in a single frame.
#[inline(always)] #[inline(always)]
pub fn interested_in_focus(&mut self, id: Id) { pub fn interested_in_focus(&mut self, id: Id) {
self.interaction.focus.interested_in_focus(id); self.interaction_mut().focus.interested_in_focus(id);
} }
/// Stop editing of active [`TextEdit`](crate::TextEdit) (if any). /// Stop editing of active [`TextEdit`](crate::TextEdit) (if any).
#[inline(always)] #[inline(always)]
pub fn stop_text_input(&mut self) { pub fn stop_text_input(&mut self) {
self.interaction.focus.focused_widget = None; self.interaction_mut().focus.focused_widget = None;
} }
/// Is any widget being dragged? /// Is any widget being dragged?
#[inline(always)] #[inline(always)]
pub fn is_anything_being_dragged(&self) -> bool { pub fn is_anything_being_dragged(&self) -> bool {
self.interaction.drag_id.is_some() self.interaction().drag_id.is_some()
} }
/// Is this specific widget being dragged? /// Is this specific widget being dragged?
#[inline(always)] #[inline(always)]
pub fn is_being_dragged(&self, id: Id) -> bool { pub fn is_being_dragged(&self, id: Id) -> bool {
self.interaction.drag_id == Some(id) self.interaction().drag_id == Some(id)
} }
/// Set which widget is being dragged. /// Set which widget is being dragged.
#[inline(always)] #[inline(always)]
pub fn set_dragged_id(&mut self, id: Id) { pub fn set_dragged_id(&mut self, id: Id) {
self.interaction.drag_id = Some(id); self.interaction_mut().drag_id = Some(id);
} }
/// Stop dragging any widget. /// Stop dragging any widget.
#[inline(always)] #[inline(always)]
pub fn stop_dragging(&mut self) { pub fn stop_dragging(&mut self) {
self.interaction.drag_id = None; self.interaction_mut().drag_id = None;
} }
/// Forget window positions, sizes etc. /// Forget window positions, sizes etc.
@ -669,7 +732,29 @@ impl Memory {
/// Obtain the previous rectangle of an area. /// Obtain the previous rectangle of an area.
pub fn area_rect(&self, id: impl Into<Id>) -> Option<Rect> { pub fn area_rect(&self, id: impl Into<Id>) -> Option<Rect> {
self.areas.get(id.into()).map(|state| state.rect()) self.areas().get(id.into()).map(|state| state.rect())
}
pub(crate) fn window_interaction(&self) -> Option<WindowInteraction> {
self.window_interactions.get(&self.viewport_id).copied()
}
pub(crate) fn set_window_interaction(&mut self, wi: Option<WindowInteraction>) {
if let Some(wi) = wi {
self.window_interactions.insert(self.viewport_id, wi);
} else {
self.window_interactions.remove(&self.viewport_id);
}
}
pub(crate) fn interaction(&self) -> &Interaction {
self.interactions
.get(&self.viewport_id)
.expect("Failed to get interaction")
}
pub(crate) fn interaction_mut(&mut self) -> &mut Interaction {
self.interactions.entry(self.viewport_id).or_default()
} }
} }

804
crates/egui/src/viewport.rs

@ -0,0 +1,804 @@
//! egui supports multiple viewports, corresponding to multiple native windows.
//!
//! Not all egui backends support multiple viewports, but `eframe` native does
//! (but not on web).
//!
//! You can spawn a new viewport using [`Context::show_viewport`] and [`Context::show_viewport_immediate`].
//! These needs to be called every frame the viewport should be visible.
//!
//! This is implemented by the native `eframe` backend, but not the web one.
//!
//! ## Viewport classes
//! The viewports form a tree of parent-child relationships.
//!
//! There are different classes of viewports.
//!
//! ### Root viewport
//! The root viewport is the original viewport, and cannot be closed without closing the application.
//!
//! ### Deferred viewports
//! These are created with [`Context::show_viewport`].
//! Deferred viewports take a closure that is called by the integration at a later time, perhaps multiple times.
//! Deferred viewports are repainted independenantly of the parent viewport.
//! This means communication with them need to done via channels, or `Arc/Mutex`.
//!
//! This is the most performant type of child viewport, though a bit more cumbersome to work with compared to immediate viewports.
//!
//! ### Immediate viewports
//! These are created with [`Context::show_viewport_immediate`].
//! Immediate viewports take a `FnOnce` closure, similar to other egui functions, and is called immediately.
//! This makes communication with them much simpler than with deferred viewports, but this simplicity comes at a cost: whenever the parent viewports needs to be repainted, so will the child viewport, and vice versa.
//! This means that if you have `N` viewports you are potentially doing `N` times as much CPU work. However, if all your viewports are showing animations, and thus are repainting constantly anyway, this doesn't matter.
//!
//! In short: immediate viewports are simpler to use, but can waste a lot of CPU time.
//!
//! ### Embedded viewports
//! These are not real, independenant viewports, but is a fallback mode for when the integration does not support real viewports. In your callback is called with [`ViewportClass::Embedded`] it means you need to create an [`crate::Window`] to wrap your ui in, which will then be embedded in the parent viewport, unable to escape it.
//!
//!
//! ## Using the viewports
//! Only one viewport is active at any one time, identified with [`Context::viewport_id`].
//! You can send commands to other viewports using [`Context::send_viewport_command_to`].
//!
//! There is an example in <https://github.com/emilk/egui/tree/master/examples/multiple_viewports/src/main.rs>.
//!
//! ## For integrations
//! * There is a [`crate::RawInput::viewport`] with information about the current viewport.
//! * The repaint callback set by [`Context::set_request_repaint_callback`] points to which viewport should be repainted.
//! * [`crate::FullOutput::viewport_output`] is a list of viewports which should result in their own independent windows.
//! * To support immediate viewports you need to call [`Context::set_immediate_viewport_renderer`].
//! * If you support viewports, you need to call [`Context::set_embed_viewports`] with `false`, or all new viewports will be embedded (the default behavior).
//!
//! ## Future work
//! There are several more things related to viewports that we want to add.
//! Read more at <https://github.com/emilk/egui/issues/3556>.
use std::sync::Arc;
use epaint::{ColorImage, Pos2, Vec2};
use crate::{Context, Id};
// ----------------------------------------------------------------------------
/// The different types of viewports supported by egui.
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ViewportClass {
/// The root viewport; i.e. the original window.
#[default]
Root,
/// A viewport run independently from the parent viewport.
///
/// This is the preferred type of viewport from a performance perspective.
///
/// Create these with [`crate::Context::show_viewport`].
Deferred,
/// A viewport run inside the parent viewport.
///
/// This is the easier type of viewport to use, but it is less performant
/// at it requires both parent and child to repaint if any one of them needs repainting,
/// which efficvely produce double work for two viewports, and triple work for three viewports, etc.
///
/// Create these with [`crate::Context::show_viewport_immediate`].
Immediate,
/// The fallback, when the egui integration doesn't support viewports,
/// or [`crate::Context::embed_viewports`] is set to `true`.
Embedded,
}
// ----------------------------------------------------------------------------
/// A unique identifier of a viewport.
///
/// This is returned by [`Context::viewport_id`] and [`Context::parent_viewport_id`].
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ViewportId(pub Id);
impl Default for ViewportId {
#[inline]
fn default() -> Self {
Self::ROOT
}
}
impl std::fmt::Debug for ViewportId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.short_debug_format().fmt(f)
}
}
impl ViewportId {
/// The `ViewportId` of the root viewport.
pub const ROOT: Self = Self(Id::NULL);
#[inline]
pub fn from_hash_of(source: impl std::hash::Hash) -> Self {
Self(Id::new(source))
}
}
impl From<ViewportId> for Id {
#[inline]
fn from(id: ViewportId) -> Self {
id.0
}
}
impl nohash_hasher::IsEnabled for ViewportId {}
/// A fast hash set of [`ViewportId`].
pub type ViewportIdSet = nohash_hasher::IntSet<ViewportId>;
/// A fast hash map from [`ViewportId`] to `T`.
pub type ViewportIdMap<T> = nohash_hasher::IntMap<ViewportId, T>;
// ----------------------------------------------------------------------------
/// A pair of [`ViewportId`], used to identify a viewport and its parent.
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ViewportIdPair {
pub this: ViewportId,
pub parent: ViewportId,
}
impl Default for ViewportIdPair {
#[inline]
fn default() -> Self {
Self::ROOT
}
}
impl ViewportIdPair {
/// The `ViewportIdPair` of the root viewport, which is its own parent.
pub const ROOT: Self = Self {
this: ViewportId::ROOT,
parent: ViewportId::ROOT,
};
#[inline]
pub fn from_self_and_parent(this: ViewportId, parent: ViewportId) -> Self {
Self { this, parent }
}
}
/// The user-code that shows the ui in the viewport, used for deferred viewports.
pub type DeferredViewportUiCallback = dyn Fn(&Context) + Sync + Send;
/// Render the given viewport, calling the given ui callback.
pub type ImmediateViewportRendererCallback = dyn for<'a> Fn(&Context, ImmediateViewport<'a>);
/// Control the building of a new egui viewport (i.e. native window).
///
/// The fields are public, but you should use the builder pattern to set them,
/// and that's where you'll find the documentation too.
///
/// Since egui is immediate mode, `ViewportBuilder` is accumulative in nature.
/// Setting any option to `None` means "keep the current value",
/// or "Use the default" if it is the first call.
///
/// The default values are implementation defined, so you may want to explicitly
/// configure the size of the window, and what buttons are shown.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[allow(clippy::option_option)]
pub struct ViewportBuilder {
/// The title of the vieweport.
/// `eframe` will use this as the title of the native window.
pub title: Option<String>,
/// This is wayland only. See [`Self::with_name`].
pub name: Option<(String, String)>,
pub position: Option<Pos2>,
pub inner_size: Option<Vec2>,
pub min_inner_size: Option<Vec2>,
pub max_inner_size: Option<Vec2>,
pub fullscreen: Option<bool>,
pub maximized: Option<bool>,
pub resizable: Option<bool>,
pub transparent: Option<bool>,
pub decorations: Option<bool>,
pub icon: Option<Arc<ColorImage>>,
pub active: Option<bool>,
pub visible: Option<bool>,
pub title_hidden: Option<bool>,
pub titlebar_transparent: Option<bool>,
pub fullsize_content_view: Option<bool>,
pub drag_and_drop: Option<bool>,
pub close_button: Option<bool>,
pub minimize_button: Option<bool>,
pub maximize_button: Option<bool>,
pub hittest: Option<bool>,
}
impl ViewportBuilder {
/// Sets the initial title of the window in the title bar.
///
/// Look at winit for more details
#[inline]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
/// Sets whether the window should have a border, a title bar, etc.
///
/// The default is `true`.
///
/// Look at winit for more details
#[inline]
pub fn with_decorations(mut self, decorations: bool) -> Self {
self.decorations = Some(decorations);
self
}
/// Sets whether the window should be put into fullscreen upon creation.
///
/// The default is `None`.
///
/// Look at winit for more details
/// This will use borderless
#[inline]
pub fn with_fullscreen(mut self, fullscreen: bool) -> Self {
self.fullscreen = Some(fullscreen);
self
}
/// Request that the window is maximized upon creation.
///
/// The default is `false`.
///
/// Look at winit for more details
#[inline]
pub fn with_maximized(mut self, maximized: bool) -> Self {
self.maximized = Some(maximized);
self
}
/// Sets whether the window is resizable or not.
///
/// The default is `true`.
///
/// Look at winit for more details
#[inline]
pub fn with_resizable(mut self, resizable: bool) -> Self {
self.resizable = Some(resizable);
self
}
/// Sets whether the background of the window should be transparent.
///
/// If this is `true`, writing colors with alpha values different than
/// `1.0` will produce a transparent window. On some platforms this
/// is more of a hint for the system and you'd still have the alpha
/// buffer.
///
/// The default is `false`.
/// If this is not working is because the graphic context dozen't support transparency,
/// you will need to set the transparency in the eframe!
#[inline]
pub fn with_transparent(mut self, transparent: bool) -> Self {
self.transparent = Some(transparent);
self
}
/// The icon needs to be wrapped in Arc because will be cloned every frame
#[inline]
pub fn with_window_icon(mut self, icon: impl Into<Arc<ColorImage>>) -> Self {
self.icon = Some(icon.into());
self
}
/// Whether the window will be initially focused or not.
///
/// The window should be assumed as not focused by default
///
/// ## Platform-specific:
///
/// **Android / iOS / X11 / Wayland / Orbital:** Unsupported.
///
/// Look at winit for more details
#[inline]
pub fn with_active(mut self, active: bool) -> Self {
self.active = Some(active);
self
}
/// Sets whether the window will be initially visible or hidden.
///
/// The default is to show the window.
///
/// Look at winit for more details
#[inline]
pub fn with_visible(mut self, visible: bool) -> Self {
self.visible = Some(visible);
self
}
/// Hides the window title.
///
/// Mac Os only.
#[inline]
pub fn with_title_hidden(mut self, title_hidden: bool) -> Self {
self.title_hidden = Some(title_hidden);
self
}
/// Makes the titlebar transparent and allows the content to appear behind it.
///
/// Mac Os only.
#[inline]
pub fn with_titlebar_transparent(mut self, value: bool) -> Self {
self.titlebar_transparent = Some(value);
self
}
/// Makes the window content appear behind the titlebar.
///
/// Mac Os only.
#[inline]
pub fn with_fullsize_content_view(mut self, value: bool) -> Self {
self.fullsize_content_view = Some(value);
self
}
/// Requests the window to be of specific dimensions.
///
/// If this is not set, some platform-specific dimensions will be used.
///
/// Should be bigger then 0
/// Look at winit for more details
#[inline]
pub fn with_inner_size(mut self, size: impl Into<Vec2>) -> Self {
self.inner_size = Some(size.into());
self
}
/// Sets the minimum dimensions a window can have.
///
/// If this is not set, the window will have no minimum dimensions (aside
/// from reserved).
///
/// Should be bigger then 0
/// Look at winit for more details
#[inline]
pub fn with_min_inner_size(mut self, size: impl Into<Vec2>) -> Self {
self.min_inner_size = Some(size.into());
self
}
/// Sets the maximum dimensions a window can have.
///
/// If this is not set, the window will have no maximum or will be set to
/// the primary monitor's dimensions by the platform.
///
/// Should be bigger then 0
/// Look at winit for more details
#[inline]
pub fn with_max_inner_size(mut self, size: impl Into<Vec2>) -> Self {
self.max_inner_size = Some(size.into());
self
}
/// X11 not working!
#[inline]
pub fn with_close_button(mut self, value: bool) -> Self {
self.close_button = Some(value);
self
}
/// X11 not working!
#[inline]
pub fn with_minimize_button(mut self, value: bool) -> Self {
self.minimize_button = Some(value);
self
}
/// X11 not working!
#[inline]
pub fn with_maximize_button(mut self, value: bool) -> Self {
self.maximize_button = Some(value);
self
}
/// This currently only work on windows to be disabled!
#[inline]
pub fn with_drag_and_drop(mut self, value: bool) -> Self {
self.drag_and_drop = Some(value);
self
}
/// This will probably not work as expected!
#[inline]
pub fn with_position(mut self, pos: impl Into<Pos2>) -> Self {
self.position = Some(pos.into());
self
}
/// This is wayland only!
/// Build window with the given name.
///
/// The `general` name sets an application ID, which should match the `.desktop`
/// file distributed with your program. The `instance` is a `no-op`.
///
/// For details about application ID conventions, see the
/// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id)
#[inline]
pub fn with_name(mut self, id: impl Into<String>, instance: impl Into<String>) -> Self {
self.name = Some((id.into(), instance.into()));
self
}
/// Is not implemented for winit
/// You should use `ViewportCommand::CursorHitTest` if you want to set this!
#[deprecated]
#[inline]
pub fn with_hittest(mut self, value: bool) -> Self {
self.hittest = Some(value);
self
}
/// Update this `ViewportBuilder` with a delta,
/// returning a list of commands and a bool intdicating if the window needs to be recreated.
pub fn patch(&mut self, new: &ViewportBuilder) -> (Vec<ViewportCommand>, bool) {
let mut commands = Vec::new();
if let Some(new_title) = &new.title {
if Some(new_title) != self.title.as_ref() {
self.title = Some(new_title.clone());
commands.push(ViewportCommand::Title(new_title.clone()));
}
}
if let Some(new_position) = new.position {
if Some(new_position) != self.position {
self.position = Some(new_position);
commands.push(ViewportCommand::OuterPosition(new_position));
}
}
if let Some(new_inner_size) = new.inner_size {
if Some(new_inner_size) != self.inner_size {
self.inner_size = Some(new_inner_size);
commands.push(ViewportCommand::InnerSize(new_inner_size));
}
}
if let Some(new_min_inner_size) = new.min_inner_size {
if Some(new_min_inner_size) != self.min_inner_size {
self.min_inner_size = Some(new_min_inner_size);
commands.push(ViewportCommand::MinInnerSize(new_min_inner_size));
}
}
if let Some(new_max_inner_size) = new.max_inner_size {
if Some(new_max_inner_size) != self.max_inner_size {
self.max_inner_size = Some(new_max_inner_size);
commands.push(ViewportCommand::MaxInnerSize(new_max_inner_size));
}
}
if let Some(new_fullscreen) = new.fullscreen {
if Some(new_fullscreen) != self.fullscreen {
self.fullscreen = Some(new_fullscreen);
commands.push(ViewportCommand::Fullscreen(new_fullscreen));
}
}
if let Some(new_maximized) = new.maximized {
if Some(new_maximized) != self.maximized {
self.maximized = Some(new_maximized);
commands.push(ViewportCommand::Maximized(new_maximized));
}
}
if let Some(new_resizable) = new.resizable {
if Some(new_resizable) != self.resizable {
self.resizable = Some(new_resizable);
commands.push(ViewportCommand::Resizable(new_resizable));
}
}
if let Some(new_transparent) = new.transparent {
if Some(new_transparent) != self.transparent {
self.transparent = Some(new_transparent);
commands.push(ViewportCommand::Transparent(new_transparent));
}
}
if let Some(new_decorations) = new.decorations {
if Some(new_decorations) != self.decorations {
self.decorations = Some(new_decorations);
commands.push(ViewportCommand::Decorations(new_decorations));
}
}
if let Some(new_icon) = &new.icon {
let is_new = match &self.icon {
Some(existing) => !Arc::ptr_eq(new_icon, existing),
None => true,
};
if is_new {
commands.push(ViewportCommand::WindowIcon(Some(new_icon.clone())));
self.icon = Some(new_icon.clone());
}
}
if let Some(new_visible) = new.visible {
if Some(new_visible) != self.active {
self.visible = Some(new_visible);
commands.push(ViewportCommand::Visible(new_visible));
}
}
if let Some(new_hittest) = new.hittest {
if Some(new_hittest) != self.hittest {
self.hittest = Some(new_hittest);
commands.push(ViewportCommand::CursorHitTest(new_hittest));
}
}
// TODO: Implement compare for windows buttons
let mut recreate_window = false;
if let Some(new_active) = new.active {
if Some(new_active) != self.active {
self.active = Some(new_active);
recreate_window = true;
}
}
if let Some(new_close_button) = new.close_button {
if Some(new_close_button) != self.close_button {
self.close_button = Some(new_close_button);
recreate_window = true;
}
}
if let Some(new_minimize_button) = new.minimize_button {
if Some(new_minimize_button) != self.minimize_button {
self.minimize_button = Some(new_minimize_button);
recreate_window = true;
}
}
if let Some(new_maximized_button) = new.maximize_button {
if Some(new_maximized_button) != self.maximize_button {
self.maximize_button = Some(new_maximized_button);
recreate_window = true;
}
}
if let Some(new_title_hidden) = new.title_hidden {
if Some(new_title_hidden) != self.title_hidden {
self.title_hidden = Some(new_title_hidden);
recreate_window = true;
}
}
if let Some(new_titlebar_transparent) = new.titlebar_transparent {
if Some(new_titlebar_transparent) != self.titlebar_transparent {
self.titlebar_transparent = Some(new_titlebar_transparent);
recreate_window = true;
}
}
if let Some(new_fullsize_content_view) = new.fullsize_content_view {
if Some(new_fullsize_content_view) != self.fullsize_content_view {
self.fullsize_content_view = Some(new_fullsize_content_view);
recreate_window = true;
}
}
(commands, recreate_window)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum WindowLevel {
Normal,
AlwaysOnBottom,
AlwaysOnTop,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum IMEPurpose {
Normal,
Password,
Terminal,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum SystemTheme {
Light,
Dark,
SystemDefault,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum CursorGrab {
None,
Confined,
Locked,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum UserAttentionType {
Informational,
Critical,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ResizeDirection {
North,
South,
West,
NorthEast,
SouthEast,
NorthWest,
SouthWest,
}
/// You can send a [`ViewportCommand`] to the viewport with [`Context::send_viewport_command`].
///
/// All coordinates are in logical points.
///
/// This is essentially a way to diff [`ViewportBuilder`].
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ViewportCommand {
/// Set the title
Title(String),
/// Turn the window transparent or not.
Transparent(bool),
/// Set the visibility of the window.
Visible(bool),
/// Moves the window with the left mouse button until the button is released.
///
/// There's no guarantee that this will work unless the left mouse button was pressed
/// immediately before this function is called.
StartDrag,
/// Set the outer position of the viewport, i.e. moves the window.
OuterPosition(Pos2),
/// Should be bigger then 0
InnerSize(Vec2),
/// Should be bigger then 0
MinInnerSize(Vec2),
/// Should be bigger then 0
MaxInnerSize(Vec2),
/// Should be bigger then 0
ResizeIncrements(Option<Vec2>),
/// Begin resizing the viewport with the left mouse button until the button is released.
///
/// There's no guarantee that this will work unless the left mouse button was pressed
/// immediately before this function is called.
BeginResize(ResizeDirection),
/// Can the window be resized?
Resizable(bool),
/// Set which window buttons are enabled
EnableButtons {
close: bool,
minimized: bool,
maximize: bool,
},
Minimized(bool),
Maximized(bool),
Fullscreen(bool),
/// Show window decorations, i.e. the chrome around the content
/// with the title bar, close buttons, resize handles, etc.
Decorations(bool),
WindowLevel(WindowLevel),
WindowIcon(Option<Arc<ColorImage>>),
IMEPosition(Pos2),
IMEAllowed(bool),
IMEPurpose(IMEPurpose),
RequestUserAttention(Option<UserAttentionType>),
SetTheme(SystemTheme),
ContentProtected(bool),
/// Will probably not work as expected!
CursorPosition(Pos2),
CursorGrab(CursorGrab),
CursorVisible(bool),
CursorHitTest(bool),
}
/// Describes a viewport, i.e. a native window.
#[derive(Clone)]
pub struct ViewportOutput {
/// Id of our parent viewport.
pub parent: ViewportId,
/// What type of viewport are we?
///
/// This will never be [`ViewportClass::Embedded`],
/// since those don't result in real viewports.
pub class: ViewportClass,
/// The window attrbiutes such as title, position, size, etc.
pub builder: ViewportBuilder,
/// The user-code that shows the GUI, used for deferred viewports.
///
/// `None` for immediate viewports and the ROOT viewport.
pub viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
/// Commands to change the viewport, e.g. window title and size.
pub commands: Vec<ViewportCommand>,
/// Schedulare a repaint of this viewport after this delay.
///
/// It is preferably to instead install a [`Context::set_request_repaint_callback`],
/// but if you haven't, you can use this instead.
///
/// If the duration is zero, schedule a repaint immediately.
pub repaint_delay: std::time::Duration,
}
impl ViewportOutput {
/// Add on new output.
pub fn append(&mut self, newer: Self) {
let Self {
parent,
class,
builder,
viewport_ui_cb,
mut commands,
repaint_delay,
} = newer;
self.parent = parent;
self.class = class;
self.builder.patch(&builder);
self.viewport_ui_cb = viewport_ui_cb;
self.commands.append(&mut commands);
self.repaint_delay = self.repaint_delay.min(repaint_delay);
}
}
/// Viewport for immediate rendering.
pub struct ImmediateViewport<'a> {
/// Id of us and our parent.
pub ids: ViewportIdPair,
pub builder: ViewportBuilder,
/// The user-code that shows the GUI.
pub viewport_ui_cb: Box<dyn FnOnce(&Context) + 'a>,
}

4
crates/egui_demo_lib/benches/benchmark.rs

@ -16,7 +16,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
let full_output = ctx.run(RawInput::default(), |ctx| { let full_output = ctx.run(RawInput::default(), |ctx| {
demo_windows.ui(ctx); demo_windows.ui(ctx);
}); });
ctx.tessellate(full_output.shapes) ctx.tessellate(full_output.shapes, full_output.pixels_per_point)
}); });
}); });
@ -32,7 +32,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
demo_windows.ui(ctx); demo_windows.ui(ctx);
}); });
c.bench_function("demo_only_tessellate", |b| { c.bench_function("demo_only_tessellate", |b| {
b.iter(|| ctx.tessellate(full_output.shapes.clone())); b.iter(|| ctx.tessellate(full_output.shapes.clone(), full_output.pixels_per_point));
}); });
} }

4
crates/egui_demo_lib/src/lib.rs

@ -77,7 +77,7 @@ fn test_egui_e2e() {
let full_output = ctx.run(raw_input.clone(), |ctx| { let full_output = ctx.run(raw_input.clone(), |ctx| {
demo_windows.ui(ctx); demo_windows.ui(ctx);
}); });
let clipped_primitives = ctx.tessellate(full_output.shapes); let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point);
assert!(!clipped_primitives.is_empty()); assert!(!clipped_primitives.is_empty());
} }
} }
@ -96,7 +96,7 @@ fn test_egui_zero_window_size() {
let full_output = ctx.run(raw_input.clone(), |ctx| { let full_output = ctx.run(raw_input.clone(), |ctx| {
demo_windows.ui(ctx); demo_windows.ui(ctx);
}); });
let clipped_primitives = ctx.tessellate(full_output.shapes); let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point);
assert!( assert!(
clipped_primitives.is_empty(), clipped_primitives.is_empty(),
"There should be nothing to show, has at least one primitive with clip_rect: {:?}", "There should be nothing to show, has at least one primitive with clip_rect: {:?}",

39
crates/egui_glow/examples/pure_glow.rs

@ -17,7 +17,7 @@ impl GlutinWindowContext {
// refactor this function to use `glutin-winit` crate eventually. // refactor this function to use `glutin-winit` crate eventually.
// preferably add android support at the same time. // preferably add android support at the same time.
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe fn new(event_loop: &winit::event_loop::EventLoopWindowTarget<()>) -> Self { unsafe fn new(event_loop: &winit::event_loop::EventLoopWindowTarget<UserEvent>) -> Self {
use egui::NumExt; use egui::NumExt;
use glutin::context::NotCurrentGlContextSurfaceAccessor; use glutin::context::NotCurrentGlContextSurfaceAccessor;
use glutin::display::GetGlDisplay; use glutin::display::GetGlDisplay;
@ -142,20 +142,37 @@ impl GlutinWindowContext {
} }
} }
#[derive(Debug)]
pub enum UserEvent {
Redraw(std::time::Duration),
}
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 = winit::event_loop::EventLoopBuilder::with_user_event().build(); let event_loop = winit::event_loop::EventLoopBuilder::<UserEvent>::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);
let mut egui_glow = egui_glow::EguiGlow::new(&event_loop, gl.clone(), None); let mut egui_glow = egui_glow::EguiGlow::new(&event_loop, gl.clone(), None, None);
let event_loop_proxy = egui::mutex::Mutex::new(event_loop.create_proxy());
egui_glow
.egui_ctx
.set_request_repaint_callback(move |info| {
event_loop_proxy
.lock()
.send_event(UserEvent::Redraw(info.delay))
.expect("Cannot send event");
});
let mut repaint_delay = std::time::Duration::MAX;
event_loop.run(move |event, _, control_flow| { event_loop.run(move |event, _, control_flow| {
let mut redraw = || { let mut redraw = || {
let mut quit = false; let mut quit = false;
let repaint_after = egui_glow.run(gl_window.window(), |egui_ctx| { egui_glow.run(gl_window.window(), |egui_ctx| {
egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| { egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| {
ui.heading("Hello World!"); ui.heading("Hello World!");
if ui.button("Quit").clicked() { if ui.button("Quit").clicked() {
@ -167,13 +184,13 @@ fn main() {
*control_flow = if quit { *control_flow = if quit {
winit::event_loop::ControlFlow::Exit winit::event_loop::ControlFlow::Exit
} else if repaint_after.is_zero() { } else if repaint_delay.is_zero() {
gl_window.window().request_redraw(); gl_window.window().request_redraw();
winit::event_loop::ControlFlow::Poll winit::event_loop::ControlFlow::Poll
} else if let Some(repaint_after_instant) = } else if let Some(repaint_delay_instant) =
std::time::Instant::now().checked_add(repaint_after) std::time::Instant::now().checked_add(repaint_delay)
{ {
winit::event_loop::ControlFlow::WaitUntil(repaint_after_instant) winit::event_loop::ControlFlow::WaitUntil(repaint_delay_instant)
} else { } else {
winit::event_loop::ControlFlow::Wait winit::event_loop::ControlFlow::Wait
}; };
@ -224,6 +241,10 @@ fn main() {
gl_window.window().request_redraw(); gl_window.window().request_redraw();
} }
} }
winit::event::Event::UserEvent(UserEvent::Redraw(delay)) => {
repaint_delay = delay;
}
winit::event::Event::LoopDestroyed => { winit::event::Event::LoopDestroyed => {
egui_glow.destroy(); egui_glow.destroy();
} }
@ -239,7 +260,7 @@ fn main() {
} }
fn create_display( fn create_display(
event_loop: &winit::event_loop::EventLoopWindowTarget<()>, event_loop: &winit::event_loop::EventLoopWindowTarget<UserEvent>,
) -> (GlutinWindowContext, glow::Context) { ) -> (GlutinWindowContext, glow::Context) {
let glutin_window_context = unsafe { GlutinWindowContext::new(event_loop) }; let glutin_window_context = unsafe { GlutinWindowContext::new(event_loop) };
let gl = unsafe { let gl = unsafe {

2
crates/egui_glow/src/lib.rs

@ -13,7 +13,7 @@
pub mod painter; pub mod painter;
pub use glow; pub use glow;
pub use painter::{CallbackFn, Painter}; pub use painter::{CallbackFn, Painter, PainterError};
mod misc_util; mod misc_util;
mod shader_version; mod shader_version;
mod vao; mod vao;

31
crates/egui_glow/src/painter.rs

@ -34,6 +34,24 @@ impl TextureFilterExt for egui::TextureFilter {
} }
} }
#[derive(Debug)]
pub struct PainterError(String);
impl std::error::Error for PainterError {}
impl std::fmt::Display for PainterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "OpenGL: {}", self.0)
}
}
impl From<String> for PainterError {
#[inline]
fn from(value: String) -> Self {
Self(value)
}
}
/// An OpenGL painter using [`glow`]. /// An OpenGL painter using [`glow`].
/// ///
/// This is responsible for painting egui and managing egui textures. /// This is responsible for painting egui and managing egui textures.
@ -103,7 +121,7 @@ impl Painter {
gl: Arc<glow::Context>, gl: Arc<glow::Context>,
shader_prefix: &str, shader_prefix: &str,
shader_version: Option<ShaderVersion>, shader_version: Option<ShaderVersion>,
) -> Result<Painter, String> { ) -> Result<Painter, PainterError> {
crate::profile_function!(); crate::profile_function!();
crate::check_for_gl_error_even_in_release!(&gl, "before Painter::new"); crate::check_for_gl_error_even_in_release!(&gl, "before Painter::new");
@ -121,7 +139,7 @@ impl Painter {
if gl.version().major < 2 { if gl.version().major < 2 {
// this checks on desktop that we are not using opengl 1.1 microsoft sw rendering context. // this checks on desktop that we are not using opengl 1.1 microsoft sw rendering context.
// ShaderVersion::get fn will segfault due to SHADING_LANGUAGE_VERSION (added in gl2.0) // ShaderVersion::get fn will segfault due to SHADING_LANGUAGE_VERSION (added in gl2.0)
return Err("egui_glow requires opengl 2.0+. ".to_owned()); return Err(PainterError("egui_glow requires opengl 2.0+. ".to_owned()));
} }
let max_texture_side = unsafe { gl.get_parameter_i32(glow::MAX_TEXTURE_SIZE) } as usize; let max_texture_side = unsafe { gl.get_parameter_i32(glow::MAX_TEXTURE_SIZE) } as usize;
@ -305,6 +323,10 @@ impl Painter {
(width_in_pixels, height_in_pixels) (width_in_pixels, height_in_pixels)
} }
pub fn clear(&self, screen_size_in_pixels: [u32; 2], clear_color: [f32; 4]) {
clear(&self.gl, screen_size_in_pixels, clear_color);
}
/// You are expected to have cleared the color buffer before calling this. /// You are expected to have cleared the color buffer before calling this.
pub fn paint_and_update_textures( pub fn paint_and_update_textures(
&mut self, &mut self,
@ -314,6 +336,7 @@ impl Painter {
textures_delta: &egui::TexturesDelta, textures_delta: &egui::TexturesDelta,
) { ) {
crate::profile_function!(); crate::profile_function!();
for (id, image_delta) in &textures_delta.set { for (id, image_delta) in &textures_delta.set {
self.set_texture(*id, image_delta); self.set_texture(*id, image_delta);
} }
@ -621,6 +644,8 @@ impl Painter {
} }
pub fn read_screen_rgba(&self, [w, h]: [u32; 2]) -> egui::ColorImage { pub fn read_screen_rgba(&self, [w, h]: [u32; 2]) -> egui::ColorImage {
crate::profile_function!();
let mut pixels = vec![0_u8; (w * h * 4) as usize]; let mut pixels = vec![0_u8; (w * h * 4) as usize];
unsafe { unsafe {
self.gl.read_pixels( self.gl.read_pixels(
@ -644,6 +669,8 @@ impl Painter {
} }
pub fn read_screen_rgb(&self, [w, h]: [u32; 2]) -> Vec<u8> { pub fn read_screen_rgb(&self, [w, h]: [u32; 2]) -> Vec<u8> {
crate::profile_function!();
let mut pixels = vec![0_u8; (w * h * 3) as usize]; let mut pixels = vec![0_u8; (w * h * 3) as usize];
unsafe { unsafe {
self.gl.read_pixels( self.gl.read_pixels(

63
crates/egui_glow/src/winit.rs

@ -1,15 +1,20 @@
use crate::shader_version::ShaderVersion;
pub use egui_winit; pub use egui_winit;
use egui_winit::winit;
pub use egui_winit::EventResponse; pub use egui_winit::EventResponse;
use egui::{ViewportId, ViewportIdPair, ViewportOutput};
use egui_winit::winit;
use crate::shader_version::ShaderVersion;
/// Use [`egui`] from a [`glow`] app based on [`winit`]. /// Use [`egui`] from a [`glow`] app based on [`winit`].
pub struct EguiGlow { pub struct EguiGlow {
pub egui_ctx: egui::Context, pub egui_ctx: egui::Context,
pub egui_winit: egui_winit::State, pub egui_winit: egui_winit::State,
pub painter: crate::Painter, pub painter: crate::Painter,
// output from the last update:
shapes: Vec<egui::epaint::ClippedShape>, shapes: Vec<egui::epaint::ClippedShape>,
pixels_per_point: f32,
textures_delta: egui::TexturesDelta, textures_delta: egui::TexturesDelta,
} }
@ -19,6 +24,7 @@ impl EguiGlow {
event_loop: &winit::event_loop::EventLoopWindowTarget<E>, event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
gl: std::sync::Arc<glow::Context>, gl: std::sync::Arc<glow::Context>,
shader_version: Option<ShaderVersion>, shader_version: Option<ShaderVersion>,
native_pixels_per_point: Option<f32>,
) -> Self { ) -> Self {
let painter = crate::Painter::new(gl, "", shader_version) let painter = crate::Painter::new(gl, "", shader_version)
.map_err(|err| { .map_err(|err| {
@ -26,11 +32,19 @@ impl EguiGlow {
}) })
.unwrap(); .unwrap();
let egui_winit = egui_winit::State::new(
event_loop,
native_pixels_per_point,
Some(painter.max_texture_side()),
);
let pixels_per_point = egui_winit.pixels_per_point();
Self { Self {
egui_ctx: Default::default(), egui_ctx: Default::default(),
egui_winit: egui_winit::State::new(event_loop), egui_winit,
painter, painter,
shapes: Default::default(), shapes: Default::default(),
pixels_per_point,
textures_delta: Default::default(), textures_delta: Default::default(),
} }
} }
@ -39,28 +53,37 @@ impl EguiGlow {
self.egui_winit.on_event(&self.egui_ctx, event) self.egui_winit.on_event(&self.egui_ctx, event)
} }
/// Returns the `Duration` of the timeout after which egui should be repainted even if there's no new events.
///
/// Call [`Self::paint`] later to paint. /// Call [`Self::paint`] later to paint.
pub fn run( pub fn run(&mut self, window: &winit::window::Window, run_ui: impl FnMut(&egui::Context)) {
&mut self, let raw_input = self
window: &winit::window::Window, .egui_winit
run_ui: impl FnMut(&egui::Context), .take_egui_input(window, ViewportIdPair::ROOT);
) -> std::time::Duration {
let raw_input = self.egui_winit.take_egui_input(window);
let egui::FullOutput { let egui::FullOutput {
platform_output, platform_output,
repaint_after,
textures_delta, textures_delta,
shapes, shapes,
pixels_per_point,
viewport_output,
} = self.egui_ctx.run(raw_input, run_ui); } = self.egui_ctx.run(raw_input, run_ui);
self.egui_winit if viewport_output.len() > 1 {
.handle_platform_output(window, &self.egui_ctx, platform_output); log::warn!("Multiple viewports not yet supported by EguiGlow");
}
for (_, ViewportOutput { commands, .. }) in viewport_output {
egui_winit::process_viewport_commands(commands, window, true);
}
self.egui_winit.handle_platform_output(
window,
ViewportId::ROOT,
&self.egui_ctx,
platform_output,
);
self.shapes = shapes; self.shapes = shapes;
self.pixels_per_point = pixels_per_point;
self.textures_delta.append(textures_delta); self.textures_delta.append(textures_delta);
repaint_after
} }
/// Paint the results of the last call to [`Self::run`]. /// Paint the results of the last call to [`Self::run`].
@ -72,13 +95,11 @@ impl EguiGlow {
self.painter.set_texture(id, &image_delta); self.painter.set_texture(id, &image_delta);
} }
let clipped_primitives = self.egui_ctx.tessellate(shapes); let pixels_per_point = self.pixels_per_point;
let clipped_primitives = self.egui_ctx.tessellate(shapes, pixels_per_point);
let dimensions: [u32; 2] = window.inner_size().into(); let dimensions: [u32; 2] = window.inner_size().into();
self.painter.paint_primitives( self.painter
dimensions, .paint_primitives(dimensions, pixels_per_point, &clipped_primitives);
self.egui_ctx.pixels_per_point(),
&clipped_primitives,
);
for id in textures_delta.free.drain(..) { for id in textures_delta.free.drain(..) {
self.painter.free_texture(id); self.painter.free_texture(id);

9
crates/epaint/src/image.rs

@ -238,6 +238,15 @@ impl From<Arc<ColorImage>> for ImageData {
} }
} }
impl std::fmt::Debug for ColorImage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ColorImage")
.field("size", &self.size)
.field("pixel-count", &self.pixels.len())
.finish_non_exhaustive()
}
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// A single-channel image designed for the font texture. /// A single-channel image designed for the font texture.

15
examples/multiple_viewports/Cargo.toml

@ -0,0 +1,15 @@
[package]
name = "multiple_viewports"
version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.70"
publish = false
[dependencies]
eframe = { path = "../../crates/eframe", features = [
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] }
env_logger = "0.10"

7
examples/multiple_viewports/README.md

@ -0,0 +1,7 @@
Example how to show multiple viewports (native windows) can be created in `egui` when using the `eframe` backend.
```sh
cargo run -p multiple_viewports
```
For a more advanced example, see [../test_viewports].

102
examples/multiple_viewports/src/main.rs

@ -0,0 +1,102 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use eframe::egui;
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(320.0, 240.0)),
..Default::default()
};
eframe::run_native(
"Multiple viewports",
options,
Box::new(|_cc| Box::<MyApp>::default()),
)
}
#[derive(Default)]
struct MyApp {
/// Immediate viewports are show immediately, so passing state to/from them is easy.
/// The downside is that their painting is linked with the parent viewport:
/// if either needs repainting, they are both repainted.
show_immediate_viewport: bool,
/// Deferred viewports run independent of the parent viewport, which can save
/// CPU if only some of the viewports require repainting.
/// However, this requires passing state with `Arc` and locks.
show_deferred_viewport: Arc<AtomicBool>,
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.label("Hello from the root viewport");
ui.checkbox(
&mut self.show_immediate_viewport,
"Show immediate child viewport",
);
let mut show_deferred_viewport = self.show_deferred_viewport.load(Ordering::Relaxed);
ui.checkbox(&mut show_deferred_viewport, "Show deferred child viewport");
self.show_deferred_viewport
.store(show_deferred_viewport, Ordering::Relaxed);
});
if self.show_immediate_viewport {
ctx.show_viewport_immediate(
egui::ViewportId::from_hash_of("immediate_viewport"),
egui::ViewportBuilder::default()
.with_title("Immediate Viewport")
.with_inner_size([200.0, 100.0]),
|ctx, class| {
assert!(
class == egui::ViewportClass::Immediate,
"This egui backend doesn't support multiple viewports"
);
egui::CentralPanel::default().show(ctx, |ui| {
ui.label("Hello from immediate viewport");
});
if ctx.input(|i| i.raw.viewport.close_requested) {
// Tell parent viewport that we should not show next frame:
self.show_immediate_viewport = false;
ctx.request_repaint(); // make sure there is a next frame
}
},
);
}
if self.show_deferred_viewport.load(Ordering::Relaxed) {
let show_deferred_viewport = self.show_deferred_viewport.clone();
ctx.show_viewport_immediate(
egui::ViewportId::from_hash_of("deferred_viewport"),
egui::ViewportBuilder::default()
.with_title("Deferred Viewport")
.with_inner_size([200.0, 100.0]),
|ctx, class| {
assert!(
class == egui::ViewportClass::Deferred,
"This egui backend doesn't support multiple viewports"
);
egui::CentralPanel::default().show(ctx, |ui| {
ui.label("Hello from deferred viewport");
});
if ctx.input(|i| i.raw.viewport.close_requested) {
// Tell parent to close us.
show_deferred_viewport.store(false, Ordering::Relaxed);
ctx.request_repaint(); // make sure there is a next frame
}
},
);
}
}
}

17
examples/test_viewports/Cargo.toml

@ -0,0 +1,17 @@
[package]
name = "test_viewports"
version = "0.1.0"
authors = ["konkitoman"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.70"
publish = false
[features]
wgpu = ["eframe/wgpu"]
[dependencies]
eframe = { path = "../../crates/eframe", features = [
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] }
env_logger = "0.10"

3
examples/test_viewports/README.md

@ -0,0 +1,3 @@
This is a test of the viewports feature of eframe and egui, where we show off using multiple windows.
For a simple example, see [../multiple_viewports].

476
examples/test_viewports/src/main.rs

@ -0,0 +1,476 @@
use std::sync::Arc;
use eframe::egui;
use egui::{mutex::RwLock, Id, InnerResponse, ViewportBuilder, ViewportId};
// Drag-and-drop between windows is not yet implemented, but if you wanna work on it, enable this:
pub const DRAG_AND_DROP_TEST: bool = false;
fn main() {
env_logger::init(); // Use `RUST_LOG=debug` to see logs.
let _ = eframe::run_native(
"Viewports",
eframe::NativeOptions {
#[cfg(feature = "wgpu")]
renderer: eframe::Renderer::Wgpu,
initial_window_size: Some(egui::Vec2::new(450.0, 400.0)),
..Default::default()
},
Box::new(|_| Box::<App>::default()),
);
}
pub struct ViewportState {
pub id: ViewportId,
pub visible: bool,
pub immediate: bool,
pub title: String,
pub children: Vec<Arc<RwLock<ViewportState>>>,
}
impl ViewportState {
pub fn new_deferred(
title: &'static str,
children: Vec<Arc<RwLock<ViewportState>>>,
) -> Arc<RwLock<Self>> {
Arc::new(RwLock::new(Self {
id: ViewportId::from_hash_of(title),
visible: false,
immediate: false,
title: title.into(),
children,
}))
}
pub fn new_immediate(
title: &'static str,
children: Vec<Arc<RwLock<ViewportState>>>,
) -> Arc<RwLock<Self>> {
Arc::new(RwLock::new(Self {
id: ViewportId::from_hash_of(title),
visible: false,
immediate: true,
title: title.into(),
children,
}))
}
pub fn show(vp_state: Arc<RwLock<ViewportState>>, ctx: &egui::Context) {
if !vp_state.read().visible {
return;
}
let vp_id = vp_state.read().id;
let immediate = vp_state.read().immediate;
let title = vp_state.read().title.clone();
let viewport = ViewportBuilder::default()
.with_title(&title)
.with_inner_size([450.0, 400.0]);
if immediate {
let mut vp_state = vp_state.write();
ctx.show_viewport_immediate(vp_id, viewport, move |ctx, class| {
show_as_popup(ctx, class, &title, vp_id.into(), |ui: &mut egui::Ui| {
generic_child_ui(ui, &mut vp_state);
});
});
} else {
let count = Arc::new(RwLock::new(0));
ctx.show_viewport(vp_id, viewport, move |ctx, class| {
let mut vp_state = vp_state.write();
let count = count.clone();
show_as_popup(
ctx,
class,
&title,
vp_id.into(),
move |ui: &mut egui::Ui| {
let current_count = *count.read();
ui.label(format!("Callback has been reused {current_count} times"));
*count.write() += 1;
generic_child_ui(ui, &mut vp_state);
},
);
});
}
}
pub fn set_visible_recursive(&mut self, visible: bool) {
self.visible = visible;
for child in &self.children {
child.write().set_visible_recursive(true);
}
}
}
pub struct App {
top: Vec<Arc<RwLock<ViewportState>>>,
}
impl Default for App {
fn default() -> Self {
Self {
top: vec![
ViewportState::new_deferred(
"Top Deferred Viewport",
vec![
ViewportState::new_deferred(
"DD: Deferred Viewport in Deferred Viewport",
vec![],
),
ViewportState::new_immediate(
"DS: Immediate Viewport in Deferred Viewport",
vec![],
),
],
),
ViewportState::new_immediate(
"Top Immediate Viewport",
vec![
ViewportState::new_deferred(
"SD: Deferred Viewport in Immediate Viewport",
vec![],
),
ViewportState::new_immediate(
"SS: Immediate Viewport in Immediate Viewport",
vec![],
),
],
),
],
}
}
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Root viewport");
{
let mut embed_viewports = ctx.embed_viewports();
ui.checkbox(&mut embed_viewports, "Embed all viewports");
if ui.button("Open all viewports").clicked() {
for viewport in &self.top {
viewport.write().set_visible_recursive(true);
}
}
ctx.set_embed_viewports(embed_viewports);
}
generic_ui(ui, &self.top);
});
}
}
/// This will make the content as a popup if cannot has his own native window
fn show_as_popup(
ctx: &egui::Context,
class: egui::ViewportClass,
title: &str,
id: Id,
content: impl FnOnce(&mut egui::Ui),
) {
if class == egui::ViewportClass::Embedded {
// Not a real viewport
egui::Window::new(title).id(id).show(ctx, content);
} else {
egui::CentralPanel::default().show(ctx, |ui| ui.push_id(id, content));
}
}
fn generic_child_ui(ui: &mut egui::Ui, vp_state: &mut ViewportState) {
ui.horizontal(|ui| {
ui.label("Title:");
if ui.text_edit_singleline(&mut vp_state.title).changed() {
// Title changes happen at the parent level:
ui.ctx().request_repaint_of(ui.ctx().parent_viewport_id());
}
});
generic_ui(ui, &vp_state.children);
}
fn generic_ui(ui: &mut egui::Ui, children: &[Arc<RwLock<ViewportState>>]) {
let container_id = ui.id();
let ctx = ui.ctx().clone();
ui.label(format!(
"Frame nr: {} (this increases when this viewport is being rendered)",
ctx.frame_nr()
));
ui.horizontal(|ui| {
let mut show_spinner =
ui.data_mut(|data| *data.get_temp_mut_or(container_id.with("show_spinner"), false));
ui.checkbox(&mut show_spinner, "Show Spinner (forces repaint)");
if show_spinner {
ui.spinner();
}
ui.data_mut(|data| data.insert_temp(container_id.with("show_spinner"), show_spinner));
});
ui.add_space(8.0);
ui.label(format!("Viewport Id: {:?}", ctx.viewport_id()));
ui.label(format!(
"Parent Viewport Id: {:?}",
ctx.parent_viewport_id()
));
ui.add_space(8.0);
if let Some(inner_rect) = ctx.input(|i| i.raw.viewport.inner_rect_px) {
ui.label(format!(
"Inner Rect: Pos: {:?}, Size: {:?}",
inner_rect.min,
inner_rect.size()
));
}
if let Some(outer_rect) = ctx.input(|i| i.raw.viewport.outer_rect_px) {
ui.label(format!(
"Outer Rect: Pos: {:?}, Size: {:?}",
outer_rect.min,
outer_rect.size()
));
}
let tmp_pixels_per_point = ctx.pixels_per_point();
let mut pixels_per_point = ui.data_mut(|data| {
*data.get_temp_mut_or(container_id.with("pixels_per_point"), tmp_pixels_per_point)
});
let res = ui.add(
egui::DragValue::new(&mut pixels_per_point)
.prefix("Pixels per Point: ")
.speed(0.1)
.clamp_range(0.5..=4.0),
);
if res.drag_released() {
ctx.set_pixels_per_point(pixels_per_point);
}
if res.dragged() {
ui.data_mut(|data| {
data.insert_temp(container_id.with("pixels_per_point"), pixels_per_point);
});
} else {
ui.data_mut(|data| {
data.insert_temp(container_id.with("pixels_per_point"), tmp_pixels_per_point);
});
}
egui::gui_zoom::zoom_with_keyboard_shortcuts(&ctx, None);
if ctx.viewport_id() != ctx.parent_viewport_id() {
let parent = ctx.parent_viewport_id();
if ui.button("Set parent pos 0,0").clicked() {
ctx.send_viewport_command_to(
parent,
egui::ViewportCommand::OuterPosition(egui::pos2(0.0, 0.0)),
);
}
}
if DRAG_AND_DROP_TEST {
drag_and_drop_test(ui);
}
if !children.is_empty() {
ui.separator();
ui.heading("Children:");
for child in children {
let visible = {
let mut child_lock = child.write();
let ViewportState { visible, title, .. } = &mut *child_lock;
ui.checkbox(visible, title.as_str());
*visible
};
if visible {
ViewportState::show(child.clone(), &ctx);
}
}
}
}
// ----------------------------------------------------------------------------
// Drag-and-drop between windows is not yet implemented, but there is some test code for it here:
fn drag_and_drop_test(ui: &mut egui::Ui) {
use std::collections::HashMap;
use std::sync::OnceLock;
let container_id = ui.id();
const COLS: usize = 2;
static DATA: OnceLock<RwLock<DragAndDrop>> = OnceLock::new();
let data = DATA.get_or_init(Default::default);
data.write().init(container_id);
#[derive(Default)]
struct DragAndDrop {
containers_data: HashMap<Id, Vec<Vec<Id>>>,
data: HashMap<Id, String>,
counter: usize,
is_dragged: Option<Id>,
}
impl DragAndDrop {
fn init(&mut self, container: Id) {
if !self.containers_data.contains_key(&container) {
for i in 0..COLS {
self.insert(
container,
i,
format!("From: {container:?}, and is: {}", self.counter),
);
}
}
}
fn insert(&mut self, container: Id, col: usize, value: impl Into<String>) {
assert!(col <= COLS, "The coll should be less then: {COLS}");
let value: String = value.into();
let id = Id::new(format!("%{}% {}", self.counter, &value));
self.data.insert(id, value);
let viewport_data = self.containers_data.entry(container).or_insert_with(|| {
let mut res = Vec::new();
res.resize_with(COLS, Default::default);
res
});
self.counter += 1;
viewport_data[col].push(id);
}
fn cols(&self, container: Id, col: usize) -> Vec<(Id, String)> {
assert!(col <= COLS, "The col should be less then: {COLS}");
let container_data = &self.containers_data[&container];
container_data[col]
.iter()
.map(|id| (*id, self.data[id].clone()))
.collect()
}
/// Move element ID to Viewport and col
fn mov(&mut self, to: Id, col: usize) {
let Some(id) = self.is_dragged.take() else {
return;
};
assert!(col <= COLS, "The col should be less then: {COLS}");
// Should be a better way to do this!
for container_data in self.containers_data.values_mut() {
for ids in container_data {
ids.retain(|i| *i != id);
}
}
if let Some(container_data) = self.containers_data.get_mut(&to) {
container_data[col].push(id);
}
}
fn dragging(&mut self, id: Id) {
self.is_dragged = Some(id);
}
}
ui.separator();
ui.label("Drag and drop:");
ui.columns(COLS, |ui| {
for col in 0..COLS {
let data = DATA.get().unwrap();
let ui = &mut ui[col];
let mut is_dragged = None;
let res = drop_target(ui, |ui| {
ui.set_min_height(60.0);
for (id, value) in data.read().cols(container_id, col) {
drag_source(ui, id, |ui| {
ui.add(egui::Label::new(value).sense(egui::Sense::click()));
if ui.memory(|mem| mem.is_being_dragged(id)) {
is_dragged = Some(id);
}
});
}
});
if let Some(id) = is_dragged {
data.write().dragging(id);
}
if res.response.hovered() && ui.input(|i| i.pointer.any_released()) {
data.write().mov(container_id, col);
}
}
});
}
// This is taken from crates/egui_demo_lib/src/debo/drag_and_drop.rs
fn drag_source<R>(
ui: &mut egui::Ui,
id: egui::Id,
body: impl FnOnce(&mut egui::Ui) -> R,
) -> InnerResponse<R> {
let is_being_dragged = ui.memory(|mem| mem.is_being_dragged(id));
if !is_being_dragged {
let res = ui.scope(body);
// Check for drags:
let response = ui.interact(res.response.rect, id, egui::Sense::drag());
if response.hovered() {
ui.ctx().set_cursor_icon(egui::CursorIcon::Grab);
}
res
} else {
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
// Paint the body to a new layer:
let layer_id = egui::LayerId::new(egui::Order::Tooltip, id);
let res = ui.with_layer_id(layer_id, body);
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
let delta = pointer_pos - res.response.rect.center();
ui.ctx().translate_layer(layer_id, delta);
}
res
}
}
// This is taken from crates/egui_demo_lib/src/debo/drag_and_drop.rs
fn drop_target<R>(
ui: &mut egui::Ui,
body: impl FnOnce(&mut egui::Ui) -> R,
) -> egui::InnerResponse<R> {
let is_being_dragged = ui.memory(|mem| mem.is_anything_being_dragged());
let margin = egui::Vec2::splat(ui.visuals().clip_rect_margin); // 3.0
let background_id = ui.painter().add(egui::Shape::Noop);
let available_rect = ui.available_rect_before_wrap();
let inner_rect = available_rect.shrink2(margin);
let mut content_ui = ui.child_ui(inner_rect, *ui.layout());
let ret = body(&mut content_ui);
let outer_rect =
egui::Rect::from_min_max(available_rect.min, content_ui.min_rect().max + margin);
let (rect, response) = ui.allocate_at_least(outer_rect.size(), egui::Sense::hover());
let style = if is_being_dragged && response.hovered() {
ui.visuals().widgets.active
} else {
ui.visuals().widgets.inactive
};
let fill = style.bg_fill;
let stroke = style.bg_stroke;
ui.painter().set(
background_id,
egui::epaint::RectShape::new(rect, style.rounding, fill, stroke),
);
egui::InnerResponse::new(ret, response)
}

2
scripts/check.sh

@ -37,7 +37,7 @@ cargo doc --document-private-items --no-deps --all-features
(cd crates/egui_extras && cargo check --no-default-features) (cd crates/egui_extras && cargo check --no-default-features)
(cd crates/egui_glow && cargo check --no-default-features) (cd crates/egui_glow && cargo check --no-default-features)
(cd crates/egui-winit && cargo check --no-default-features --features "wayland") (cd crates/egui-winit && cargo check --no-default-features --features "wayland")
(cd crates/egui-winit && cargo check --no-default-features --features "winit/x11") (cd crates/egui-winit && cargo check --no-default-features --features "x11")
(cd crates/emath && cargo check --no-default-features) (cd crates/emath && cargo check --no-default-features)
(cd crates/epaint && cargo check --no-default-features --release) (cd crates/epaint && cargo check --no-default-features --release)
(cd crates/epaint && cargo check --no-default-features) (cd crates/epaint && cargo check --no-default-features)

Loading…
Cancel
Save