mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 14:49:06 -04:00
Call logic even while browser tab is in background (#8257)
* Closes https://github.com/emilk/egui/issues/5112 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1400,6 +1400,7 @@ dependencies = [
|
|||||||
"egui_kittest",
|
"egui_kittest",
|
||||||
"image",
|
"image",
|
||||||
"jiff",
|
"jiff",
|
||||||
|
"log",
|
||||||
"mimalloc",
|
"mimalloc",
|
||||||
"rand 0.10.1",
|
"rand 0.10.1",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -113,12 +113,32 @@ fn install_blur_focus(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
|
|||||||
// NOTE: because of the text agent we sometime miss 'blur' events,
|
// NOTE: because of the text agent we sometime miss 'blur' events,
|
||||||
// so we also poll the focus state each frame in `AppRunner::logic`.
|
// so we also poll the focus state each frame in `AppRunner::logic`.
|
||||||
for event_name in ["blur", "focus", "visibilitychange"] {
|
for event_name in ["blur", "focus", "visibilitychange"] {
|
||||||
let closure = move |_event: web_sys::MouseEvent, runner: &mut AppRunner| {
|
let closure =
|
||||||
log::trace!("{} {event_name:?}", runner.canvas().id());
|
move |_event: web_sys::MouseEvent, runner: &mut AppRunner, web_runner: &WebRunner| {
|
||||||
runner.update_focus();
|
log::trace!("{} {event_name:?}", runner.canvas().id());
|
||||||
};
|
runner.update_focus();
|
||||||
|
|
||||||
runner_ref.add_event_listener(target, event_name, closure)?;
|
if event_name == "visibilitychange" {
|
||||||
|
// The tab was hidden or shown. An in-flight `requestAnimationFrame` is paused
|
||||||
|
// while hidden, so reschedule the paint loop using the scheduling mechanism
|
||||||
|
// (`setTimeout` vs `requestAnimationFrame`) appropriate for the new state.
|
||||||
|
// This keeps `App::update` running while hidden, and switches back to smooth
|
||||||
|
// animation frames once visible again.
|
||||||
|
if let Err(err) = web_runner.reschedule_frame() {
|
||||||
|
log::error!(
|
||||||
|
"Failed to reschedule frame on visibility change: {}",
|
||||||
|
super::string_from_js_value(&err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
runner_ref.add_event_listener_ex(
|
||||||
|
target,
|
||||||
|
event_name,
|
||||||
|
&web_sys::AddEventListenerOptions::default(),
|
||||||
|
closure,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,8 +129,7 @@ impl WebRunner {
|
|||||||
self.unsubscribe_from_all_events();
|
self.unsubscribe_from_all_events();
|
||||||
|
|
||||||
if let Some(frame) = self.frame.take() {
|
if let Some(frame) = self.frame.take() {
|
||||||
let window = web_sys::window().unwrap();
|
frame.cancel(&web_sys::window().unwrap());
|
||||||
window.cancel_animation_frame(frame.id).ok();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(runner) = self.app_runner.replace(None) {
|
if let Some(runner) = self.app_runner.replace(None) {
|
||||||
@@ -234,6 +233,12 @@ impl WebRunner {
|
|||||||
///
|
///
|
||||||
/// It is safe to call `request_animation_frame` multiple times in quick succession,
|
/// It is safe to call `request_animation_frame` multiple times in quick succession,
|
||||||
/// this function guarantees that only one animation frame is scheduled at a time.
|
/// this function guarantees that only one animation frame is scheduled at a time.
|
||||||
|
///
|
||||||
|
/// A hidden browser tab (e.g. a backgrounded tab) does not receive
|
||||||
|
/// `requestAnimationFrame` callbacks, which would otherwise stop our paint loop
|
||||||
|
/// and prevent `App::update` from running in response to `request_repaint`.
|
||||||
|
/// To keep running in that case, we fall back to `setTimeout`, which keeps firing
|
||||||
|
/// while hidden (the browser throttles it to roughly once per second).
|
||||||
pub(crate) fn request_animation_frame(&self) -> Result<(), wasm_bindgen::JsValue> {
|
pub(crate) fn request_animation_frame(&self) -> Result<(), wasm_bindgen::JsValue> {
|
||||||
if self.frame.borrow().is_some() {
|
if self.frame.borrow().is_some() {
|
||||||
// there is already an animation frame in flight
|
// there is already an animation frame in flight
|
||||||
@@ -252,14 +257,47 @@ impl WebRunner {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let id = window.request_animation_frame(closure.as_ref().unchecked_ref())?;
|
let hidden = window.document().is_some_and(|document| document.hidden());
|
||||||
self.frame.borrow_mut().replace(AnimationFrameRequest {
|
|
||||||
id,
|
let request = if hidden {
|
||||||
_closure: closure,
|
// The tab is hidden: `requestAnimationFrame` would not fire, so use a timer instead.
|
||||||
});
|
let id = window.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||||
|
closure.as_ref().unchecked_ref(),
|
||||||
|
// Browsers clamp background timers to ~1s, so the exact value has little effect
|
||||||
|
// while hidden, but keeps us responsive right after the tab is hidden:
|
||||||
|
10,
|
||||||
|
)?;
|
||||||
|
AnimationFrameRequest {
|
||||||
|
id,
|
||||||
|
kind: AnimationFrameKind::Timeout,
|
||||||
|
_closure: closure,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let id = window.request_animation_frame(closure.as_ref().unchecked_ref())?;
|
||||||
|
AnimationFrameRequest {
|
||||||
|
id,
|
||||||
|
kind: AnimationFrameKind::AnimationFrame,
|
||||||
|
_closure: closure,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.frame.borrow_mut().replace(request);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cancel any in-flight frame request and schedule a fresh one.
|
||||||
|
///
|
||||||
|
/// Called on `visibilitychange` so we switch between `requestAnimationFrame` (visible)
|
||||||
|
/// and `setTimeout` (hidden) scheduling. This is necessary because an in-flight
|
||||||
|
/// `requestAnimationFrame` is paused while the tab is hidden, which would otherwise
|
||||||
|
/// stall the paint loop and stop `App::update` from running while hidden.
|
||||||
|
pub(crate) fn reschedule_frame(&self) -> Result<(), wasm_bindgen::JsValue> {
|
||||||
|
if let Some(frame) = self.frame.borrow_mut().take() {
|
||||||
|
frame.cancel(&web_sys::window().unwrap());
|
||||||
|
}
|
||||||
|
self.request_animation_frame()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
@@ -269,11 +307,37 @@ struct AnimationFrameRequest {
|
|||||||
/// Represents the ID of a frame in flight.
|
/// Represents the ID of a frame in flight.
|
||||||
id: i32,
|
id: i32,
|
||||||
|
|
||||||
|
/// How the frame was scheduled, so we know how to cancel it.
|
||||||
|
kind: AnimationFrameKind,
|
||||||
|
|
||||||
/// The callback given to `request_animation_frame`, stored here both to prevent it
|
/// The callback given to `request_animation_frame`, stored here both to prevent it
|
||||||
/// from being canceled, and from having to `.forget()` it.
|
/// from being canceled, and from having to `.forget()` it.
|
||||||
_closure: Closure<dyn FnMut() -> Result<(), JsValue>>,
|
_closure: Closure<dyn FnMut() -> Result<(), JsValue>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AnimationFrameRequest {
|
||||||
|
/// Cancel the in-flight frame request, using the API matching how it was scheduled.
|
||||||
|
fn cancel(&self, window: &web_sys::Window) {
|
||||||
|
match self.kind {
|
||||||
|
AnimationFrameKind::AnimationFrame => {
|
||||||
|
window.cancel_animation_frame(self.id).ok();
|
||||||
|
}
|
||||||
|
AnimationFrameKind::Timeout => {
|
||||||
|
window.clear_timeout_with_handle(self.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How an [`AnimationFrameRequest`] was scheduled.
|
||||||
|
enum AnimationFrameKind {
|
||||||
|
/// Scheduled with `requestAnimationFrame` (visible tab).
|
||||||
|
AnimationFrame,
|
||||||
|
|
||||||
|
/// Scheduled with `setTimeout` (hidden tab).
|
||||||
|
Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
struct TargetEvent {
|
struct TargetEvent {
|
||||||
target: web_sys::EventTarget,
|
target: web_sys::EventTarget,
|
||||||
event_name: String,
|
event_name: String,
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ pub(crate) fn seconds_since_midnight() -> f64 {
|
|||||||
pub trait DemoApp {
|
pub trait DemoApp {
|
||||||
fn demo_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame);
|
fn demo_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame);
|
||||||
|
|
||||||
|
/// Run background logic, called every frame even when the app is hidden.
|
||||||
|
///
|
||||||
|
/// See [`eframe::App::logic`].
|
||||||
|
fn logic(&mut self, _ctx: &egui::Context) {}
|
||||||
|
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {}
|
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ impl DemoApp for DemoWindows {
|
|||||||
fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||||
self.ui(ui);
|
self.ui(ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn logic(&mut self, ctx: &egui::Context) {
|
||||||
|
self.logic(ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
@@ -280,6 +284,14 @@ impl eframe::App for WrapApp {
|
|||||||
color.to_normalized_gamma_f32()
|
color.to_normalized_gamma_f32()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
// Run background logic for every app, even the ones not currently shown,
|
||||||
|
// so they keep working while the app is hidden (e.g. a backgrounded tab).
|
||||||
|
for (_name, _anchor, app) in self.apps_iter_mut() {
|
||||||
|
app.logic(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
if let Some(anchor) = frame
|
if let Some(anchor) = frame
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ syntect = ["egui_extras/syntect"]
|
|||||||
egui = { workspace = true, default-features = false, features = ["color-hex"] }
|
egui = { workspace = true, default-features = false, features = ["color-hex"] }
|
||||||
egui_extras = { workspace = true, features = ["image", "svg"] }
|
egui_extras = { workspace = true, features = ["image", "svg"] }
|
||||||
|
|
||||||
|
log.workspace = true
|
||||||
unicode_names2.workspace = true # this old version has fewer dependencies
|
unicode_names2.workspace = true # this old version has fewer dependencies
|
||||||
|
|
||||||
#! ### Optional dependencies
|
#! ### Optional dependencies
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ impl DemoGroup {
|
|||||||
set_open(open, demo.name(), is_open);
|
set_open(open, demo.name(), is_open);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn logic(&mut self, ctx: &egui::Context) {
|
||||||
|
for demo in &mut self.demos {
|
||||||
|
demo.logic(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_open(open: &mut BTreeSet<String>, key: &'static str, is_open: bool) {
|
fn set_open(open: &mut BTreeSet<String>, key: &'static str, is_open: bool) {
|
||||||
@@ -160,6 +166,11 @@ impl DemoGroups {
|
|||||||
demos.windows(ui, open);
|
demos.windows(ui, open);
|
||||||
tests.windows(ui, open);
|
tests.windows(ui, open);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn logic(&mut self, ctx: &egui::Context) {
|
||||||
|
self.demos.logic(ctx);
|
||||||
|
self.tests.logic(ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
@@ -212,6 +223,13 @@ impl DemoWindows {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run background logic for all demos.
|
||||||
|
///
|
||||||
|
/// Called every frame, even when hidden, so demos can keep working in the background.
|
||||||
|
pub fn logic(&mut self, ctx: &egui::Context) {
|
||||||
|
self.groups.logic(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
fn about_is_open(&self) -> bool {
|
fn about_is_open(&self) -> bool {
|
||||||
self.open.contains(About::default().name())
|
self.open.contains(About::default().name())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub struct MiscDemoWindow {
|
|||||||
tree: Tree,
|
tree: Tree,
|
||||||
box_painting: BoxPainting,
|
box_painting: BoxPainting,
|
||||||
text_rotation: TextRotation,
|
text_rotation: TextRotation,
|
||||||
|
repaint: Repaint,
|
||||||
|
|
||||||
dummy_bool: bool,
|
dummy_bool: bool,
|
||||||
dummy_usize: usize,
|
dummy_usize: usize,
|
||||||
@@ -36,6 +37,7 @@ impl Default for MiscDemoWindow {
|
|||||||
tree: Tree::demo(),
|
tree: Tree::demo(),
|
||||||
box_painting: Default::default(),
|
box_painting: Default::default(),
|
||||||
text_rotation: Default::default(),
|
text_rotation: Default::default(),
|
||||||
|
repaint: Default::default(),
|
||||||
|
|
||||||
dummy_bool: false,
|
dummy_bool: false,
|
||||||
dummy_usize: 0,
|
dummy_usize: 0,
|
||||||
@@ -57,6 +59,10 @@ impl Demo for MiscDemoWindow {
|
|||||||
.constrain_to(ui.available_rect_before_wrap())
|
.constrain_to(ui.available_rect_before_wrap())
|
||||||
.show(ui, |ui| self.ui(ui));
|
.show(ui, |ui| self.ui(ui));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn logic(&mut self, ctx: &egui::Context) {
|
||||||
|
self.repaint.logic(ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View for MiscDemoWindow {
|
impl View for MiscDemoWindow {
|
||||||
@@ -102,6 +108,10 @@ impl View for MiscDemoWindow {
|
|||||||
.default_open(false)
|
.default_open(false)
|
||||||
.show(ui, |ui| self.tree.ui(ui));
|
.show(ui, |ui| self.tree.ui(ui));
|
||||||
|
|
||||||
|
CollapsingHeader::new("Repaint")
|
||||||
|
.default_open(false)
|
||||||
|
.show(ui, |ui| self.repaint.ui(ui));
|
||||||
|
|
||||||
CollapsingHeader::new("Checkboxes")
|
CollapsingHeader::new("Checkboxes")
|
||||||
.default_open(false)
|
.default_open(false)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
@@ -292,6 +302,134 @@ impl Widgets {
|
|||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Demonstrates [`egui::Context::request_repaint`] and
|
||||||
|
/// [`egui::Context::request_repaint_after`].
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
|
struct Repaint {
|
||||||
|
/// Request a repaint every frame, so we run as fast as the integration allows.
|
||||||
|
repaint_continuously: bool,
|
||||||
|
|
||||||
|
/// Request a repaint after [`Self::delay`].
|
||||||
|
repaint_after_delay: bool,
|
||||||
|
|
||||||
|
/// How long to wait before the next repaint when [`Self::repaint_after_delay`] is set.
|
||||||
|
delay: f64,
|
||||||
|
|
||||||
|
/// Issue the repaint requests from `logic` (which runs even while hidden) instead of `ui`.
|
||||||
|
in_background: bool,
|
||||||
|
|
||||||
|
/// Log each `ui` and `logic` frame, so background activity is visible in the console.
|
||||||
|
log_each_frame: bool,
|
||||||
|
|
||||||
|
/// How many times [`Self::ui`] has run since the last reset.
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
|
ui_count: u64,
|
||||||
|
|
||||||
|
/// How many times [`Self::logic`] has run since the last reset.
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
|
logic_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Repaint {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
repaint_continuously: false,
|
||||||
|
repaint_after_delay: false,
|
||||||
|
delay: 1.0,
|
||||||
|
in_background: false,
|
||||||
|
log_each_frame: false,
|
||||||
|
ui_count: 0,
|
||||||
|
logic_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Repaint {
|
||||||
|
fn ui(&mut self, ui: &mut Ui) {
|
||||||
|
self.ui_count += 1;
|
||||||
|
if self.log_each_frame {
|
||||||
|
log::info!("Repaint demo: `ui` frame {}", self.ui_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.label("Use this to verify if logic is correctly called while in background.");
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.button("Reset counts").clicked() {
|
||||||
|
self.ui_count = 0;
|
||||||
|
self.logic_count = 0;
|
||||||
|
}
|
||||||
|
ui.label(format!(
|
||||||
|
"`ui`: {}, `logic`: {}",
|
||||||
|
self.ui_count, self.logic_count
|
||||||
|
))
|
||||||
|
.on_hover_text(
|
||||||
|
"`ui` is incremented in `App::ui` (only runs while visible), \
|
||||||
|
`logic` in `App::logic` (runs even while hidden).",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.checkbox(
|
||||||
|
&mut self.repaint_continuously,
|
||||||
|
"Repaint continuously (every frame)",
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.checkbox(&mut self.repaint_after_delay, "Repaint after");
|
||||||
|
ui.add_enabled(
|
||||||
|
self.repaint_after_delay,
|
||||||
|
Slider::new(&mut self.delay, 0.0..=5.0)
|
||||||
|
.suffix(" s")
|
||||||
|
.text("delay"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.checkbox(&mut self.in_background, "In the background (during logic)")
|
||||||
|
.on_hover_text(
|
||||||
|
"Issue the repaint requests from `App::logic` (which runs even while hidden) \
|
||||||
|
instead of `App::ui` (which is skipped while hidden).\n\n\
|
||||||
|
With this enabled, hide this tab for a while, then come back: \
|
||||||
|
the `logic` count will have kept climbing.",
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.checkbox(&mut self.log_each_frame, "Log each frame")
|
||||||
|
.on_hover_text("Log each `ui` and `logic` frame to the console.");
|
||||||
|
|
||||||
|
// When not in background mode, drive the repaints from here (`ui`), which only
|
||||||
|
// runs while visible. Otherwise they are driven from `logic` (see below).
|
||||||
|
if !self.in_background {
|
||||||
|
self.request_repaint(ui.ctx());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs even when the app is hidden, unlike [`Self::ui`].
|
||||||
|
fn logic(&mut self, ctx: &egui::Context) {
|
||||||
|
self.logic_count += 1;
|
||||||
|
if self.log_each_frame {
|
||||||
|
log::info!("Repaint demo: `logic` frame {}", self.logic_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.in_background {
|
||||||
|
self.request_repaint(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request repaints according to the selected options.
|
||||||
|
fn request_repaint(&self, ctx: &egui::Context) {
|
||||||
|
if self.repaint_continuously {
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
if self.repaint_after_delay {
|
||||||
|
ctx.request_repaint_after(std::time::Duration::from_secs_f64(self.delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
|
|||||||
@@ -62,4 +62,8 @@ pub trait Demo {
|
|||||||
|
|
||||||
/// Show windows, etc
|
/// Show windows, etc
|
||||||
fn show(&mut self, ui: &mut egui::Ui, open: &mut bool);
|
fn show(&mut self, ui: &mut egui::Ui, open: &mut bool);
|
||||||
|
|
||||||
|
/// Run background logic, called every frame even when the demo window is closed
|
||||||
|
/// or the app is hidden.
|
||||||
|
fn logic(&mut self, _ctx: &egui::Context) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2909f098b5edacefef1b5c4d81982b0c84ebd27f896934f0bef92ceb283bbe79
|
oid sha256:a825dc9c62979fbb8d1ca3d441b4d9e7dbbd234b994901026f35fe7d591ff196
|
||||||
size 60288
|
size 60217
|
||||||
|
|||||||
Reference in New Issue
Block a user