mirror of https://github.com/emilk/egui.git
Browse Source
* 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: [pull/3557/head78a0ae8
](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>
Konkitoman
12 months ago
committed by
GitHub
43 changed files with 5087 additions and 1273 deletions
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -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>, |
|||
} |
@ -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" |
@ -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]. |
@ -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
|
|||
} |
|||
}, |
|||
); |
|||
} |
|||
} |
|||
} |
@ -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" |
@ -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]. |
@ -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) |
|||
} |
Loading…
Reference in new issue