Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/eframe/src/epi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ pub trait App {
///
/// You may NOT show any ui or do any painting during the call to [`Self::logic`].
///
/// While the window is hidden, `eframe` runs no egui pass at all (so that no ui state is
/// disturbed), and calls this via [`egui::Context::run_logic`] instead.
/// You can then still tell that the window is hidden with
/// [`egui::InputState::viewport`], but the rest of [`egui::Context::input`]
/// (events, time, …) is that of the last shown frame.
///
/// The [`egui::Context`] can be cloned and saved if you like.
///
/// To force another call to [`Self::logic`], call [`egui::Context::request_repaint`] at any time (e.g. from another thread).
Expand Down
102 changes: 80 additions & 22 deletions crates/eframe/src/native/epi_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ pub struct EpiIntegration {
pub beginning: Instant,
is_first_frame: bool,
pub egui_ctx: egui::Context,

/// Input that we have received, but not yet given to egui,
/// because we haven't run any pass since (see [`Self::update_logic_only`]).
pending_raw_input: egui::RawInput,

pending_full_output: egui::FullOutput,

/// When set, it is time to close the native window.
Expand Down Expand Up @@ -215,6 +220,7 @@ impl EpiIntegration {
Self {
frame,
last_auto_save: Instant::now(),
pending_raw_input: Default::default(),
pending_full_output: Default::default(),
close: false,
can_drag_window: false,
Expand Down Expand Up @@ -262,59 +268,111 @@ impl EpiIntegration {

/// Run user code - this can create immediate viewports, so hold no locks over this!
///
/// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::ui`].
/// If `viewport_ui_cb` is None, we are in the root viewport and will call
/// [`crate::App::logic`] and [`crate::App::ui`].
///
/// Only call this when the ui will actually be shown;
/// use [`Self::update_logic_only`] otherwise.
pub fn update(
&mut self,
app: &mut dyn epi::App,
viewport_ui_cb: Option<&DeferredViewportUiCallback>,
mut raw_input: egui::RawInput,
is_visible: bool,
raw_input: egui::RawInput,
) -> egui::FullOutput {
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
let raw_input = self.prepare_raw_input(app, raw_input);

let close_requested = raw_input.viewport().close_requested();

app.raw_input_hook(&self.egui_ctx, &mut raw_input);
let is_root_viewport = viewport_ui_cb.is_none();

let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
if let Some(viewport_ui_cb) = viewport_ui_cb {
// Child viewport
if is_visible {
profiling::scope!("viewport_callback");
viewport_ui_cb(ui);
}
profiling::scope!("viewport_callback");
viewport_ui_cb(ui);
} else {
{
profiling::scope!("App::logic");
app.logic(ui.ctx(), &mut self.frame);
}

if is_visible {
{
profiling::scope!("App::ui");
app.ui(ui, &mut self.frame);
}
{
profiling::scope!("App::ui");
app.ui(ui, &mut self.frame);
}
}
});

let is_root_viewport = viewport_ui_cb.is_none();
if is_root_viewport && close_requested {
let canceled = full_output.viewport_output[&ViewportId::ROOT]
.commands
.contains(&egui::ViewportCommand::CancelClose);
if canceled {
log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose");
} else {
log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)");
self.close = true;
}
self.handle_close_request(canceled);
}

self.pending_full_output.append(full_output);
std::mem::take(&mut self.pending_full_output)
}

/// Let the app tick its logic without showing any ui,
/// because the window is minimized or occluded.
///
/// No egui pass is run, so all ui state is left untouched:
/// the app will find everything where it left it once the window is visible again.
///
/// Only call this for the root viewport: only it has [`crate::App::logic`].
pub fn update_logic_only(
&mut self,
app: &mut dyn epi::App,
raw_input: egui::RawInput,
) -> egui::LogicOutput {
let raw_input = self.prepare_raw_input(app, raw_input);

let close_requested = raw_input.viewport().close_requested();

let logic_output = self.egui_ctx.run_logic(&raw_input, |ctx| {
profiling::scope!("App::logic");
app.logic(ctx, &mut self.frame);
});

// No pass consumed the input, so save it for the next one:
self.pending_raw_input = raw_input;

if close_requested {
let canceled = logic_output
.viewport_commands
.get(&ViewportId::ROOT)
.is_some_and(|commands| commands.contains(&egui::ViewportCommand::CancelClose));
self.handle_close_request(canceled);
}

logic_output
}

/// Prepend any input we couldn't give to egui earlier, set the time, and run the app hook.
fn prepare_raw_input(
&mut self,
app: &mut dyn epi::App,
new_input: egui::RawInput,
) -> egui::RawInput {
let mut raw_input = std::mem::take(&mut self.pending_raw_input);
raw_input.append(new_input); // The new input wins where they overlap

raw_input.time = Some(self.beginning.elapsed().as_secs_f64());

app.raw_input_hook(&self.egui_ctx, &mut raw_input);

raw_input
}

fn handle_close_request(&mut self, canceled: bool) {
if canceled {
log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose");
} else {
log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)");
self.close = true;
}
}

pub fn report_frame_time(&mut self, seconds: f32) {
self.frame.info.cpu_usage = Some(seconds);
}
Expand Down
125 changes: 89 additions & 36 deletions crates/eframe/src/native/glow_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use super::{
use crate::epaint::textures::TexturesDelta;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
native::{epi_integration::EpiIntegration, winit_integration::sleep_if_invisible_or_minimized},
};

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -139,6 +139,27 @@ struct Viewport {
egui_winit: Option<egui_winit::State>,
}

impl Viewport {
/// Apply the commands, or defer them until we have a window.
fn process_commands(
&mut self,
egui_ctx: &egui::Context,
mut commands: Vec<egui::ViewportCommand>,
) {
self.deferred_commands.append(&mut commands);

if let Some(window) = &self.window {
egui_winit::process_viewport_commands(
egui_ctx,
&mut self.info,
std::mem::take(&mut self.deferred_commands),
window,
&mut self.actions_requested,
);
}
}
}

impl Drop for Viewport {
fn drop(&mut self) {
// Avoid debug panic when dropping unapplied deltas on teardown
Expand Down Expand Up @@ -579,7 +600,7 @@ impl GlowWinitRunning<'_> {
}
}

let (raw_input, viewport_ui_cb, is_visible, run_ui) = {
let (raw_input, viewport_ui_cb, is_visible, show_ui) = {
let mut glutin = self.glutin.borrow_mut();
let egui_ctx = glutin.egui_ctx.clone();
let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else {
Expand All @@ -598,7 +619,7 @@ impl GlowWinitRunning<'_> {
let mut raw_input = egui_winit.take_egui_input(window);
let viewport_ui_cb = viewport.viewport_ui_cb.clone();

let run_ui =
let show_ui =
is_visible || is_viewport_or_descendant_visible(&glutin.viewports, viewport_id);

self.integration.pre_update();
Expand All @@ -610,9 +631,58 @@ impl GlowWinitRunning<'_> {
.map(|(id, viewport)| (*id, viewport.info.clone()))
.collect();

(raw_input, viewport_ui_cb, is_visible, run_ui)
(raw_input, viewport_ui_cb, is_visible, show_ui)
};

if !show_ui {
// Nothing will be shown, so we run no egui pass at all.
// That way all ui state is left untouched, and is still there
// when this viewport becomes visible again.
let is_root_viewport = viewport_ui_cb.is_none();
if is_root_viewport {
// The app logic keeps ticking, so it can e.g. ask to be shown again:
let egui::LogicOutput {
platform_output,
viewport_commands,
} = self
.integration
.update_logic_only(self.app.as_mut(), raw_input);

let mut glutin = self.glutin.borrow_mut();
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
viewport.info.events.clear(); // they should have been processed
if let Some(window) = viewport.window.clone()
&& let Some(egui_winit) = viewport.egui_winit.as_mut()
{
egui_winit.handle_platform_output_with_event_loop(
&window,
event_loop,
platform_output,
);
}
}
for (id, commands) in viewport_commands {
if let Some(viewport) = glutin.viewports.get_mut(&id) {
viewport.process_commands(&self.integration.egui_ctx, commands);
}
}
}

sleep_if_invisible_or_minimized(
self.glutin
.borrow()
.viewports
.get(&viewport_id)
.and_then(|viewport| viewport.window.as_deref()),
);

return Ok(if self.integration.should_close() {
EventResult::CloseRequested
} else {
EventResult::Wait
});
}

// HACK: In order to get the right clear_color, the system theme needs to be set, which
// usually only happens in the `update` call. So we call Options::begin_pass early
// to set the right theme. Without this there would be a black flash on the first frame.
Expand Down Expand Up @@ -661,12 +731,9 @@ impl GlowWinitRunning<'_> {
// The update function, which could call immediate viewports,
// so make sure we don't hold any locks here required by the immediate viewports rendeer.

let full_output = self.integration.update(
self.app.as_mut(),
viewport_ui_cb.as_deref(),
raw_input,
run_ui,
);
let full_output =
self.integration
.update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input);

// ------------------------------------------------------------

Expand Down Expand Up @@ -800,14 +867,7 @@ impl GlowWinitRunning<'_> {

integration.maybe_autosave(app.as_mut(), Some(&window));

if is_invisible_or_minimized(&window) {
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
// On Windows, an invisible window also uses up all CPU:
// https://github.com/emilk/egui/issues/7776
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
sleep_if_invisible_or_minimized(Some(&window));

if integration.should_close() {
Ok(EventResult::CloseRequested)
Expand Down Expand Up @@ -1380,7 +1440,7 @@ impl GlutinWindowContext {
class,
builder,
viewport_ui_cb,
mut commands,
commands,
repaint_delay: _, // ignored - we listened to the repaint callback instead
},
) in viewport_output.clone()
Expand All @@ -1395,25 +1455,18 @@ impl GlutinWindowContext {
viewport_ui_cb,
);

if let Some(window) = &viewport.window {
let old_inner_size = window.inner_size();
let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size());

viewport.deferred_commands.append(&mut commands);
viewport.process_commands(egui_ctx, commands);

egui_winit::process_viewport_commands(
egui_ctx,
&mut viewport.info,
std::mem::take(&mut viewport.deferred_commands),
window,
&mut viewport.actions_requested,
);

// For Wayland : https://github.com/emilk/egui/issues/4196
if cfg!(target_os = "linux") {
let new_inner_size = window.inner_size();
if new_inner_size != old_inner_size {
self.resize(viewport_id, new_inner_size);
}
// For Wayland : https://github.com/emilk/egui/issues/4196
if cfg!(target_os = "linux")
&& let Some(window) = &viewport.window
&& let Some(old_inner_size) = old_inner_size
{
let new_inner_size = window.inner_size();
if new_inner_size != old_inner_size {
self.resize(viewport_id, new_inner_size);
}
}
}
Expand Down
Loading
Loading