mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
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>
This commit is contained in:
15
examples/multiple_viewports/Cargo.toml
Normal file
15
examples/multiple_viewports/Cargo.toml
Normal file
@@ -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
Normal file
7
examples/multiple_viewports/README.md
Normal file
@@ -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
Normal file
102
examples/multiple_viewports/src/main.rs
Normal file
@@ -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
Normal file
17
examples/test_viewports/Cargo.toml
Normal file
@@ -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
Normal file
3
examples/test_viewports/README.md
Normal file
@@ -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
Normal file
476
examples/test_viewports/src/main.rs
Normal file
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user