1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! OxiEguiApp — native egui/eframe integration (non-wasm32 only).
//!
//! This module provides `OxiEguiApp`, the `eframe::App` implementation that
//! drives the user's content closure, lifecycle hooks, and plugins through
//! egui's immediate-mode frame loop.
use crate::null_ctx::NullUiCtx;
use crate::runner::{LifecycleEvent, LifecycleSnapshot, LifecycleTracker};
use crate::{ContentFn, EguiFrameHook, HookFn, Plugin};
/// The eframe application struct that drives the OxiUI content closure.
///
/// Constructed inside `EguiRunner::run_native` and passed to
/// `eframe::run_native`. Not part of the public API.
pub struct OxiEguiApp {
pub content: Option<ContentFn>,
pub on_init: Vec<HookFn>,
pub on_frame: Vec<HookFn>,
pub plugins: Vec<Box<dyn Plugin>>,
pub initialised: bool,
/// If true, yield CPU when no input events occurred this frame.
pub frame_skip: bool,
/// Raw egui::Context escape-hatch callbacks.
pub egui_frame_hooks: Vec<EguiFrameHook>,
/// Hooks fired once when the window is closing (`eframe::App::on_exit`).
pub on_close: Vec<HookFn>,
/// Hooks fired when the viewport size changes.
pub on_resize: Vec<HookFn>,
/// Hooks fired when the window focus state flips.
pub on_focus: Vec<HookFn>,
/// Deduplicates raw size / focus / close snapshots into fired events.
pub tracker: LifecycleTracker,
}
impl OxiEguiApp {
/// Fire the hooks corresponding to a batch of deduplicated lifecycle events.
///
/// Hooks run against a [`NullUiCtx`]: lifecycle events fire outside a live
/// drawing frame, so there is no `EguiUiCtx` to bind them to.
fn dispatch_lifecycle(&mut self, events: &[LifecycleEvent]) {
if events.is_empty() {
return;
}
let mut null = NullUiCtx;
for ev in events {
match ev {
LifecycleEvent::Resized(_, _) => {
for hook in self.on_resize.iter_mut() {
hook(&mut null);
}
}
LifecycleEvent::Focus(_) => {
for hook in self.on_focus.iter_mut() {
hook(&mut null);
}
}
LifecycleEvent::Close => {
for hook in self.on_close.iter_mut() {
hook(&mut null);
}
}
}
}
}
}
impl eframe::App for OxiEguiApp {
/// Called each frame with the root [`egui::Ui`].
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
// Clone the context now (cheap Arc clone) so we can pass it to hooks
// without conflicting with the EguiUiCtx borrow below.
let egui_ctx = ui.ctx().clone();
let mut ctx_bridge = oxiui_egui::EguiUiCtx::new(ui);
// Fire init hooks exactly once.
if !self.initialised {
self.initialised = true;
for hook in self.on_init.iter_mut() {
hook(&mut ctx_bridge);
}
for plugin in self.plugins.iter_mut() {
plugin.init(&mut ctx_bridge);
}
}
// Content closure.
if let Some(ref mut f) = self.content {
f(&mut ctx_bridge);
}
// Per-frame hooks and plugin updates.
for hook in self.on_frame.iter_mut() {
hook(&mut ctx_bridge);
}
for plugin in self.plugins.iter_mut() {
plugin.update(&mut ctx_bridge);
}
// egui escape-hatch callbacks.
for hook in &mut self.egui_frame_hooks {
hook(&egui_ctx);
}
// Drop the UiCtx borrow before polling window lifecycle state.
drop(ctx_bridge);
// Poll viewport size + focus and fire resize / focus hooks on change.
let (size, focused) = egui_ctx.input(|i| {
let rect = i.viewport_rect();
((rect.width(), rect.height()), i.focused)
});
let events = self.tracker.observe(LifecycleSnapshot {
size: Some(size),
focused: Some(focused),
close_requested: false,
});
self.dispatch_lifecycle(&events);
// Frame-skip: if no input events occurred this frame, defer the next repaint.
if self.frame_skip && egui_ctx.input(|i| i.events.is_empty()) {
egui_ctx.request_repaint_after(std::time::Duration::from_secs(1));
}
}
/// Called once on shutdown (non-glow eframe signature).
///
/// Fires the `on_close` hooks exactly once via the shared
/// [`LifecycleTracker`] close guard.
fn on_exit(&mut self) {
let events = self.tracker.observe(LifecycleSnapshot {
size: None,
focused: None,
close_requested: true,
});
self.dispatch_lifecycle(&events);
}
}