Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple viewports/windows #3172

Merged
merged 367 commits into from
Nov 16, 2023
Merged

Multiple viewports/windows #3172

merged 367 commits into from
Nov 16, 2023

Conversation

konkitoman
Copy link
Contributor

@konkitoman konkitoman commented Jul 23, 2023


(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 #3556


Outdated PR description by @konkitoman

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:

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!

New

  • 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

Changes

  • 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

- repaint_after
+ viewports
+ viewport_commands
  • egui::RawInput
+ inner_rect
+ outer_rect
  • egui::Event
+ WindowEvent

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

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

egui popups, context_menu, tooltip

To be a native window

@emilk
Copy link
Owner

emilk commented Aug 9, 2023

Whoa, this looks very promising - thanks for working on this ❤️

I tried running cargo run -p viewports on my Mac and after resizing the window and clicking "Show Sync Viewport" I do indeed get two windows 🥳

My biggest feedback so far is to add docstrings to the code. I'm trying to follow along in examples/viewports/src/main.rs but it is quite opaque what default_embedded, show_async, get_viewport_id, set_render_sync_callback etc does.

Second piece of feedback: wrap the naked u64 viewport id in a newtype, e.g. struct ViewportId(u64). That way we get type safety and better readability of the code (I can look at the docstring of ViewportId to get an explanation of what a viewport id is).

Lastly: run cargo cranky on the code ;)

@konkitoman
Copy link
Contributor Author

Thank you for more motivation and feedback! ❤️

examples/viewports/src/main.rs is currently my playground, and is really messy!
All current errors are in crates/glow because is a backend and currently i am only working for eframe glow and winit!
crates/glow should have support for multiples windows?

Today i will add ViewportId and some documentation!

@konkitoman
Copy link
Contributor Author

This is the end of the day for me!
Now every where viewport id is needed, now is used ViewportId
ViewportId cannot be created, only we can get the main viewport id with ViewportId::MAIN
In the feature will be able to get the ViewportId of a viewport if we know his name!

I updated the viewports example and i added some documentation!

Tomorrow i will make every Popup to use a Context::create_viewport_sync to be rendered!

@konkitoman
Copy link
Contributor Author

This is the end of the day for me!

I made crates/egui/src/containers/popup.rs show_tooltip_area_dyn to create a sync viewport!
Now every tool-tip will open a native window if he can, but is really broken because the window has a fixed size!

I changed ViewportBuilder, everything in it to be optional. this will allow to create a viewport and run stuff in it without changing the window attributes, this will be used for was_tooltip_open_last_frame or to change a window attributes with commands manually.
I implemented all winit window attributes for ViewportCommand

And move code from eframe to egui-winit to be more generic.

@konkitoman
Copy link
Contributor Author

@emilk
The last commit 4bbdab1
The screen_rect should not work like that?

Is was not storing the window position!
I was always Pos2::ZERO as the position.

If did i understated screen_rect purpose wrong, how the window position should be stored?

@emilk
Copy link
Owner

emilk commented Aug 15, 2023

All coordinates in egui, including ctx.screen_rect, is relative to the native window containing egui. egui has no notion of anything outside the main window (at least until this PR!).

How do you plan on handling the coordinate system of multiple windows?

@konkitoman
Copy link
Contributor Author

konkitoman commented Aug 15, 2023

Currently in ctx.screen_rect.min is the outside position of the native window and when we cannot get the outside position will be Pos2::ZERO
Currently in ctx.screen_rect.max is the exact size of the window size!

I can make it relative so the ctx.screen_rect.size() to work again!
Should i do that?

You should run viewports example to see how is currently working!
Currently the window is not redrawn if is moved!
You should see like this:
This is on a KDE Plasma X11 system!
image

You could access other viewport screen_rect with

ctx.input_for(ViewportId::MAIN, |i| println!("Screen rect: {}", i.screen_rect()))

You can get the ViewportId of a viewport with

let viewport_id: Option<ViewportId> = ctx.get_viewport_id_by_id("Win 2")

@stefnotch
Copy link

If you see this

Please give feedback!

If I want to try this out, is there any sort of feedback you'd be looking for in particular? Or should I simply tell you if I notice anything odd.

@konkitoman
Copy link
Contributor Author

@stefnotch
I am looking for feedback to find out what is needed to build a docking system!

@stefnotch
Copy link

stefnotch commented Aug 23, 2023

Is it intentional that the windows apparently don't update unless they're being resized? I tried out the viewport demo, and this is what I got. I cannot press any of the buttons.

Code_2023-08-23-0321.webm

Operating system: Windows 10

@konkitoman
Copy link
Contributor Author

Is it intentional that the windows apparently don't update unless they're being resized? I tried out the viewport demo, and this is what I got. I cannot press any of the buttons.

Code_2023-08-23-0321.webm
Operating system: Windows 10

That is not normal!
This should fix the issue 3b41253
Please tell me if that fixed the issue!

@stefnotch
Copy link

@konkitoman Lovely, that does fix the issue.
I did find another odd bug, where the application will hang while the console spams

Set size!
Set size!
Set size!
Set size!
Set size!

Operating system: Windows 10

Steps to reproduce:
Start the viewport demo. Press "Show Async Window". (Or press all of them.)
Then quickly toggle the "Force embedding". After a few tries, the application lands in that bugged state.

firefox_2023-08-23-0324.webm

@konkitoman
Copy link
Contributor Author

@konkitoman Lovely, that does fix the issue. I did find another odd bug, where the application will hang while the console spams

But you can resize the egui::Window?
I'am referring to any thing that has "Window" as a name in viewports example.

The spamming of Set Size! is for testing, but should not hang the application!

Set size!
Set size!
Set size!
Set size!
Set size!

Operating system: Windows 10

Steps to reproduce: Start the viewport demo. Press "Show Async Window". (Or press all of them.) Then quickly toggle the "Force embedding". After a few tries, the application lands in that bugged state.

firefox_2023-08-23-0324.webm

You are sure that is a bugged state or is crashing, look in the console, but that thing is not happening to me!

@konkitoman
Copy link
Contributor Author

This is how is working for me!
And how should work!

OS: Arch Linux
Desktop Env: KDE Plasma X11

video.mp4

The video is on speed up some times to 130% or 200%, because i was needed to be less than 10MB

The starching is because of X11!

When appear two viewports from nowhere, is because "Sync Viewport" and "Async Viewport" has the same state for showing the two other viewports!

I unchecked every window/viewport from the main viewport at the end to show how the child viewport of the child viewport is closing!

@stefnotch
Copy link

It might be a Windows specific bug. It only seems to happen if I repeatedly, and quickly, toggle "Force embedding".

@konkitoman
Copy link
Contributor Author

konkitoman commented Aug 23, 2023

It might be a Windows specific bug. It only seems to happen if I repeatedly, and quickly, toggle "Force embedding".
So is not crashing?

@stefnotch
But you can resize the egui::Window?
I'am referring to some thing that has "Window" in name in checkbox buttons!

The main window is closing normally?

Because in Wine resize and closing is not working!
Wine is a program that let's linux users to run Windows apps without emulation!

"Force embedding" is some thing that should be set at the start of the program, but should work and at runtime!
But is not meant to be spammed, but still should not freeze the app!
Is probably some thing how winit::window::Window::request_redraw is working on Windows

@stefnotch
Copy link

@konkitoman I tested it on Windows 10.
"Show Async Window" and "Show Sync Window" leads to windows that

  • can be moved
  • cannot be resized
  • will glitch out when pressing the triangle button twice (close and reopen)

@konkitoman
Copy link
Contributor Author

Thanks @stefnotch

  • cannot be resized

The problem with resizing is fixed in winit 0.29.1-beta
I'am waiting for winit to get out of beta before updating!

  • will glitch out when pressing the triangle button twice (close and reopen)

If you are referring to this image is not really a bug is implemented badly, because if the window can be resize is only annoying!

Now i see that when you said:

Then quickly toggle the "Force embedding". After a few tries, the application lands in that bugged state.

The window is getting smaller and smaller, and the bugged state is happening when the window has width=1, at the beginning the blue bar i was thinking that was a encoder artifact!
Then the:

  • will glitch out when pressing the triangle button twice (close and reopen)

You are not referring to this, as glitch out?
image

@konkitoman konkitoman marked this pull request as ready for review August 30, 2023 11:54
@konkitoman konkitoman marked this pull request as draft August 30, 2023 11:55
@konkitoman
Copy link
Contributor Author

I wanted to see the auto merge conflict and i pressed "Ready for review" by mistake!

…wport_outer_pos, viewport_inner_size, viewport_outer_size are stored as inner_pos, outer_pos, inner_size, outer_pos and Now every (i32, i32) is stored as egui::Pos2

Addes some documentation
@konkitoman
Copy link
Contributor Author

konkitoman commented Sep 4, 2023

I see this as finished!

The only problem that i see that, for every sync viewport the fps is cut in half, because if a sync viewport needs to be redraw his parent needs to, the only fixes that i see is to set vsync to off or shadow update the parent!

@konkitoman konkitoman marked this pull request as ready for review September 5, 2023 07:50
@emilk
Copy link
Owner

emilk commented Nov 16, 2023

I think this is ready to be merged (finally!)

I've created an issue for follow-up work in #3556

@emilk emilk changed the title Adding support for multiples viewports/windows Multiple viewports/windows Nov 16, 2023
@emilk emilk merged commit 83aa310 into emilk:master Nov 16, 2023
19 checks passed
@Turun
Copy link

Turun commented Nov 20, 2023

Amazing!

I was very confused why the examples in this Repo did not compile for me, despite being on the latest release of egui/eframe. Then I noticed that this was only merged (at the time of surprise) 4h ago!

In case other people stumble here, you need to switch your Cargo.toml to:

eframe = { git = "https://github.com/emilk/egui/", rev = "83aa3109d31eb7ab09c40530ef4b7d5f3e370fd4" , features = ["persistence"]}
# other egui crates can be used like this too:
egui_plot = { git = "https://github.com/emilk/egui/", rev = "83aa3109d31eb7ab09c40530ef4b7d5f3e370fd4"}

(or any commit after this one, so that the multiple windows code is included)

emilk added a commit that referenced this pull request Nov 27, 2023
Added by @konkitoman in #3172

I'm not sure what the hack is supposed to solve, but calling it
every frame is a bad idea, as @ItsEthra reported in
#3628 (comment)

* Closes #3620
* Closes #3628
emilk added a commit that referenced this pull request Nov 27, 2023
This was caused by a hack added by @konkitoman in
#3172

The solution was reported by @ItsEthra in
#3628 (comment)

* Closes #3620
* Closes #3628
emilk added a commit that referenced this pull request Jan 17, 2024
This broke in #3172

Since 0.24.1, `stable_dt` has been fixed at 1/60s, which is a little
bit _too_ stable.

The code issue was the logic for asking "Is this the result of an
immeditate repaint?" was completely broken (always returning false).

* Closes #3830
@emilk emilk mentioned this pull request Jan 17, 2024
emilk added a commit that referenced this pull request Jan 17, 2024
This broke in #3172

Since 0.24.1, `stable_dt` has been fixed at 1/60s, which is a little bit
_too_ stable.

The code issue was the logic for asking "Is this the result of an
immeditate repaint?" was completely broken (always returning false).

* Closes #3830
emilk pushed a commit that referenced this pull request Jan 24, 2024
Addition for <#3847>
In previous one i only fixed crash occurring with Wgpu backend. This
fixes crash with Glow backend as well.
I only tested this change with android so most things i changed are
behind ```#[cfg(target_os = "android")]```.

Both fixes are dirty thought. As
<#3172> says that "The root viewport
is the original viewport, and cannot be closed without closing the
application.". So they break rules i guess? But i can't think about
better solution for now.

Closes <#3861>.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
eframe Relates to epi and eframe egui feature New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Multiple native windows
7 participants