Skip to content

Commit 9487dc3

Browse files
authored
Viewports: give the caller a Ui instead of Context (#7779)
* Part of #3524 This is a breaking change, as it changes the how embedded viewports work. Before it was up to the user to display a `egui::Window` if they wanted. Now egui creates an `egui::Window` for you, so you only need to add the contents. To signal this change in behavior, `ViewportClass::Embedded` is gone and is now called `ViewportClass::EmbeddedWindow`.
1 parent 4a81ca8 commit 9487dc3

File tree

7 files changed

+145
-81
lines changed

7 files changed

+145
-81
lines changed

crates/egui/src/containers/window.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,41 @@ impl<'open> Window<'open> {
7070
}
7171
}
7272

73+
/// Construct a [`Window`] that follows the given viewport.
74+
pub fn from_viewport(id: ViewportId, viewport: ViewportBuilder) -> Self {
75+
let ViewportBuilder {
76+
title,
77+
app_id,
78+
inner_size,
79+
min_inner_size,
80+
max_inner_size,
81+
resizable,
82+
decorations,
83+
title_shown,
84+
minimize_button,
85+
.. // A lot of things not implemented yet
86+
} = viewport;
87+
88+
let mut window = Self::new(title.or(app_id).unwrap_or_else(String::new)).id(Id::new(id));
89+
90+
if let Some(inner_size) = inner_size {
91+
window = window.default_size(inner_size);
92+
}
93+
if let Some(min_inner_size) = min_inner_size {
94+
window = window.min_size(min_inner_size);
95+
}
96+
if let Some(max_inner_size) = max_inner_size {
97+
window = window.max_size(max_inner_size);
98+
}
99+
if let Some(resizable) = resizable {
100+
window = window.resizable(resizable);
101+
}
102+
window = window.title_bar(decorations.unwrap_or(true) && title_shown.unwrap_or(true));
103+
window = window.collapsible(minimize_button.unwrap_or(true));
104+
105+
window
106+
}
107+
73108
/// Assign a unique id to the Window. Required if the title changes, or is shared with another window.
74109
#[inline]
75110
pub fn id(mut self, id: Id) -> Self {

crates/egui/src/context.rs

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ impl ContextImpl {
193193
pub struct ViewportState {
194194
/// The type of viewport.
195195
///
196-
/// This will never be [`ViewportClass::Embedded`],
196+
/// This will never be [`ViewportClass::EmbeddedWindow`],
197197
/// since those don't result in real viewports.
198198
pub class: ViewportClass,
199199

@@ -4013,21 +4013,23 @@ impl Context {
40134013
///
40144014
/// If [`Context::embed_viewports`] is `true` (e.g. if the current egui
40154015
/// backend does not support multiple viewports), the given callback
4016-
/// will be called immediately, embedding the new viewport in the current one.
4017-
/// You can check this with the [`ViewportClass`] given in the callback.
4018-
/// If you find [`ViewportClass::Embedded`], you need to create a new [`crate::Window`] for you content.
4016+
/// will be called immediately, embedding the new viewport in the current one,
4017+
/// inside of a [`crate::Window`].
4018+
/// You can know by checking for [`ViewportClass::EmbeddedWindow`].
40194019
///
40204020
/// See [`crate::viewport`] for more information about viewports.
40214021
pub fn show_viewport_deferred(
40224022
&self,
40234023
new_viewport_id: ViewportId,
40244024
viewport_builder: ViewportBuilder,
4025-
viewport_ui_cb: impl Fn(&Self, ViewportClass) + Send + Sync + 'static,
4025+
viewport_ui_cb: impl Fn(&mut Ui, ViewportClass) + Send + Sync + 'static,
40264026
) {
40274027
profiling::function_scope!();
40284028

40294029
if self.embed_viewports() {
4030-
viewport_ui_cb(self, ViewportClass::Embedded);
4030+
crate::Window::from_viewport(new_viewport_id, viewport_builder).show(self, |ui| {
4031+
viewport_ui_cb(ui, ViewportClass::EmbeddedWindow);
4032+
});
40314033
} else {
40324034
self.write(|ctx| {
40334035
ctx.viewport_parents
@@ -4038,7 +4040,9 @@ impl Context {
40384040
viewport.builder = viewport_builder;
40394041
viewport.used = true;
40404042
viewport.viewport_ui_cb = Some(Arc::new(move |ctx| {
4041-
(viewport_ui_cb)(ctx, ViewportClass::Deferred);
4043+
crate::CentralPanel::no_frame().show(ctx, |ui| {
4044+
(viewport_ui_cb)(ui, ViewportClass::Deferred);
4045+
});
40424046
}));
40434047
});
40444048
}
@@ -4065,28 +4069,32 @@ impl Context {
40654069
///
40664070
/// If [`Context::embed_viewports`] is `true` (e.g. if the current egui
40674071
/// backend does not support multiple viewports), the given callback
4068-
/// will be called immediately, embedding the new viewport in the current one.
4069-
/// You can check this with the [`ViewportClass`] given in the callback.
4070-
/// If you find [`ViewportClass::Embedded`], you need to create a new [`crate::Window`] for you content.
4072+
/// will be called immediately, embedding the new viewport in the current one,
4073+
/// inside of a [`crate::Window`].
4074+
/// You can know by checking for [`ViewportClass::EmbeddedWindow`].
40714075
///
40724076
/// See [`crate::viewport`] for more information about viewports.
40734077
pub fn show_viewport_immediate<T>(
40744078
&self,
40754079
new_viewport_id: ViewportId,
40764080
builder: ViewportBuilder,
4077-
mut viewport_ui_cb: impl FnMut(&Self, ViewportClass) -> T,
4081+
mut viewport_ui_cb: impl FnMut(&mut Ui, ViewportClass) -> T,
40784082
) -> T {
40794083
profiling::function_scope!();
40804084

40814085
if self.embed_viewports() {
4082-
return viewport_ui_cb(self, ViewportClass::Embedded);
4086+
return self.show_embedded_viewport(new_viewport_id, builder, |ui| {
4087+
viewport_ui_cb(ui, ViewportClass::EmbeddedWindow)
4088+
});
40834089
}
40844090

40854091
IMMEDIATE_VIEWPORT_RENDERER.with(|immediate_viewport_renderer| {
40864092
let immediate_viewport_renderer = immediate_viewport_renderer.borrow();
40874093
let Some(immediate_viewport_renderer) = immediate_viewport_renderer.as_ref() else {
40884094
// This egui backend does not support multiple viewports.
4089-
return viewport_ui_cb(self, ViewportClass::Embedded);
4095+
return self.show_embedded_viewport(new_viewport_id, builder, |ui| {
4096+
viewport_ui_cb(ui, ViewportClass::EmbeddedWindow)
4097+
});
40904098
};
40914099

40924100
let ids = self.write(|ctx| {
@@ -4110,8 +4118,10 @@ impl Context {
41104118
let viewport = ImmediateViewport {
41114119
ids,
41124120
builder,
4113-
viewport_ui_cb: Box::new(move |context| {
4114-
*out = Some(viewport_ui_cb(context, ViewportClass::Immediate));
4121+
viewport_ui_cb: Box::new(move |ctx| {
4122+
crate::CentralPanel::no_frame().show(ctx, |ui| {
4123+
*out = Some((viewport_ui_cb)(ui, ViewportClass::Immediate));
4124+
});
41154125
}),
41164126
};
41174127

@@ -4123,6 +4133,20 @@ impl Context {
41234133
)
41244134
})
41254135
}
4136+
4137+
fn show_embedded_viewport<T>(
4138+
&self,
4139+
new_viewport_id: ViewportId,
4140+
builder: ViewportBuilder,
4141+
viewport_ui_cb: impl FnOnce(&mut Ui) -> T,
4142+
) -> T {
4143+
crate::Window::from_viewport(new_viewport_id, builder)
4144+
.collapsible(false)
4145+
.show(self, |ui| viewport_ui_cb(ui))
4146+
.unwrap_or_else(|| panic!("Window did not show"))
4147+
.inner
4148+
.unwrap_or_else(|| panic!("Window was collapsed"))
4149+
}
41264150
}
41274151

41284152
/// ## Interaction

crates/egui/src/viewport.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
//! In short: immediate viewports are simpler to use, but can waste a lot of CPU time.
3434
//!
3535
//! ### Embedded viewports
36-
//! These are not real, independent 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 a [`crate::Window`] to wrap your ui in, which will then be embedded in the parent viewport, unable to escape it.
36+
//! These are not real, independent viewports, but is a fallback mode for when the integration does not support real viewports.
37+
//! In your callback is called with [`ViewportClass::EmbeddedWindow`] it means the viewport is embedded inside of
38+
//! a regular [`crate::Window`], trapped in the parent viewport.
3739
//!
3840
//!
3941
//! ## Using the viewports
@@ -101,7 +103,10 @@ pub enum ViewportClass {
101103

102104
/// The fallback, when the egui integration doesn't support viewports,
103105
/// or [`crate::Context::embed_viewports`] is set to `true`.
104-
Embedded,
106+
///
107+
/// If you get this, it is because you are already wrapped in a [`crate::Window`]
108+
/// inside of the parent viewport.
109+
EmbeddedWindow,
105110
}
106111

107112
// ----------------------------------------------------------------------------
@@ -1189,7 +1194,7 @@ pub struct ViewportOutput {
11891194

11901195
/// What type of viewport are we?
11911196
///
1192-
/// This will never be [`ViewportClass::Embedded`],
1197+
/// This will never be [`ViewportClass::EmbeddedWindow`],
11931198
/// since those don't result in real viewports.
11941199
pub class: ViewportClass,
11951200

crates/egui_demo_lib/src/demo/extra_viewport.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,12 @@ impl crate::Demo for ExtraViewport {
2222
egui::ViewportBuilder::default()
2323
.with_title(self.name())
2424
.with_inner_size([400.0, 512.0]),
25-
|ctx, class| {
26-
if class == egui::ViewportClass::Embedded {
25+
|ui, class| {
26+
if class == egui::ViewportClass::EmbeddedWindow {
2727
// Not a real viewport
28-
egui::Window::new(self.name())
29-
.id(id)
30-
.open(open)
31-
.show(ctx, |ui| {
32-
ui.label("This egui integration does not support multiple viewports");
33-
});
28+
ui.label("This egui integration does not support multiple viewports");
3429
} else {
35-
egui::CentralPanel::default().show(ctx, |ui| {
30+
egui::CentralPanel::default().show_inside(ui, |ui| {
3631
viewport_content(ui, ctx, open);
3732
});
3833
}
Lines changed: 2 additions & 2 deletions
Loading

examples/multiple_viewports/src/main.rs

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,20 @@ impl eframe::App for MyApp {
4444
"Show immediate child viewport",
4545
);
4646

47-
let mut show_deferred_viewport = self.show_deferred_viewport.load(Ordering::Relaxed);
48-
ui.checkbox(&mut show_deferred_viewport, "Show deferred child viewport");
49-
self.show_deferred_viewport
50-
.store(show_deferred_viewport, Ordering::Relaxed);
47+
{
48+
let mut show_deferred_viewport =
49+
self.show_deferred_viewport.load(Ordering::Relaxed);
50+
ui.checkbox(&mut show_deferred_viewport, "Show deferred child viewport");
51+
self.show_deferred_viewport
52+
.store(show_deferred_viewport, Ordering::Relaxed);
53+
}
54+
55+
ui.add_space(16.0);
56+
{
57+
let mut embedded = ui.embed_viewports();
58+
ui.checkbox(&mut embedded, "Embed all viewports");
59+
ui.set_embed_viewports(embedded);
60+
}
5161
});
5262

5363
if self.show_immediate_viewport {
@@ -56,19 +66,20 @@ impl eframe::App for MyApp {
5666
egui::ViewportBuilder::default()
5767
.with_title("Immediate Viewport")
5868
.with_inner_size([200.0, 100.0]),
59-
|ctx, class| {
60-
assert!(
61-
class == egui::ViewportClass::Immediate,
62-
"This egui backend doesn't support multiple viewports"
63-
);
64-
65-
egui::CentralPanel::default().show(ctx, |ui| {
66-
ui.label("Hello from immediate viewport");
67-
});
69+
|ui, class| {
70+
if class == egui::ViewportClass::EmbeddedWindow {
71+
ui.label(
72+
"This viewport is embedded in the parent window, and cannot be moved outside of it.",
73+
);
74+
} else {
75+
egui::CentralPanel::default().show_inside(ui, |ui| {
76+
ui.label("Hello from immediate viewport");
6877

69-
if ctx.input(|i| i.viewport().close_requested()) {
70-
// Tell parent viewport that we should not show next frame:
71-
self.show_immediate_viewport = false;
78+
if ui.input(|i| i.viewport().close_requested()) {
79+
// Tell parent viewport that we should not show next frame:
80+
self.show_immediate_viewport = false;
81+
}
82+
});
7283
}
7384
},
7485
);
@@ -81,18 +92,20 @@ impl eframe::App for MyApp {
8192
egui::ViewportBuilder::default()
8293
.with_title("Deferred Viewport")
8394
.with_inner_size([200.0, 100.0]),
84-
move |ctx, class| {
85-
assert!(
86-
class == egui::ViewportClass::Deferred,
87-
"This egui backend doesn't support multiple viewports"
88-
);
95+
move |ui, class| {
96+
if class == egui::ViewportClass::EmbeddedWindow {
97+
ui.label(
98+
"This viewport is embedded in the parent window, and cannot be moved outside of it.",
99+
);
100+
} else {
101+
egui::CentralPanel::default().show_inside(ui, |ui| {
102+
ui.label("Hello from deferred viewport");
89103

90-
egui::CentralPanel::default().show(ctx, |ui| {
91-
ui.label("Hello from deferred viewport");
92-
});
93-
if ctx.input(|i| i.viewport().close_requested()) {
94-
// Tell parent to close us.
95-
show_deferred_viewport.store(false, Ordering::Relaxed);
104+
if ui.input(|i| i.viewport().close_requested()) {
105+
// Tell parent to close us.
106+
show_deferred_viewport.store(false, Ordering::Relaxed);
107+
}
108+
});
96109
}
97110
},
98111
);

tests/test_viewports/src/main.rs

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -76,35 +76,29 @@ impl ViewportState {
7676

7777
if immediate {
7878
let mut vp_state = vp_state.write();
79-
ctx.show_viewport_immediate(vp_id, viewport, move |ctx, class| {
80-
if ctx.input(|i| i.viewport().close_requested()) {
79+
ctx.show_viewport_immediate(vp_id, viewport, move |ui, class| {
80+
if ui.input(|i| i.viewport().close_requested()) {
8181
vp_state.visible = false;
8282
}
83-
show_as_popup(ctx, class, &title, vp_id.into(), |ui: &mut egui::Ui| {
83+
show_as_popup(ui, class, |ui: &mut egui::Ui| {
8484
generic_child_ui(ui, &mut vp_state, close_button);
8585
});
8686
});
8787
} else {
8888
let count = Arc::new(RwLock::new(0));
89-
ctx.show_viewport_deferred(vp_id, viewport, move |ctx, class| {
89+
ctx.show_viewport_deferred(vp_id, viewport, move |ui, class| {
9090
let mut vp_state = vp_state.write();
91-
if ctx.input(|i| i.viewport().close_requested()) {
91+
if ui.input(|i| i.viewport().close_requested()) {
9292
vp_state.visible = false;
9393
}
9494
let count = count.clone();
95-
show_as_popup(
96-
ctx,
97-
class,
98-
&title,
99-
vp_id.into(),
100-
move |ui: &mut egui::Ui| {
101-
let current_count = *count.read();
102-
ui.label(format!("Callback has been reused {current_count} times"));
103-
*count.write() += 1;
104-
105-
generic_child_ui(ui, &mut vp_state, close_button);
106-
},
107-
);
95+
show_as_popup(ui, class, move |ui: &mut egui::Ui| {
96+
let current_count = *count.read();
97+
ui.label(format!("Callback has been reused {current_count} times"));
98+
*count.write() += 1;
99+
100+
generic_child_ui(ui, &mut vp_state, close_button);
101+
});
108102
});
109103
}
110104
}
@@ -180,17 +174,15 @@ impl eframe::App for App {
180174

181175
/// This will make the content as a popup if cannot has his own native window
182176
fn show_as_popup(
183-
ctx: &egui::Context,
177+
ui: &mut egui::Ui,
184178
class: egui::ViewportClass,
185-
title: &str,
186-
id: Id,
187179
content: impl FnOnce(&mut egui::Ui),
188180
) {
189-
if class == egui::ViewportClass::Embedded {
190-
// Not a real viewport
191-
egui::Window::new(title).id(id).show(ctx, content);
181+
if class == egui::ViewportClass::EmbeddedWindow {
182+
// Not a real viewport - already has a frame
183+
content(ui);
192184
} else {
193-
egui::CentralPanel::default().show(ctx, content);
185+
egui::CentralPanel::default().show_inside(ui, content);
194186
}
195187
}
196188

0 commit comments

Comments
 (0)